From a3891f6f5ae1bcc502118f858300d75f19a22029 Mon Sep 17 00:00:00 2001 From: reudismam Date: Tue, 30 Jan 2018 12:50:20 -0300 Subject: [PATCH 001/376] Use the pattern ''string literal''.equals(something) to prevent NPE as it is done in many other locations in the codebase. --- .../android/exoplayer2/demo/DemoApplication.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 14 +++++++------- .../exoplayer2/metadata/id3/Id3Decoder.java | 2 +- .../exoplayer2/upstream/DefaultHttpDataSource.java | 6 +++--- .../com/google/android/exoplayer2/util/Util.java | 4 ++-- .../source/dash/manifest/DashManifestParser.java | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 5d019e4c53..bc2ca738bc 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -48,7 +48,7 @@ public class DemoApplication extends Application { } public boolean useExtensionRenderers() { - return BuildConfig.FLAVOR.equals("withExtensions"); + return "withExtensions".equals(BuildConfig.FLAVOR); } } 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 7ae8eb3cd4..47ca5cf1cb 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 @@ -354,15 +354,15 @@ public final class MediaCodecUtil { // Work around https://github.com/google/ExoPlayer/issues/3249. if (Util.SDK_INT < 24 && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) - && Util.MANUFACTURER.equals("samsung") + && "samsung".equals(Util.MANUFACTURER) && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ - || Util.DEVICE.equals("SC-05G") // Galaxy S6 - || Util.DEVICE.equals("marinelteatt") // Galaxy S6 Active - || Util.DEVICE.equals("404SC") // Galaxy S6 Edge - || Util.DEVICE.equals("SC-04G") - || Util.DEVICE.equals("SCV31"))) { + || "SC-05G".equals(Util.DEVICE) // Galaxy S6 + || "marinelteatt".equals(Util.DEVICE) // Galaxy S6 Active + || "404SC".equals(Util.DEVICE) // Galaxy S6 Edge + || "SC-04G".equals(Util.DEVICE) + || "SCV31".equals(Util.DEVICE))) { return false; } @@ -421,7 +421,7 @@ public final class MediaCodecUtil { */ private static boolean codecNeedsDisableAdaptationWorkaround(String name) { return Util.SDK_INT <= 22 - && (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10")) + && ("ODROID-XU3".equals(Util.MODEL) || "Nexus 10".equals(Util.MODEL)) && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } 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 6b2e5c3675..5ffb6ccb7e 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 @@ -529,7 +529,7 @@ public final class Id3Decoder implements MetadataDecoder { if (majorVersion == 2) { mimeTypeEndIndex = 2; mimeType = "image/" + Util.toLowerInvariant(new String(data, 0, 3, "ISO-8859-1")); - if (mimeType.equals("image/jpg")) { + if ("image/jpg".equals(mimeType)) { mimeType = "image/jpeg"; } } else { 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 599cdddeb9..e8b7f00a56 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 @@ -613,9 +613,9 @@ public class DefaultHttpDataSource implements HttpDataSource { return; } String className = inputStream.getClass().getName(); - if (className.equals("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream") - || className.equals( - "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream")) { + if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className) + || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream".equals( + className)) { Class superclass = inputStream.getClass().getSuperclass(); Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); unexpectedEndOfInput.setAccessible(true); 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 ceefe758bb..a0c5b4a624 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 @@ -160,7 +160,7 @@ public final class Util { */ public static boolean isLocalFileUri(Uri uri) { String scheme = uri.getScheme(); - return TextUtils.isEmpty(scheme) || scheme.equals("file"); + return TextUtils.isEmpty(scheme) || "file".equals(scheme); } /** @@ -629,7 +629,7 @@ public final class Util { } else { timezoneShift = ((Integer.parseInt(matcher.group(12)) * 60 + Integer.parseInt(matcher.group(13)))); - if (matcher.group(11).equals("-")) { + if ("-".equals(matcher.group(11))) { timezoneShift *= -1; } } 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 bda2a1fb85..ab0a06745f 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 @@ -112,7 +112,7 @@ public class DashManifestParser extends DefaultHandler long durationMs = parseDuration(xpp, "mediaPresentationDuration", C.TIME_UNSET); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", C.TIME_UNSET); String typeString = xpp.getAttributeValue(null, "type"); - boolean dynamic = typeString != null && typeString.equals("dynamic"); + boolean dynamic = typeString != null && "dynamic".equals(typeString); long minUpdateTimeMs = dynamic ? parseDuration(xpp, "minimumUpdatePeriod", C.TIME_UNSET) : C.TIME_UNSET; long timeShiftBufferDepthMs = dynamic From b3da82dc1c9bfb0c5de78e3b001b38fe55098977 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 24 Jan 2018 02:54:44 -0800 Subject: [PATCH 002/376] Open source DownloadService, DownloadManager and related classes Issue: #2643 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184844484 --- RELEASENOTES.md | 4 + core_settings.gradle | 2 + extensions/jobdispatcher/README.md | 23 + extensions/jobdispatcher/build.gradle | 43 ++ .../src/main/AndroidManifest.xml | 18 + .../jobdispatcher/JobDispatcherScheduler.java | 200 +++++ .../offline/DownloadManagerTest.java | 707 +++++++++++++++++ .../exoplayer2/offline/ActionFile.java | 96 +++ .../exoplayer2/offline/DownloadAction.java | 164 ++++ .../exoplayer2/offline/DownloadManager.java | 717 ++++++++++++++++++ .../exoplayer2/offline/DownloadService.java | 396 ++++++++++ .../offline/ProgressiveDownloadAction.java | 122 +++ .../offline/SegmentDownloadAction.java | 142 ++++ .../util/scheduler/PlatformScheduler.java | 195 +++++ .../util/scheduler/Requirements.java | 226 ++++++ .../util/scheduler/RequirementsWatcher.java | 211 ++++++ .../exoplayer2/util/scheduler/Scheduler.java | 39 + .../exoplayer2/offline/ActionFileTest.java | 252 ++++++ .../ProgressiveDownloadActionTest.java | 142 ++++ .../dash/offline/DownloadManagerDashTest.java | 243 ++++++ .../dash/offline/DownloadServiceDashTest.java | 212 ++++++ .../dash/offline/TestDownloadListener.java | 74 ++ .../dash/offline/DashDownloadAction.java | 85 +++ .../source/hls/offline/HlsDownloadAction.java | 80 ++ .../offline/SsDownloadAction.java | 83 ++ .../ui/DownloadNotificationUtil.java | 114 +++ 26 files changed, 4590 insertions(+) create mode 100644 extensions/jobdispatcher/README.md create mode 100644 extensions/jobdispatcher/build.gradle create mode 100644 extensions/jobdispatcher/src/main/AndroidManifest.xml create mode 100644 extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 01ddda93f8..1171d35411 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -89,6 +89,10 @@ * `EventLogger` moved from the demo app into the core library. * Fix ANR issue on Huawei P8 Lite ([#3724](https://github.com/google/ExoPlayer/issues/3724)). +* Fix potential NPE when removing media sources from a + DynamicConcatenatingMediaSource + ([#3796](https://github.com/google/ExoPlayer/issues/3796)). +* Open source DownloadService, DownloadManager and related classes. ### 2.6.1 ### diff --git a/core_settings.gradle b/core_settings.gradle index 20a7c87bde..5687f19396 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -35,6 +35,7 @@ include modulePrefix + 'extension-opus' include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' include modulePrefix + 'extension-leanback' +include modulePrefix + 'extension-jobdispatcher' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -54,6 +55,7 @@ project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensi project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') 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') if (gradle.ext.has('exoplayerIncludeCronetExtension') && gradle.ext.exoplayerIncludeCronetExtension) { diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md new file mode 100644 index 0000000000..d9efc77012 --- /dev/null +++ b/extensions/jobdispatcher/README.md @@ -0,0 +1,23 @@ +# ExoPlayer Firebase JobDispatcher extension # + +This extension provides a Scheduler implementation which uses [Firebase JobDispatcher][]. + +[Firebase JobDispatcher]: https://github.com/firebase/firebase-jobdispatcher-android + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-jobdispatcher:rX.X.X' +``` + +where `rX.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/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle new file mode 100644 index 0000000000..fd5fce9ec8 --- /dev/null +++ b/extensions/jobdispatcher/build.gradle @@ -0,0 +1,43 @@ +/* + * 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 + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile 'com.firebase:firebase-jobdispatcher:0.8.5' +} + +ext { + javadocTitle = 'Firebase JobDispatcher extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-jobdispatcher' + releaseDescription = 'Firebase JobDispatcher extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/jobdispatcher/src/main/AndroidManifest.xml b/extensions/jobdispatcher/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..306a087e6c --- /dev/null +++ b/extensions/jobdispatcher/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + 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 new file mode 100644 index 0000000000..908e7f26c7 --- /dev/null +++ b/extensions/jobdispatcher/src/main/java/com/google/android/exoplayer2/ext/jobdispatcher/JobDispatcherScheduler.java @@ -0,0 +1,200 @@ +/* + * 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.jobdispatcher; + +import android.app.Notification; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import com.firebase.jobdispatcher.Constraint; +import com.firebase.jobdispatcher.FirebaseJobDispatcher; +import com.firebase.jobdispatcher.GooglePlayDriver; +import com.firebase.jobdispatcher.Job; +import com.firebase.jobdispatcher.Job.Builder; +import com.firebase.jobdispatcher.JobParameters; +import com.firebase.jobdispatcher.JobService; +import com.firebase.jobdispatcher.Lifetime; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.util.scheduler.Requirements; +import com.google.android.exoplayer2.util.scheduler.Scheduler; + +/** + * A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to + * schedule a {@link Service} to be started when its requirements are met. The started service must + * call {@link Service#startForeground(int, Notification)} to make itself a foreground service upon + * being started, as documented by {@link Service#startForegroundService(Intent)}. + * + *

To use {@link JobDispatcherScheduler} application needs to have RECEIVE_BOOT_COMPLETED + * permission and you need to define JobDispatcherSchedulerService in your manifest: + * + *

{@literal
+ * 
+ *
+ * 
+ *   
+ *     
+ *   
+ * 
+ * }
+ * + * The service to be scheduled must be defined in the manifest with an intent-filter: + * + *
{@literal
+ * 
+ *  
+ *    
+ *    
+ *  
+ * 
+ * }
+ * + *

This Scheduler uses Google Play services but does not do any availability checks. Any uses + * should be guarded with a call to {@code + * GoogleApiAvailability#isGooglePlayServicesAvailable(android.content.Context)} + * + * @see GoogleApiAvailability + */ +public final class JobDispatcherScheduler implements Scheduler { + + private static final String TAG = "JobDispatcherScheduler"; + private static final String SERVICE_ACTION = "SERVICE_ACTION"; + private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; + private static final String REQUIREMENTS = "REQUIREMENTS"; + + private final String jobTag; + private final Job job; + private final FirebaseJobDispatcher jobDispatcher; + + /** + * @param context Used to create a {@link FirebaseJobDispatcher} service. + * @param requirements The requirements to execute the job. + * @param jobTag Unique tag for the job. Using the same tag as a previous job can cause that job + * to be replaced or canceled. + * @param serviceAction The action which the service will be started with. + * @param servicePackage The package of the service which contains the logic of the job. + */ + public JobDispatcherScheduler( + Context context, + Requirements requirements, + String jobTag, + String serviceAction, + String servicePackage) { + this.jobDispatcher = new FirebaseJobDispatcher(new GooglePlayDriver(context)); + this.jobTag = jobTag; + this.job = buildJob(jobDispatcher, requirements, jobTag, serviceAction, servicePackage); + } + + @Override + public boolean schedule() { + int result = jobDispatcher.schedule(job); + logd("Scheduling JobDispatcher job: " + jobTag + " result: " + result); + return result == FirebaseJobDispatcher.SCHEDULE_RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + int result = jobDispatcher.cancel(jobTag); + logd("Canceling JobDispatcher job: " + jobTag + " result: " + result); + return result == FirebaseJobDispatcher.CANCEL_RESULT_SUCCESS; + } + + private static Job buildJob( + FirebaseJobDispatcher dispatcher, + Requirements requirements, + String tag, + String serviceAction, + String servicePackage) { + Builder builder = + dispatcher + .newJobBuilder() + .setService(JobDispatcherSchedulerService.class) // the JobService that will be called + .setTag(tag); + + switch (requirements.getRequiredNetworkType()) { + case Requirements.NETWORK_TYPE_NONE: + // do nothing. + break; + case Requirements.NETWORK_TYPE_ANY: + builder.addConstraint(Constraint.ON_ANY_NETWORK); + break; + case Requirements.NETWORK_TYPE_UNMETERED: + builder.addConstraint(Constraint.ON_UNMETERED_NETWORK); + break; + default: + throw new UnsupportedOperationException(); + } + + if (requirements.isIdleRequired()) { + builder.addConstraint(Constraint.DEVICE_IDLE); + } + if (requirements.isChargingRequired()) { + builder.addConstraint(Constraint.DEVICE_CHARGING); + } + builder.setLifetime(Lifetime.FOREVER).setReplaceCurrent(true); + + // Extras, work duration. + Bundle extras = new Bundle(); + extras.putString(SERVICE_ACTION, serviceAction); + extras.putString(SERVICE_PACKAGE, servicePackage); + extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); + + builder.setExtras(extras); + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link JobService} to start a service if the requirements are met. */ + public static final class JobDispatcherSchedulerService extends JobService { + @Override + public boolean onStartJob(JobParameters params) { + logd("JobDispatcherSchedulerService is started"); + Bundle extras = params.getExtras(); + Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + logd("requirements are met"); + String serviceAction = extras.getString(SERVICE_ACTION); + String servicePackage = extras.getString(SERVICE_PACKAGE); + Intent intent = new Intent(serviceAction).setPackage(servicePackage); + logd("starting service action: " + serviceAction + " package: " + servicePackage); + if (Util.SDK_INT >= 26) { + startForegroundService(intent); + } else { + startService(intent); + } + } else { + logd("requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java new file mode 100644 index 0000000000..85af6c32cd --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -0,0 +1,707 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.ConditionVariable; +import android.support.annotation.Nullable; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; +import com.google.android.exoplayer2.testutil.MockitoUtil; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.Util; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import org.mockito.Mockito; + +/** Tests {@link DownloadManager}. */ +public class DownloadManagerTest extends InstrumentationTestCase { + + /* Used to check if condition becomes true in this time interval. */ + private static final int ASSERT_TRUE_TIMEOUT = 10000; + /* Used to check if condition stays false for this time interval. */ + private static final int ASSERT_FALSE_TIME = 1000; + /* Maximum retry delay in DownloadManager. */ + private static final int MAX_RETRY_DELAY = 5000; + + private static final int MIN_RETRY_COUNT = 3; + + private DownloadManager downloadManager; + private File actionFile; + private TestDownloadListener testDownloadListener; + + @Override + public void setUp() throws Exception { + super.setUp(); + MockitoUtil.setUpMockito(this); + + actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); + testDownloadListener = new TestDownloadListener(); + setUpDownloadManager(100); + } + + @Override + public void tearDown() throws Exception { + releaseDownloadManager(); + actionFile.delete(); + super.tearDown(); + } + + private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { + if (downloadManager != null) { + releaseDownloadManager(); + } + try { + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY), + maxActiveDownloadTasks, + MIN_RETRY_COUNT, + actionFile.getAbsolutePath()); + downloadManager.addListener(testDownloadListener); + downloadManager.startDownloads(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + private void releaseDownloadManager() throws Exception { + try { + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.release(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + public void testDownloadActionRuns() throws Throwable { + doTestActionRuns(createDownloadAction("media 1")); + } + + public void testRemoveActionRuns() throws Throwable { + doTestActionRuns(createRemoveAction("media 1")); + } + + public void testDownloadRetriesThenFails() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction("media 1"); + downloadAction.post(); + FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + fakeDownloader.enableDownloadIOException = true; + for (int i = 0; i <= MIN_RETRY_COUNT; i++) { + fakeDownloader.assertStarted(MAX_RETRY_DELAY).unblock(); + } + downloadAction.assertError(); + testDownloadListener.clearDownloadError(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testDownloadNoRetryWhenCancelled() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); + downloadAction.getFakeDownloader().enableDownloadIOException = true; + downloadAction.post().assertStarted(); + + FakeDownloadAction removeAction = createRemoveAction("media 1").post(); + + downloadAction.unblock().assertCancelled(); + removeAction.unblock(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testDownloadRetriesThenContinues() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction("media 1"); + downloadAction.post(); + FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + fakeDownloader.enableDownloadIOException = true; + for (int i = 0; i <= MIN_RETRY_COUNT; i++) { + fakeDownloader.assertStarted(MAX_RETRY_DELAY); + if (i == MIN_RETRY_COUNT) { + fakeDownloader.enableDownloadIOException = false; + } + fakeDownloader.unblock(); + } + downloadAction.assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) + public void testDownloadRetryCountResetsOnProgress() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction("media 1"); + downloadAction.post(); + FakeDownloader fakeDownloader = downloadAction.getFakeDownloader(); + fakeDownloader.enableDownloadIOException = true; + fakeDownloader.downloadedBytes = 0; + for (int i = 0; i <= MIN_RETRY_COUNT + 10; i++) { + fakeDownloader.assertStarted(MAX_RETRY_DELAY); + fakeDownloader.downloadedBytes++; + if (i == MIN_RETRY_COUNT + 10) { + fakeDownloader.enableDownloadIOException = false; + } + fakeDownloader.unblock(); + } + downloadAction.assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createDownloadAction("media 2")); + } + + public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createRemoveAction("media 2")); + } + + public void testSameMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createDownloadAction("media 1")); + } + + public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { + doTestActionsRunSequentially(createDownloadAction("media 1"), + createRemoveAction("media 1")); + } + + public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction("media 1"), + createDownloadAction("media 1")); + } + + public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction("media 1"), + createRemoveAction("media 1")); + } + + public void testSameMediaMultipleActions() throws Throwable { + FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction("media 1"); + FakeDownloadAction downloadAction3 = createDownloadAction("media 1"); + FakeDownloadAction removeAction2 = createRemoveAction("media 1"); + + // Two download actions run in parallel. + downloadAction1.post().assertStarted(); + downloadAction2.post().assertStarted(); + // removeAction1 is added. It interrupts the two download actions' threads but they are + // configured to ignore it so removeAction1 doesn't start. + removeAction1.post().assertDoesNotStart(); + + // downloadAction2 finishes but it isn't enough to start removeAction1. + downloadAction2.unblock().assertCancelled(); + removeAction1.assertDoesNotStart(); + // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. + downloadAction3.post().assertDoesNotStart(); + + // When downloadAction1 finishes, removeAction1 starts. + downloadAction1.unblock().assertCancelled(); + removeAction1.assertStarted(); + // downloadAction3 still waits removeAction1 + downloadAction3.assertDoesNotStart(); + + // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 + // starts immediately. + removeAction2.post(); + removeAction1.assertCancelled(); + downloadAction3.assertCancelled(); + removeAction2.assertStarted().unblock().assertEnded(); + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { + FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); + FakeDownloadAction removeAction2 = createRemoveAction("media 1"); + FakeDownloadAction removeAction3 = createRemoveAction("media 1"); + + removeAction1.post().assertStarted(); + removeAction2.post().assertDoesNotStart(); + removeAction3.post().assertDoesNotStart(); + + removeAction2.assertCancelled(); + + removeAction1.unblock().assertCancelled(); + removeAction3.assertStarted().unblock().assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testGetTasks() throws Throwable { + FakeDownloadAction removeAction = createRemoveAction("media 1"); + FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); + FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + DownloadState[] states = downloadManager.getDownloadStates(); + assertThat(states).hasLength(3); + assertThat(states[0].downloadAction).isEqualTo(removeAction); + assertThat(states[1].downloadAction).isEqualTo(downloadAction1); + assertThat(states[2].downloadAction).isEqualTo(downloadAction2); + } + + public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { + FakeDownloadAction removeAction = createRemoveAction("media 1"); + FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); + FakeDownloadAction downloadAction2 = createDownloadAction("media 1"); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + removeAction.unblock().assertEnded(); + downloadAction1.assertStarted(); + downloadAction2.assertStarted(); + downloadAction1.unblock().assertEnded(); + downloadAction2.unblock().assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { + FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); + FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); + FakeDownloadAction downloadAction2 = createDownloadAction("media 2"); + + removeAction.post().assertStarted(); + downloadAction1.post().assertDoesNotStart(); + downloadAction2.post().assertDoesNotStart(); + + removeAction.unblock().assertEnded(); + downloadAction1.assertStarted(); + downloadAction2.assertStarted(); + downloadAction1.unblock().assertEnded(); + downloadAction2.unblock().assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { + FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction("media 1"); + FakeDownloadAction removeAction2 = createRemoveAction("media 2"); + + downloadAction.post().assertStarted(); + removeAction1.post().assertDoesNotStart(); + removeAction2.post().assertStarted(); + + downloadAction.unblock().assertCancelled(); + removeAction2.unblock().assertEnded(); + + removeAction1.assertStarted(); + removeAction1.unblock().assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testStopAndResume() throws Throwable { + FakeDownloadAction download1Action = createDownloadAction("media 1"); + FakeDownloadAction remove2Action = createRemoveAction("media 2"); + FakeDownloadAction download2Action = createDownloadAction("media 2"); + FakeDownloadAction remove1Action = createRemoveAction("media 1"); + FakeDownloadAction download3Action = createDownloadAction("media 3"); + + download1Action.post().assertStarted(); + remove2Action.post().assertStarted(); + download2Action.post().assertDoesNotStart(); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.stopDownloads(); + } + }); + + download1Action.assertStopped(); + + // remove actions aren't stopped. + remove2Action.unblock().assertEnded(); + // Although remove2Action is finished, download2Action doesn't start. + download2Action.assertDoesNotStart(); + + // When a new remove action is added, it cancels stopped download actions with the same media. + remove1Action.post(); + download1Action.assertCancelled(); + remove1Action.assertStarted().unblock().assertEnded(); + + // New download actions can be added but they don't start. + download3Action.post().assertDoesNotStart(); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.startDownloads(); + } + }); + + download2Action.assertStarted().unblock().assertEnded(); + download3Action.assertStarted().unblock().assertEnded(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testResumeBeforeTotallyStopped() throws Throwable { + setUpDownloadManager(2); + FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction download2Action = createDownloadAction("media 2"); + FakeDownloadAction download3Action = createDownloadAction("media 3"); + + download1Action.post().assertStarted(); + download2Action.post().assertStarted(); + // download3Action doesn't start as DM was configured to run two downloads in parallel. + download3Action.post().assertDoesNotStart(); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.stopDownloads(); + } + }); + + // download1Action doesn't stop yet as it ignores interrupts. + download2Action.assertStopped(); + + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.startDownloads(); + } + }); + + // download2Action starts immediately. + download2Action.assertStarted(); + + // download3Action doesn't start as download1Action still holds its slot. + download3Action.assertDoesNotStart(); + + // when unblocked download1Action stops and starts immediately. + download1Action.unblock().assertStopped().assertStarted(); + + download1Action.unblock(); + download2Action.unblock(); + download3Action.unblock(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionRuns(FakeDownloadAction action) throws Throwable { + action.post().assertStarted().unblock().assertEnded(); + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunSequentially(FakeDownloadAction action1, + FakeDownloadAction action2) throws Throwable { + action1.ignoreInterrupts().post().assertStarted(); + action2.post().assertDoesNotStart(); + + action1.unblock(); + action2.assertStarted(); + + action2.unblock().assertEnded(); + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunInParallel(FakeDownloadAction action1, + FakeDownloadAction action2) throws Throwable { + action1.post().assertStarted(); + action2.post().assertStarted(); + action1.unblock().assertEnded(); + action2.unblock().assertEnded(); + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private FakeDownloadAction createDownloadAction(String mediaId) { + return new FakeDownloadAction(mediaId, false); + } + + private FakeDownloadAction createRemoveAction(String mediaId) { + return new FakeDownloadAction(mediaId, true); + } + + private static final class TestDownloadListener implements DownloadListener { + + private ConditionVariable downloadFinishedCondition; + private Throwable downloadError; + + private TestDownloadListener() { + downloadFinishedCondition = new ConditionVariable(); + } + + @Override + public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { + if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) { + downloadError = downloadState.error; + } + ((FakeDownloadAction) downloadState.downloadAction).onStateChange(downloadState.state); + } + + @Override + public void onIdle(DownloadManager downloadManager) { + downloadFinishedCondition.open(); + } + + private void clearDownloadError() { + this.downloadError = null; + } + + private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + assertThat(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue(); + downloadFinishedCondition.close(); + if (downloadError != null) { + throw new Exception(downloadError); + } + } + + } + + private class FakeDownloadAction extends DownloadAction { + + private final String mediaId; + private final boolean removeAction; + private final FakeDownloader downloader; + private final BlockingQueue states; + + private FakeDownloadAction(String mediaId, boolean removeAction) { + super(mediaId); + this.mediaId = mediaId; + this.removeAction = removeAction; + this.downloader = new FakeDownloader(removeAction); + this.states = new ArrayBlockingQueue<>(10); + } + + @Override + protected String getType() { + return "FakeDownloadAction"; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + // do nothing. + } + + @Override + public boolean isRemoveAction() { + return removeAction; + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + return other instanceof FakeDownloadAction + && mediaId.equals(((FakeDownloadAction) other).mediaId); + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return downloader; + } + + private FakeDownloader getFakeDownloader() { + return downloader; + } + + private FakeDownloadAction post() throws Throwable { + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + downloadManager.handleAction(FakeDownloadAction.this); + } + }); + return this; + } + + private FakeDownloadAction assertDoesNotStart() { + assertThat(downloader.started.block(ASSERT_FALSE_TIME)).isFalse(); + return this; + } + + private FakeDownloadAction assertStarted() { + downloader.assertStarted(ASSERT_TRUE_TIMEOUT); + return assertState(DownloadState.STATE_STARTED); + } + + private FakeDownloadAction assertEnded() { + return assertState(DownloadState.STATE_ENDED); + } + + private FakeDownloadAction assertError() { + return assertState(DownloadState.STATE_ERROR); + } + + private FakeDownloadAction assertCancelled() { + return assertState(DownloadState.STATE_CANCELED); + } + + private FakeDownloadAction assertStopped() { + assertState(DownloadState.STATE_STOPPING); + return assertState(DownloadState.STATE_WAITING); + } + + private FakeDownloadAction assertState(@State int expectedState) { + ArrayList receivedStates = new ArrayList<>(); + while (true) { + Integer state = null; + try { + state = states.poll(ASSERT_TRUE_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail(e.getMessage()); + } + if (state != null) { + if (expectedState == state) { + return this; + } + receivedStates.add(state); + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < receivedStates.size(); i++) { + if (i > 0) { + sb.append(','); + } + sb.append(DownloadState.getStateString(receivedStates.get(i))); + } + fail( + String.format( + Locale.US, + "expected:<%s> but was:<%s>", + DownloadState.getStateString(expectedState), + sb)); + } + } + } + + private FakeDownloadAction unblock() { + downloader.unblock(); + return this; + } + + private FakeDownloadAction ignoreInterrupts() { + downloader.ignoreInterrupts = true; + return this; + } + + private void onStateChange(int state) { + states.add(state); + } + } + + private static class FakeDownloader implements Downloader { + private final ConditionVariable started; + private final com.google.android.exoplayer2.util.ConditionVariable blocker; + private final boolean removeAction; + private boolean ignoreInterrupts; + private volatile boolean enableDownloadIOException; + private volatile int downloadedBytes = C.LENGTH_UNSET; + + private FakeDownloader(boolean removeAction) { + this.removeAction = removeAction; + this.started = new ConditionVariable(); + this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); + } + + @Override + public void init() throws InterruptedException, IOException { + // do nothing. + } + + @Override + public void download(@Nullable ProgressListener listener) + throws InterruptedException, IOException { + assertThat(removeAction).isFalse(); + started.open(); + block(); + if (enableDownloadIOException) { + throw new IOException(); + } + } + + @Override + public void remove() throws InterruptedException { + assertThat(removeAction).isTrue(); + started.open(); + block(); + } + + private void block() throws InterruptedException { + try { + while (true) { + try { + blocker.block(); + break; + } catch (InterruptedException e) { + if (!ignoreInterrupts) { + throw e; + } + } + } + } finally { + blocker.close(); + } + } + + private FakeDownloader assertStarted(int timeout) { + assertThat(started.block(timeout)).isTrue(); + started.close(); + return this; + } + + private FakeDownloader unblock() { + blocker.open(); + return this; + } + + @Override + public long getDownloadedBytes() { + return downloadedBytes; + } + + @Override + public float getDownloadPercentage() { + return Float.NaN; + } + } + +} 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 new file mode 100644 index 0000000000..0724755ac7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -0,0 +1,96 @@ +/* + * 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.offline; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * Stores and loads {@link DownloadAction}s to/from a file. + */ +public final class ActionFile { + + private final AtomicFile atomicFile; + private final File actionFile; + + /** + * @param actionFile File to be used to store and load {@link DownloadAction}s. + */ + public ActionFile(File actionFile) { + this.actionFile = actionFile; + atomicFile = new AtomicFile(actionFile); + } + + /** + * Loads {@link DownloadAction}s from file. + * + * @param deserializers {@link Deserializer}s to deserialize DownloadActions. + * @return Loaded DownloadActions. If the action file doesn't exists returns an empty array. + * @throws IOException If there is an error during loading. + */ + public DownloadAction[] load(Deserializer... deserializers) throws IOException { + if (!actionFile.exists()) { + return new DownloadAction[0]; + } + InputStream inputStream = null; + try { + inputStream = atomicFile.openRead(); + DataInputStream dataInputStream = new DataInputStream(inputStream); + int version = dataInputStream.readInt(); + if (version > DownloadAction.MASTER_VERSION) { + throw new IOException("Not supported action file version: " + version); + } + int actionCount = dataInputStream.readInt(); + DownloadAction[] actions = new DownloadAction[actionCount]; + for (int i = 0; i < actionCount; i++) { + actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version); + } + return actions; + } finally { + Util.closeQuietly(inputStream); + } + } + + /** + * Stores {@link DownloadAction}s to file. + * + * @param downloadActions DownloadActions to store to file. + * @throws IOException If there is an error during storing. + */ + public void store(DownloadAction... downloadActions) throws IOException { + DataOutputStream output = null; + try { + output = new DataOutputStream(atomicFile.startWrite()); + output.writeInt(DownloadAction.MASTER_VERSION); + output.writeInt(downloadActions.length); + for (DownloadAction action : downloadActions) { + DownloadAction.serializeToStream(action, output); + } + atomicFile.endWrite(output); + // Avoid calling close twice. + output = null; + } finally { + Util.closeQuietly(output); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java new file mode 100644 index 0000000000..eee9f0530c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -0,0 +1,164 @@ +/* + * 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.offline; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** Contains the necessary parameters for a download or remove action. */ +public abstract class DownloadAction { + + /** + * Master version for all {@link DownloadAction} serialization/deserialization implementations. On + * each change on any {@link DownloadAction} serialization format this version needs to be + * increased. + */ + public static final int MASTER_VERSION = 0; + + /** Used to deserialize {@link DownloadAction}s. */ + public interface Deserializer { + + /** Returns the type string of the {@link DownloadAction}. This string should be unique. */ + String getType(); + + /** + * Deserializes a {@link DownloadAction} from the {@code input}. + * + * @param version Version of the data. + * @param input DataInputStream to read data from. + * @see DownloadAction#writeToStream(DataOutputStream) + * @see DownloadAction#MASTER_VERSION + */ + DownloadAction readFromStream(int version, DataInputStream input) throws IOException; + } + + /** + * Deserializes one {@code action} which was serialized by {@link + * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the + * {@link Deserializer}s which supports the type of the action. + * + *

The caller is responsible for closing the given {@link InputStream}. + * + * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}. + * @param input Input stream to read serialized data. + * @return The deserialized {@link DownloadAction}. + * @throws IOException If there is an IO error from {@code input} or the action type isn't + * supported by any of the {@code deserializers}. + */ + public static DownloadAction deserializeFromStream( + Deserializer[] deserializers, InputStream input) throws IOException { + return deserializeFromStream(deserializers, input, MASTER_VERSION); + } + + /** + * Deserializes one {@code action} which was serialized by {@link + * #serializeToStream(DownloadAction, OutputStream)} from the {@code input} using one of the + * {@link Deserializer}s which supports the type of the action. + * + *

The caller is responsible for closing the given {@link InputStream}. + * + * @param deserializers Array of {@link Deserializer}s to deserialize a {@link DownloadAction}. + * @param input Input stream to read serialized data. + * @param version Master version of the serialization. See {@link DownloadAction#MASTER_VERSION}. + * @return The deserialized {@link DownloadAction}. + * @throws IOException If there is an IO error from {@code input}. + * @throws DownloadException If the action type isn't supported by any of the {@code + * deserializers}. + */ + public static DownloadAction deserializeFromStream( + Deserializer[] deserializers, InputStream input, int version) throws IOException { + // Don't close the stream as it closes the underlying stream too. + DataInputStream dataInputStream = new DataInputStream(input); + String type = dataInputStream.readUTF(); + for (Deserializer deserializer : deserializers) { + if (type.equals(deserializer.getType())) { + return deserializer.readFromStream(version, dataInputStream); + } + } + throw new DownloadException("No Deserializer can be found to parse the data."); + } + + /** Serializes {@code action} type and data into the {@code output}. */ + public static void serializeToStream(DownloadAction action, OutputStream output) + throws IOException { + // Don't close the stream as it closes the underlying stream too. + DataOutputStream dataOutputStream = new DataOutputStream(output); + dataOutputStream.writeUTF(action.getType()); + action.writeToStream(dataOutputStream); + dataOutputStream.flush(); + } + + private final String data; + + /** @param data Optional custom data for this action. If null, an empty string is used. */ + protected DownloadAction(String data) { + this.data = data != null ? data : ""; + } + + /** Serializes itself into a byte array. */ + public final byte[] toByteArray() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try { + serializeToStream(this, output); + } catch (IOException e) { + // ByteArrayOutputStream shouldn't throw IOException. + throw new IllegalStateException(); + } + return output.toByteArray(); + } + + /** Returns custom data for this action. */ + public final String getData() { + return data; + } + + /** Returns whether this is a remove action or a download action. */ + public abstract boolean isRemoveAction(); + + /** Returns the type string of the {@link DownloadAction}. This string should be unique. */ + protected abstract String getType(); + + /** Serializes itself into the {@code output}. */ + protected abstract void writeToStream(DataOutputStream output) throws IOException; + + /** Returns whether this is action is for the same media as the {@code other}. */ + protected abstract boolean isSameMedia(DownloadAction other); + + /** Creates a {@link Downloader} with the given parameters. */ + protected abstract Downloader createDownloader( + DownloaderConstructorHelper downloaderConstructorHelper); + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + DownloadAction that = (DownloadAction) o; + return data.equals(that.data) && isRemoveAction() == that.isRemoveAction(); + } + + @Override + public int hashCode() { + int result = data.hashCode(); + result = 31 * result + (isRemoveAction() ? 1 : 0); + return result; + } + +} 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 new file mode 100644 index 0000000000..2df2069a89 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -0,0 +1,717 @@ +/* + * 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.offline; + +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELED; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_CANCELING; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ENDED; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_ERROR; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STARTED; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_STOPPING; +import static com.google.android.exoplayer2.offline.DownloadManager.DownloadState.STATE_WAITING; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.support.annotation.IntDef; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Manages multiple stream download and remove requests. + * + *

By default downloads are stopped. Call {@link #startDownloads()} to start downloads. + * + *

WARNING: Methods of this class must be called only on the main thread of the application. + */ +public final class DownloadManager { + + /** + * Listener for download events. Listener methods are called on the main thread of the + * application. + */ + public interface DownloadListener { + /** + * Called on download state change. + * + * @param downloadManager The reporting instance. + * @param downloadState The download task. + */ + void onStateChange(DownloadManager downloadManager, DownloadState downloadState); + + /** + * Called when there is no active task left. + * + * @param downloadManager The reporting instance. + */ + void onIdle(DownloadManager downloadManager); + } + + private static final String TAG = "DownloadManager"; + private static final boolean DEBUG = false; + + private final DownloaderConstructorHelper downloaderConstructorHelper; + private final int maxActiveDownloadTasks; + private final int minRetryCount; + private final ActionFile actionFile; + private final DownloadAction.Deserializer[] deserializers; + private final ArrayList tasks; + private final ArrayList activeDownloadTasks; + private final Handler handler; + private final HandlerThread fileIOThread; + private final Handler fileIOHandler; + private final CopyOnWriteArraySet listeners; + + private int nextTaskId; + private boolean actionFileLoadCompleted; + private boolean released; + private boolean downloadsStopped; + + /** + * Constructs a {@link DownloadManager}. + * + * @param constructorHelper A {@link DownloaderConstructorHelper} to create {@link Downloader}s + * for downloading data. + * @param maxActiveDownloadTasks Max number of download tasks to be started in parallel. + * @param minRetryCount The minimum number of times the downloads must be retried before failing. + * @param actionSaveFile File to save active actions. + * @param deserializers Used to deserialize {@link DownloadAction}s. + */ + public DownloadManager( + DownloaderConstructorHelper constructorHelper, + int maxActiveDownloadTasks, + int minRetryCount, + String actionSaveFile, + Deserializer... deserializers) { + this.downloaderConstructorHelper = constructorHelper; + this.maxActiveDownloadTasks = maxActiveDownloadTasks; + this.minRetryCount = minRetryCount; + this.actionFile = new ActionFile(new File(actionSaveFile)); + this.deserializers = deserializers; + this.downloadsStopped = true; + + tasks = new ArrayList<>(); + activeDownloadTasks = new ArrayList<>(); + handler = new Handler(Looper.getMainLooper()); + + fileIOThread = new HandlerThread("DownloadManager file i/o"); + fileIOThread.start(); + fileIOHandler = new Handler(fileIOThread.getLooper()); + + listeners = new CopyOnWriteArraySet<>(); + + loadActions(); + logd("DownloadManager is created"); + } + + /** + * Stops all of the tasks and releases resources. If the action file isn't up to date, + * waits for the changes to be written. + */ + public void release() { + released = true; + for (int i = 0; i < tasks.size(); i++) { + tasks.get(i).stop(); + } + final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); + fileIOHandler.post(new Runnable() { + @Override + public void run() { + fileIOFinishedCondition.open(); + } + }); + fileIOFinishedCondition.block(); + fileIOThread.quit(); + logd("DownloadManager is released"); + } + + /** Stops all of the download tasks. Call {@link #startDownloads()} to restart tasks. */ + public void stopDownloads() { + if (!downloadsStopped) { + downloadsStopped = true; + for (int i = 0; i < activeDownloadTasks.size(); i++) { + activeDownloadTasks.get(i).stop(); + } + logd("Downloads are stopping"); + } + } + + /** Starts the download tasks. */ + public void startDownloads() { + if (downloadsStopped) { + downloadsStopped = false; + maybeStartTasks(); + logd("Downloads are started"); + } + } + + /** + * Adds a {@link DownloadListener}. + * + * @param listener The listener to be added. + */ + public void addListener(DownloadListener listener) { + listeners.add(listener); + } + + /** + * Removes a {@link DownloadListener}. + * + * @param listener The listener to be removed. + */ + public void removeListener(DownloadListener listener) { + listeners.remove(listener); + } + + /** + * Deserializes one {@link DownloadAction} from {@code actionData} and calls {@link + * #handleAction(DownloadAction)}. + * + * @param actionData Serialized {@link DownloadAction} data. + * @return The task id. + * @throws IOException If an error occurs during handling action. + */ + public int handleAction(byte[] actionData) throws IOException { + ByteArrayInputStream input = new ByteArrayInputStream(actionData); + DownloadAction action = DownloadAction.deserializeFromStream(deserializers, input); + return handleAction(action); + } + + /** + * Handles the given {@link DownloadAction}. A task is created and added to the task queue. If + * it's a remove action then this method cancels any download tasks which works on the same media + * immediately. + * + * @param downloadAction Action to be executed. + * @return The task id. + */ + public int handleAction(DownloadAction downloadAction) { + DownloadTask downloadTask = createDownloadTask(downloadAction); + saveActions(); + if (downloadsStopped && !downloadAction.isRemoveAction()) { + logd("Can't start the task as downloads are stopped", downloadTask); + } else { + maybeStartTasks(); + } + return downloadTask.id; + } + + private DownloadTask createDownloadTask(DownloadAction downloadAction) { + DownloadTask downloadTask = new DownloadTask(nextTaskId++, this, downloadAction, minRetryCount); + tasks.add(downloadTask); + logd("Task is added", downloadTask); + notifyListenersTaskStateChange(downloadTask); + return downloadTask; + } + + /** Returns number of tasks. */ + public int getTaskCount() { + return tasks.size(); + } + + /** Returns a {@link DownloadTask} for a task. */ + public DownloadState getDownloadState(int taskId) { + for (int i = 0; i < tasks.size(); i++) { + DownloadTask task = tasks.get(i); + if (task.id == taskId) { + return task.getDownloadState(); + } + } + return null; + } + + /** Returns {@link DownloadState}s for all tasks. */ + public DownloadState[] getDownloadStates() { + return getDownloadStates(tasks); + } + + /** Returns an array of {@link DownloadState}s for active download tasks. */ + public DownloadState[] getActiveDownloadStates() { + return getDownloadStates(activeDownloadTasks); + } + + /** Returns whether there are no active tasks. */ + public boolean isIdle() { + if (!actionFileLoadCompleted) { + return false; + } + for (int i = 0; i < tasks.size(); i++) { + if (tasks.get(i).isRunning()) { + return false; + } + } + return true; + } + + /** + * Iterates through the task queue and starts any task if all of the following are true: + * + *

    + *
  • It hasn't started yet. + *
  • There are no preceding conflicting tasks. + *
  • If it's a download task then there are no preceding download tasks on hold and the + * maximum number of active downloads hasn't been reached. + *
+ * + * If the task is a remove action then preceding conflicting tasks are canceled. + */ + private void maybeStartTasks() { + if (released) { + return; + } + + boolean skipDownloadActions = downloadsStopped + || activeDownloadTasks.size() == maxActiveDownloadTasks; + for (int i = 0; i < tasks.size(); i++) { + DownloadTask downloadTask = tasks.get(i); + if (!downloadTask.canStart()) { + continue; + } + + DownloadAction downloadAction = downloadTask.downloadAction; + boolean removeAction = downloadAction.isRemoveAction(); + if (!removeAction && skipDownloadActions) { + continue; + } + + boolean canStartTask = true; + for (int j = 0; j < i; j++) { + DownloadTask task = tasks.get(j); + if (task.downloadAction.isSameMedia(downloadAction)) { + if (removeAction) { + canStartTask = false; + logd(downloadTask + " clashes with " + task); + task.cancel(); + // Continue loop to cancel any other preceding clashing tasks. + } else if (task.downloadAction.isRemoveAction()) { + canStartTask = false; + skipDownloadActions = true; + break; + } + } + } + + if (canStartTask) { + downloadTask.start(); + if (!removeAction) { + activeDownloadTasks.add(downloadTask); + skipDownloadActions = activeDownloadTasks.size() == maxActiveDownloadTasks; + } + } + } + } + + private void maybeNotifyListenersIdle() { + if (!isIdle()) { + return; + } + logd("Notify idle state"); + for (DownloadListener listener : listeners) { + listener.onIdle(this); + } + } + + private void onTaskStateChange(DownloadTask downloadTask) { + if (released) { + return; + } + logd("Task state is changed", downloadTask); + boolean stopped = !downloadTask.isRunning(); + if (stopped) { + activeDownloadTasks.remove(downloadTask); + } + notifyListenersTaskStateChange(downloadTask); + if (downloadTask.isFinished()) { + tasks.remove(downloadTask); + saveActions(); + } + if (stopped) { + maybeStartTasks(); + maybeNotifyListenersIdle(); + } + } + + private void notifyListenersTaskStateChange(DownloadTask downloadTask) { + DownloadState downloadState = downloadTask.getDownloadState(); + for (DownloadListener listener : listeners) { + listener.onStateChange(this, downloadState); + } + } + + private void loadActions() { + fileIOHandler.post( + new Runnable() { + @Override + public void run() { + DownloadAction[] loadedActions; + try { + loadedActions = actionFile.load(DownloadManager.this.deserializers); + logd("Action file is loaded."); + } catch (Throwable e) { + Log.e(TAG, "Action file loading failed.", e); + loadedActions = new DownloadAction[0]; + } + final DownloadAction[] actions = loadedActions; + handler.post( + new Runnable() { + @Override + public void run() { + try { + for (DownloadAction action : actions) { + createDownloadTask(action); + } + logd("Tasks are created."); + maybeStartTasks(); + } finally { + actionFileLoadCompleted = true; + maybeNotifyListenersIdle(); + } + } + }); + } + }); + } + + private void saveActions() { + if (!actionFileLoadCompleted || released) { + return; + } + final DownloadAction[] actions = new DownloadAction[tasks.size()]; + for (int i = 0; i < tasks.size(); i++) { + actions[i] = tasks.get(i).downloadAction; + } + fileIOHandler.post(new Runnable() { + @Override + public void run() { + try { + actionFile.store(actions); + logd("Actions persisted."); + } catch (IOException e) { + Log.e(TAG, "Persisting actions failed.", e); + } + } + }); + } + + private void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + private void logd(String message, DownloadTask task) { + logd(message + ": " + task); + } + + private static DownloadState[] getDownloadStates(ArrayList tasks) { + DownloadState[] states = new DownloadState[tasks.size()]; + for (int i = 0; i < tasks.size(); i++) { + DownloadTask task = tasks.get(i); + states[i] = task.getDownloadState(); + } + return states; + } + + /** Represents state of a download task. */ + public static final class DownloadState { + + /** + * Task states. + * + *

Transition map (vertical states are source states): + *

+     *           +-------+-------+-----+---------+--------+--------+-----+
+     *           |waiting|started|ended|canceling|canceled|stopping|error|
+     * +---------+-------+-------+-----+---------+--------+--------+-----+
+     * |waiting  |       |   X   |     |    X    |        |        |     |
+     * |started  |       |       |  X  |    X    |        |   X    |  X  |
+     * |canceling|       |       |     |         |   X    |        |     |
+     * |stopping |   X   |       |     |         |        |        |     |
+     * +---------+-------+-------+-----+---------+--------+--------+-----+
+     * 
+ */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_WAITING, STATE_STARTED, STATE_ENDED, STATE_CANCELING, STATE_CANCELED, + STATE_STOPPING, STATE_ERROR}) + public @interface State {} + /** The task is waiting to be started. */ + public static final int STATE_WAITING = 0; + /** The task is currently started. */ + public static final int STATE_STARTED = 1; + /** The task completed. */ + public static final int STATE_ENDED = 2; + /** The task is about to be canceled. */ + public static final int STATE_CANCELING = 3; + /** The task was canceled. */ + public static final int STATE_CANCELED = 4; + /** The task is about to be stopped. */ + public static final int STATE_STOPPING = 5; + /** The task failed. */ + public static final int STATE_ERROR = 6; + + /** Returns the state string for the given state value. */ + public static String getStateString(@State int state) { + switch (state) { + case STATE_WAITING: + return "WAITING"; + case STATE_STARTED: + return "STARTED"; + case STATE_ENDED: + return "ENDED"; + case STATE_CANCELING: + return "CANCELING"; + case STATE_CANCELED: + return "CANCELED"; + case STATE_STOPPING: + return "STOPPING"; + case STATE_ERROR: + return "ERROR"; + default: + throw new IllegalStateException(); + } + } + + /** Unique id of the task. */ + public final int taskId; + /** The {@link DownloadAction} which is being executed. */ + public final DownloadAction downloadAction; + /** The state of the task. See {@link State}. */ + public final @State int state; + /** + * The download percentage, or {@link Float#NaN} if it can't be calculated or the task is for + * removing. + */ + public final float downloadPercentage; + /** + * The downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been calculated yet or the task + * is for removing. + */ + public final long downloadedBytes; + /** If {@link #state} is {@link #STATE_ERROR} then this is the cause, otherwise null. */ + public final Throwable error; + + private DownloadState( + int taskId, + DownloadAction downloadAction, + int state, + float downloadPercentage, + long downloadedBytes, + Throwable error) { + this.taskId = taskId; + this.downloadAction = downloadAction; + this.state = state; + this.downloadPercentage = downloadPercentage; + this.downloadedBytes = downloadedBytes; + this.error = error; + } + + /** Returns whether the task is finished. */ + public boolean isFinished() { + return state == STATE_ERROR || state == STATE_ENDED || state == STATE_CANCELED; + } + } + + private static final class DownloadTask implements Runnable { + + private final int id; + private final DownloadManager downloadManager; + private final DownloadAction downloadAction; + private final int minRetryCount; + private volatile @State int currentState; + private volatile Downloader downloader; + private Thread thread; + private Throwable error; + + private DownloadTask( + int id, DownloadManager downloadManager, DownloadAction downloadAction, int minRetryCount) { + this.id = id; + this.downloadManager = downloadManager; + this.downloadAction = downloadAction; + this.currentState = STATE_WAITING; + this.minRetryCount = minRetryCount; + } + + public DownloadState getDownloadState() { + return new DownloadState( + id, downloadAction, currentState, getDownloadPercentage(), getDownloadedBytes(), error); + } + + /** Returns the {@link DownloadAction}. */ + public DownloadAction getDownloadAction() { + return downloadAction; + } + + /** Returns the state of the task. */ + public @State int getState() { + return currentState; + } + + /** Returns whether the task is finished. */ + public boolean isFinished() { + return currentState == STATE_ERROR || currentState == STATE_ENDED + || currentState == STATE_CANCELED; + } + + /** Returns whether the task is running. */ + public boolean isRunning() { + return currentState == STATE_STARTED + || currentState == STATE_STOPPING + || currentState == STATE_CANCELING; + } + + /** + * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This + * value can be an estimation. + */ + public float getDownloadPercentage() { + return downloader != null ? downloader.getDownloadPercentage() : Float.NaN; + } + + /** + * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been + * calculated yet. + */ + public long getDownloadedBytes() { + return downloader != null ? downloader.getDownloadedBytes() : C.LENGTH_UNSET; + } + + @Override + public String toString() { + if (!DEBUG) { + return super.toString(); + } + return downloadAction.getType() + + ' ' + + (downloadAction.isRemoveAction() ? "remove" : "download") + + ' ' + + downloadAction.getData() + + ' ' + + DownloadState.getStateString(currentState); + } + + private void start() { + if (changeStateAndNotify(STATE_WAITING, STATE_STARTED)) { + thread = new Thread(this); + thread.start(); + } + } + + private boolean canStart() { + return currentState == STATE_WAITING; + } + + private void cancel() { + if (changeStateAndNotify(STATE_WAITING, STATE_CANCELING)) { + downloadManager.handler.post(new Runnable() { + @Override + public void run() { + changeStateAndNotify(STATE_CANCELING, STATE_CANCELED); + } + }); + } else if (changeStateAndNotify(STATE_STARTED, STATE_CANCELING)) { + thread.interrupt(); + } + } + + private void stop() { + if (changeStateAndNotify(STATE_STARTED, STATE_STOPPING)) { + downloadManager.logd("Stopping", this); + thread.interrupt(); + } + } + + private boolean changeStateAndNotify(@State int oldState, @State int newState) { + return changeStateAndNotify(oldState, newState, null); + } + + private boolean changeStateAndNotify(@State int oldState, @State int newState, + Throwable error) { + if (currentState != oldState) { + return false; + } + currentState = newState; + this.error = error; + downloadManager.onTaskStateChange(DownloadTask.this); + return true; + } + + /* Methods running on download thread. */ + + @Override + public void run() { + downloadManager.logd("Task is started", DownloadTask.this); + Throwable error = null; + try { + downloader = downloadAction.createDownloader(downloadManager.downloaderConstructorHelper); + if (downloadAction.isRemoveAction()) { + downloader.remove(); + } else { + int errorCount = 0; + long errorPosition = C.LENGTH_UNSET; + while (true) { + try { + downloader.download(null); + break; + } catch (IOException e) { + long downloadedBytes = downloader.getDownloadedBytes(); + if (downloadedBytes != errorPosition) { + downloadManager.logd( + "Reset error count. downloadedBytes = " + downloadedBytes, this); + errorPosition = downloadedBytes; + errorCount = 0; + } + if (currentState != STATE_STARTED || ++errorCount > minRetryCount) { + throw e; + } + downloadManager.logd("Download error. Retry " + errorCount, this); + Thread.sleep(getRetryDelayMillis(errorCount)); + } + } + } + } catch (Throwable e){ + error = e; + } + final Throwable finalError = error; + downloadManager.handler.post(new Runnable() { + @Override + public void run() { + if (changeStateAndNotify(STATE_STARTED, + finalError != null ? STATE_ERROR : STATE_ENDED, finalError) + || changeStateAndNotify(STATE_CANCELING, STATE_CANCELED) + || changeStateAndNotify(STATE_STOPPING, STATE_WAITING)) { + return; + } + throw new IllegalStateException(); + } + }); + } + + private int getRetryDelayMillis(int errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + } + +} 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 new file mode 100644 index 0000000000..b6899299c9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -0,0 +1,396 @@ +/* + * 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.offline; + +import android.app.Notification; +import android.app.Notification.Builder; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.util.scheduler.Requirements; +import com.google.android.exoplayer2.util.scheduler.RequirementsWatcher; +import com.google.android.exoplayer2.util.scheduler.Scheduler; +import java.io.IOException; + +/** + * A {@link Service} that downloads streams in the background. + * + *

To start the service, create an instance of one of the subclasses of {@link DownloadAction} + * and call {@link #addDownloadAction(Context, Class, DownloadAction)} with it. + */ +public abstract class DownloadService extends Service implements DownloadManager.DownloadListener { + + /** Use this action to initialize {@link DownloadManager}. */ + public static final String ACTION_INIT = + "com.google.android.exoplayer.downloadService.action.INIT"; + + /** Use this action to add a {@link DownloadAction} to {@link DownloadManager} action queue. */ + public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; + + /** Use this action to make {@link DownloadManager} stop download tasks. */ + private static final String ACTION_STOP = + "com.google.android.exoplayer.downloadService.action.STOP"; + + /** Use this action to make {@link DownloadManager} start download tasks. */ + private static final String ACTION_START = + "com.google.android.exoplayer.downloadService.action.START"; + + /** A {@link DownloadAction} to be executed. */ + public static final String DOWNLOAD_ACTION = "DownloadAction"; + + /** Default progress update interval in milliseconds. */ + public static final long DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS = 1000; + + private static final String TAG = "DownloadService"; + private static final boolean DEBUG = false; + + // Keep requirementsWatcher and scheduler alive beyond DownloadService life span (until the app is + // killed) because it may take long time for Scheduler to start the service. + private static RequirementsWatcher requirementsWatcher; + private static Scheduler scheduler; + + private final int notificationIdOffset; + private final long progressUpdateIntervalMillis; + + private DownloadManager downloadManager; + private ProgressUpdater progressUpdater; + private int lastStartId; + + /** @param notificationIdOffset Value to offset notification ids. Must be greater than 0. */ + protected DownloadService(int notificationIdOffset) { + this(notificationIdOffset, DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS); + } + + /** + * @param notificationIdOffset Value to offset notification ids. Must be greater than 0. + * @param progressUpdateIntervalMillis {@link #onProgressUpdate(DownloadState[])} is called using + * this interval. If it's {@link C#TIME_UNSET}, then {@link + * #onProgressUpdate(DownloadState[])} isn't called. + */ + protected DownloadService(int notificationIdOffset, long progressUpdateIntervalMillis) { + this.notificationIdOffset = notificationIdOffset; + this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; + } + + /** + * Creates an {@link Intent} to be used to start this service and adds the {@link DownloadAction} + * to the {@link DownloadManager}. + * + * @param context A {@link Context} of the application calling this service. + * @param clazz Class object of DownloadService or subclass. + * @param downloadAction A {@link DownloadAction} to be executed. + * @return Created Intent. + */ + public static Intent createAddDownloadActionIntent( + Context context, Class clazz, DownloadAction downloadAction) { + return new Intent(context, clazz) + .setAction(ACTION_ADD) + .putExtra(DOWNLOAD_ACTION, downloadAction.toByteArray()); + } + + /** + * Adds a {@link DownloadAction} to the {@link DownloadManager}. This will start the download + * service if it was not running. + * + * @param context A {@link Context} of the application calling this service. + * @param clazz Class object of DownloadService or subclass. + * @param downloadAction A {@link DownloadAction} to be executed. + * @see #createAddDownloadActionIntent(Context, Class, DownloadAction) + */ + public static void addDownloadAction( + Context context, Class clazz, DownloadAction downloadAction) { + context.startService(createAddDownloadActionIntent(context, clazz, downloadAction)); + } + + @Override + public void onCreate() { + logd("onCreate"); + downloadManager = getDownloadManager(); + downloadManager.addListener(this); + + if (requirementsWatcher == null) { + Requirements requirements = getRequirements(); + if (requirements != null) { + scheduler = getScheduler(); + RequirementsListener listener = + new RequirementsListener(getApplicationContext(), getClass(), scheduler); + requirementsWatcher = + new RequirementsWatcher(getApplicationContext(), listener, requirements); + requirementsWatcher.start(); + } + } + + progressUpdater = new ProgressUpdater(this, progressUpdateIntervalMillis); + } + + @Override + public void onDestroy() { + logd("onDestroy"); + progressUpdater.stop(); + downloadManager.removeListener(this); + if (downloadManager.getTaskCount() == 0) { + if (requirementsWatcher != null) { + requirementsWatcher.stop(); + requirementsWatcher = null; + } + if (scheduler != null) { + scheduler.cancel(); + scheduler = null; + } + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + this.lastStartId = startId; + String intentAction = intent != null ? intent.getAction() : null; + if (intentAction == null) { + intentAction = ACTION_INIT; + } + logd("onStartCommand action: " + intentAction + " startId: " + startId); + switch (intentAction) { + case ACTION_INIT: + // Do nothing. DownloadManager and RequirementsWatcher is initialized. If there are download + // or remove tasks loaded from file, they will start if the requirements are met. + break; + case ACTION_ADD: + byte[] actionData = intent.getByteArrayExtra(DOWNLOAD_ACTION); + if (actionData == null) { + onCommandError(intent, new IllegalArgumentException("DownloadAction is missing.")); + } else { + try { + onNewTask(intent, downloadManager.handleAction(actionData)); + } catch (IOException e) { + onCommandError(intent, e); + } + } + break; + case ACTION_STOP: + downloadManager.stopDownloads(); + break; + case ACTION_START: + downloadManager.startDownloads(); + break; + default: + onCommandError(intent, new IllegalArgumentException("Unknown action: " + intentAction)); + break; + } + if (downloadManager.isIdle()) { + onIdle(null); + } + return START_STICKY; + } + + /** + * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the + * life cycle of the service. + */ + protected abstract DownloadManager getDownloadManager(); + + /** + * Returns a {@link Scheduler} which contains a job to initialize {@link DownloadService} when the + * requirements are met, or null. If not null, scheduler is used to start downloads even when the + * app isn't running. + */ + protected abstract @Nullable Scheduler getScheduler(); + + /** Returns requirements for downloads to take place, or null. */ + protected abstract @Nullable Requirements getRequirements(); + + /** Called on error in start command. */ + protected void onCommandError(Intent intent, Exception error) { + // Do nothing. + } + + /** Called when a new task is added to the {@link DownloadManager}. */ + protected void onNewTask(Intent intent, int taskId) { + // Do nothing. + } + + /** Returns a notification channelId. See {@link NotificationChannel}. */ + protected abstract String getNotificationChannelId(); + + /** + * Helper method which calls {@link #startForeground(int, Notification)} with {@code + * notificationIdOffset} and {@code foregroundNotification}. + */ + public void startForeground(Notification foregroundNotification) { + // logd("start foreground"); + startForeground(notificationIdOffset, foregroundNotification); + } + + /** + * Sets/replaces or cancels the notification for the given id. + * + * @param id A unique id for the notification. This value is offset by {@code + * notificationIdOffset}. + * @param notification If not null, it's showed, replacing any previous notification. Otherwise + * any previous notification is canceled. + */ + public void setNotification(int id, @Nullable Notification notification) { + NotificationManager notificationManager = + (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notification != null) { + notificationManager.notify(notificationIdOffset + 1 + id, notification); + } else { + notificationManager.cancel(notificationIdOffset + 1 + id); + } + } + + /** + * Override this method to get notified. + * + *

{@inheritDoc} + */ + @CallSuper + @Override + public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { + if (downloadState.state == DownloadState.STATE_STARTED) { + progressUpdater.start(); + } + } + + /** + * Override this method to get notified. + * + *

{@inheritDoc} + */ + @CallSuper + @Override + public void onIdle(DownloadManager downloadManager) { + // Make sure startForeground is called before stopping. + if (Util.SDK_INT >= 26) { + Builder notificationBuilder = new Builder(this, getNotificationChannelId()); + Notification foregroundNotification = notificationBuilder.build(); + startForeground(foregroundNotification); + } + boolean stopSelfResult = stopSelfResult(lastStartId); + logd("stopSelf(" + lastStartId + ") result: " + stopSelfResult); + } + + /** Override this method to get notified on every second while there are active downloads. */ + protected void onProgressUpdate(DownloadState[] activeDownloadTasks) { + // Do nothing. + } + + private void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + private static final class ProgressUpdater implements Runnable { + + private final DownloadService downloadService; + private final long progressUpdateIntervalMillis; + private final Handler handler; + private boolean stopped; + + public ProgressUpdater(DownloadService downloadService, long progressUpdateIntervalMillis) { + this.downloadService = downloadService; + this.progressUpdateIntervalMillis = progressUpdateIntervalMillis; + this.handler = new Handler(Looper.getMainLooper()); + stopped = true; + } + + @Override + public void run() { + DownloadState[] activeDownloadTasks = + downloadService.downloadManager.getActiveDownloadStates(); + if (activeDownloadTasks.length > 0) { + downloadService.onProgressUpdate(activeDownloadTasks); + if (progressUpdateIntervalMillis != C.TIME_UNSET) { + handler.postDelayed(this, progressUpdateIntervalMillis); + } + } else { + stop(); + } + } + + public void stop() { + stopped = true; + handler.removeCallbacks(this); + } + + public void start() { + if (stopped) { + stopped = false; + if (progressUpdateIntervalMillis != C.TIME_UNSET) { + handler.post(this); + } + } + } + + } + + private static final class RequirementsListener implements RequirementsWatcher.Listener { + + private final Context context; + private final Class serviceClass; + private final Scheduler scheduler; + + private RequirementsListener( + Context context, Class serviceClass, Scheduler scheduler) { + this.context = context; + this.serviceClass = serviceClass; + this.scheduler = scheduler; + } + + @Override + public void requirementsMet(RequirementsWatcher requirementsWatcher) { + startServiceWithAction(DownloadService.ACTION_START); + if (scheduler != null) { + scheduler.cancel(); + } + } + + @Override + public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { + startServiceWithAction(DownloadService.ACTION_STOP); + if (scheduler != null) { + if (!scheduler.schedule()) { + Log.e(TAG, "Scheduling downloads failed."); + } + } + } + + private void startServiceWithAction(String action) { + Intent intent = new Intent(context, serviceClass).setAction(action); + if (Util.SDK_INT >= 26) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java new file mode 100644 index 0000000000..7c9549cd3a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadAction.java @@ -0,0 +1,122 @@ +/* + * 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.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded progressive streams. */ +public final class ProgressiveDownloadAction extends DownloadAction { + + public static final Deserializer DESERIALIZER = new Deserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + public ProgressiveDownloadAction readFromStream(int version, DataInputStream input) + throws IOException { + return new ProgressiveDownloadAction(input.readUTF(), + input.readBoolean() ? input.readUTF() : null, input.readBoolean(), input.readUTF()); + } + + }; + + private static final String TYPE = "ProgressiveDownloadAction"; + + private final String uri; + private final @Nullable String customCacheKey; + private final boolean removeAction; + + /** + * @param uri Uri of the data to be downloaded. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param removeAction Whether the data should be downloaded or removed. + * @param data Optional custom data for this action. If null, an empty string is used. + */ + public ProgressiveDownloadAction(String uri, @Nullable String customCacheKey, + boolean removeAction, String data) { + super(data); + this.uri = Assertions.checkNotNull(uri); + this.customCacheKey = customCacheKey; + this.removeAction = removeAction; + } + + @Override + public boolean isRemoveAction() { + return removeAction; + } + + @Override + protected ProgressiveDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + return new ProgressiveDownloader(uri, customCacheKey, constructorHelper); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + output.writeUTF(uri); + boolean customCacheKeyAvailable = customCacheKey != null; + output.writeBoolean(customCacheKeyAvailable); + if (customCacheKeyAvailable) { + output.writeUTF(customCacheKey); + } + output.writeBoolean(isRemoveAction()); + output.writeUTF(getData()); + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + if (!(other instanceof ProgressiveDownloadAction)) { + return false; + } + ProgressiveDownloadAction action = (ProgressiveDownloadAction) other; + return customCacheKey != null ? customCacheKey.equals(action.customCacheKey) + : uri.equals(action.uri); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + ProgressiveDownloadAction that = (ProgressiveDownloadAction) o; + return uri.equals(that.uri) && Util.areEqual(customCacheKey, that.customCacheKey); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + uri.hashCode(); + result = 31 * result + (customCacheKey != null ? customCacheKey.hashCode() : 0); + return result; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java new file mode 100644 index 0000000000..b77ac5bad8 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -0,0 +1,142 @@ +/* + * 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.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.util.Assertions; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +/** + * {@link DownloadAction} for {@link SegmentDownloader}s. + * + * @param The type of the representation key object. + */ +public abstract class SegmentDownloadAction extends DownloadAction { + + /** + * Base class for {@link SegmentDownloadAction} {@link Deserializer}s. + * + * @param The type of the representation key object. + */ + protected abstract static class SegmentDownloadActionDeserializer implements Deserializer { + + @Override + public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { + Uri manifestUri = Uri.parse(input.readUTF()); + String data = input.readUTF(); + int keyCount = input.readInt(); + boolean removeAction = keyCount == -1; + K[] keys; + if (removeAction) { + keys = null; + } else { + keys = createKeyArray(keyCount); + for (int i = 0; i < keyCount; i++) { + keys[i] = readKey(input); + } + } + return createDownloadAction(manifestUri, removeAction, data, keys); + } + + /** Deserializes a key from the {@code input}. */ + protected abstract K readKey(DataInputStream input) throws IOException; + + /** Returns a key array. */ + protected abstract K[] createKeyArray(int keyCount); + + /** Returns a {@link DownloadAction}. */ + protected abstract DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String data, K[] keys); + + } + + protected final Uri manifestUri; + protected final K[] keys; + private final boolean removeAction; + + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param removeAction Whether the data will be removed. If {@code false} it will be downloaded. + * @param data Optional custom data for this action. If null, an empty string is used. + * @param keys Keys of representations to be downloaded. If empty or null, all representations are + * downloaded. If {@code removeAction} is true, this is ignored. + */ + protected SegmentDownloadAction(Uri manifestUri, boolean removeAction, String data, K[] keys) { + super(data); + Assertions.checkArgument(!removeAction || keys == null || keys.length == 0); + this.manifestUri = manifestUri; + this.keys = keys; + this.removeAction = removeAction; + } + + @Override + public final boolean isRemoveAction() { + return removeAction; + } + + @Override + public final void writeToStream(DataOutputStream output) throws IOException { + output.writeUTF(manifestUri.toString()); + output.writeUTF(getData()); + if (isRemoveAction()) { + output.writeInt(-1); + } else { + output.writeInt(keys.length); + for (K key : keys) { + writeKey(output, key); + } + } + } + + /** Serializes the {@code key} into the {@code output}. */ + protected abstract void writeKey(DataOutputStream output, K key) throws IOException; + + + @Override + public boolean isSameMedia(DownloadAction other) { + return other instanceof SegmentDownloadAction + && manifestUri.equals(((SegmentDownloadAction) other).manifestUri); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + SegmentDownloadAction that = (SegmentDownloadAction) o; + return manifestUri.equals(that.manifestUri) + && (keys == null || keys.length == 0 + ? (that.keys == null || that.keys.length == 0) + : (that.keys != null + && that.keys.length == keys.length + && Arrays.asList(keys).containsAll(Arrays.asList(that.keys)))); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + manifestUri.hashCode(); + result = 31 * result + Arrays.hashCode(keys); + return result; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java new file mode 100644 index 0000000000..2bc0034c9b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java @@ -0,0 +1,195 @@ +/* + * 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.util.scheduler; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.Service; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.PersistableBundle; +import android.util.Log; +import com.google.android.exoplayer2.util.Util; + +/** + * A {@link Scheduler} which uses {@link android.app.job.JobScheduler} to schedule a {@link Service} + * to be started when its requirements are met. The started service must call {@link + * Service#startForeground(int, Notification)} to make itself a foreground service upon being + * started, as documented by {@link Service#startForegroundService(Intent)}. + * + *

To use {@link PlatformScheduler} application needs to have RECEIVE_BOOT_COMPLETED permission + * and you need to define PlatformSchedulerService in your manifest: + * + *

{@literal
+ * 
+ *
+ * 
+ * }
+ * + * The service to be scheduled must be defined in the manifest with an intent-filter: + * + *
{@literal
+ * 
+ *  
+ *    
+ *    
+ *  
+ * 
+ * }
+ */ +@TargetApi(21) +public final class PlatformScheduler implements Scheduler { + + private static final String TAG = "PlatformScheduler"; + private static final String SERVICE_ACTION = "SERVICE_ACTION"; + private static final String SERVICE_PACKAGE = "SERVICE_PACKAGE"; + private static final String REQUIREMENTS = "REQUIREMENTS"; + + private final int jobId; + private final JobInfo jobInfo; + private final JobScheduler jobScheduler; + + /** + * @param context Used to access to {@link JobScheduler} service. + * @param requirements The requirements to execute the job. + * @param jobId Unique identifier for the job. Using the same id as a previous job can cause that + * job to be replaced or canceled. + * @param serviceAction The action which the service will be started with. + * @param servicePackage The package of the service which contains the logic of the job. + */ + public PlatformScheduler( + Context context, + Requirements requirements, + int jobId, + String serviceAction, + String servicePackage) { + this.jobId = jobId; + this.jobInfo = buildJobInfo(context, requirements, jobId, serviceAction, servicePackage); + this.jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + } + + @Override + public boolean schedule() { + int result = jobScheduler.schedule(jobInfo); + logd("Scheduling JobScheduler job: " + jobId + " result: " + result); + return result == JobScheduler.RESULT_SUCCESS; + } + + @Override + public boolean cancel() { + logd("Canceling JobScheduler job: " + jobId); + jobScheduler.cancel(jobId); + return true; + } + + private static JobInfo buildJobInfo( + Context context, + Requirements requirements, + int jobId, + String serviceAction, + String servicePackage) { + JobInfo.Builder builder = + new JobInfo.Builder(jobId, new ComponentName(context, PlatformSchedulerService.class)); + + int networkType; + switch (requirements.getRequiredNetworkType()) { + case Requirements.NETWORK_TYPE_NONE: + networkType = JobInfo.NETWORK_TYPE_NONE; + break; + case Requirements.NETWORK_TYPE_ANY: + networkType = JobInfo.NETWORK_TYPE_ANY; + break; + case Requirements.NETWORK_TYPE_UNMETERED: + networkType = JobInfo.NETWORK_TYPE_UNMETERED; + break; + case Requirements.NETWORK_TYPE_NOT_ROAMING: + if (Util.SDK_INT >= 24) { + networkType = JobInfo.NETWORK_TYPE_NOT_ROAMING; + } else { + throw new UnsupportedOperationException(); + } + break; + case Requirements.NETWORK_TYPE_METERED: + if (Util.SDK_INT >= 26) { + networkType = JobInfo.NETWORK_TYPE_METERED; + } else { + throw new UnsupportedOperationException(); + } + break; + default: + throw new UnsupportedOperationException(); + } + + builder.setRequiredNetworkType(networkType); + builder.setRequiresDeviceIdle(requirements.isIdleRequired()); + builder.setRequiresCharging(requirements.isChargingRequired()); + builder.setPersisted(true); + + // Extras, work duration. + PersistableBundle extras = new PersistableBundle(); + extras.putString(SERVICE_ACTION, serviceAction); + extras.putString(SERVICE_PACKAGE, servicePackage); + extras.putInt(REQUIREMENTS, requirements.getRequirementsData()); + + builder.setExtras(extras); + return builder.build(); + } + + private static void logd(String message) { + if (DEBUG) { + Log.d(TAG, message); + } + } + + /** A {@link JobService} to start a service if the requirements are met. */ + public static final class PlatformSchedulerService extends JobService { + @Override + public boolean onStartJob(JobParameters params) { + logd("PlatformSchedulerService is started"); + PersistableBundle extras = params.getExtras(); + Requirements requirements = new Requirements(extras.getInt(REQUIREMENTS)); + if (requirements.checkRequirements(this)) { + logd("requirements are met"); + String serviceAction = extras.getString(SERVICE_ACTION); + String servicePackage = extras.getString(SERVICE_PACKAGE); + Intent intent = new Intent(serviceAction).setPackage(servicePackage); + logd("starting service action: " + serviceAction + " package: " + servicePackage); + if (Util.SDK_INT >= 26) { + startForegroundService(intent); + } else { + startService(intent); + } + } else { + logd("requirements are not met"); + jobFinished(params, /* needsReschedule */ true); + } + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java new file mode 100644 index 0000000000..80895241c2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java @@ -0,0 +1,226 @@ +/* + * 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.util.scheduler; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.PowerManager; +import android.support.annotation.IntDef; +import android.util.Log; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Defines a set of device state requirements. + * + *

To use network type requirement, application needs to have ACCESS_NETWORK_STATE permission. + */ +public final class Requirements { + + /** Network types. */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + NETWORK_TYPE_NONE, + NETWORK_TYPE_ANY, + NETWORK_TYPE_UNMETERED, + NETWORK_TYPE_NOT_ROAMING, + NETWORK_TYPE_METERED, + }) + public @interface NetworkType {} + /** This job doesn't require network connectivity. */ + public static final int NETWORK_TYPE_NONE = 0; + /** This job requires network connectivity. */ + public static final int NETWORK_TYPE_ANY = 1; + /** This job requires network connectivity that is unmetered. */ + public static final int NETWORK_TYPE_UNMETERED = 2; + /** This job requires network connectivity that is not roaming. */ + public static final int NETWORK_TYPE_NOT_ROAMING = 3; + /** This job requires metered connectivity such as most cellular data networks. */ + public static final int NETWORK_TYPE_METERED = 4; + /** This job requires the device to be idle. */ + private static final int DEVICE_IDLE = 8; + /** This job requires the device to be charging. */ + private static final int DEVICE_CHARGING = 16; + + private static final int NETWORK_TYPE_MASK = 7; + + private static final String TAG = "Requirements"; + + private static final String[] NETWORK_TYPE_STRINGS; + + static { + if (Scheduler.DEBUG) { + NETWORK_TYPE_STRINGS = + new String[] { + "NETWORK_TYPE_NONE", + "NETWORK_TYPE_ANY", + "NETWORK_TYPE_UNMETERED", + "NETWORK_TYPE_NOT_ROAMING", + "NETWORK_TYPE_METERED" + }; + } else { + NETWORK_TYPE_STRINGS = null; + } + } + + private final int requirements; + + /** + * @param networkType Required network type. + * @param charging Whether the device should be charging. + * @param idle Whether the device should be idle. + */ + public Requirements(@NetworkType int networkType, boolean charging, boolean idle) { + this(networkType | (charging ? DEVICE_CHARGING : 0) | (idle ? DEVICE_IDLE : 0)); + } + + /** @param requirementsData The value returned by {@link #getRequirementsData()}. */ + public Requirements(int requirementsData) { + this.requirements = requirementsData; + } + + /** Returns required network type. */ + public int getRequiredNetworkType() { + return requirements & NETWORK_TYPE_MASK; + } + + /** Returns whether the device should be charging. */ + public boolean isChargingRequired() { + return (requirements & DEVICE_CHARGING) != 0; + } + + /** Returns whether the device should be idle. */ + public boolean isIdleRequired() { + return (requirements & DEVICE_IDLE) != 0; + } + + /** + * Returns whether the requirements are met. + * + * @param context Any context. + */ + public boolean checkRequirements(Context context) { + return checkNetworkRequirements(context) + && checkChargingRequirement(context) + && checkIdleRequirement(context); + } + + /** Returns the encoded requirements data which can be used with {@link #Requirements(int)}. */ + public int getRequirementsData() { + return requirements; + } + + private boolean checkNetworkRequirements(Context context) { + int networkRequirement = getRequiredNetworkType(); + if (networkRequirement == NETWORK_TYPE_NONE) { + return true; + } + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnected()) { + logd("No network info or no connection."); + return false; + } else if (Util.SDK_INT >= 23) { + // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API + // levels prior to 23. + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + logd("No active network."); + return false; + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + if (networkCapabilities == null + || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + logd("Net capability isn't validated."); + return false; + } + } + boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered(); + switch (networkRequirement) { + case NETWORK_TYPE_ANY: + return true; + case NETWORK_TYPE_UNMETERED: + if (activeNetworkMetered) { + logd("Network is metered."); + } + return !activeNetworkMetered; + case NETWORK_TYPE_NOT_ROAMING: + boolean roaming = networkInfo.isRoaming(); + if (roaming) { + logd("Roaming."); + } + return !roaming; + case NETWORK_TYPE_METERED: + if (!activeNetworkMetered) { + logd("Network isn't metered."); + } + return activeNetworkMetered; + default: + throw new IllegalStateException(); + } + } + + private boolean checkChargingRequirement(Context context) { + if (!isChargingRequired()) { + return true; + } + Intent batteryStatus = + context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + if (batteryStatus == null) { + return false; + } + int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); + return status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + } + + private boolean checkIdleRequirement(Context context) { + if (!isIdleRequired()) { + return true; + } + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return Util.SDK_INT >= 23 + ? !powerManager.isDeviceIdleMode() + : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); + } + + private static void logd(String message) { + if (Scheduler.DEBUG) { + Log.d(TAG, message); + } + } + + @Override + public String toString() { + if (!Scheduler.DEBUG) { + return super.toString(); + } + return "requirements{" + + NETWORK_TYPE_STRINGS[getRequiredNetworkType()] + + (isChargingRequired() ? ",charging" : "") + + (isIdleRequired() ? ",idle" : "") + + '}'; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java new file mode 100644 index 0000000000..05a1e7a493 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java @@ -0,0 +1,211 @@ +/* + * 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.util.scheduler; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.os.PowerManager; +import android.support.annotation.RequiresApi; +import android.util.Log; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +/** + * Watches whether the {@link Requirements} are met and notifies the {@link Listener} on changes. + */ +public final class RequirementsWatcher { + + /** + * Notified when RequirementsWatcher instance first created and on changes whether the {@link + * Requirements} are met. + */ + public interface Listener { + + /** + * Called when the requirements are met. + * + * @param requirementsWatcher Calling instance. + */ + void requirementsMet(RequirementsWatcher requirementsWatcher); + + /** + * Called when the requirements are not met. + * + * @param requirementsWatcher Calling instance. + */ + void requirementsNotMet(RequirementsWatcher requirementsWatcher); + } + + private static final String TAG = "RequirementsWatcher"; + + private final Context context; + private final Listener listener; + private final Requirements requirements; + private DeviceStatusChangeReceiver receiver; + + private boolean requirementsWereMet; + private CapabilityValidatedCallback networkCallback; + + /** + * @param context Used to register for broadcasts. + * @param listener Notified whether the {@link Requirements} are met. + * @param requirements The requirements to watch. + */ + public RequirementsWatcher(Context context, Listener listener, Requirements requirements) { + this.requirements = requirements; + this.listener = listener; + this.context = context; + logd(this + " created"); + } + + /** + * Starts watching for changes. Must be called from a thread that has an associated {@link + * Looper}. Listener methods are called on the caller thread. + */ + public void start() { + Assertions.checkNotNull(Looper.myLooper()); + + checkRequirements(true); + + IntentFilter filter = new IntentFilter(); + if (requirements.getRequiredNetworkType() != Requirements.NETWORK_TYPE_NONE) { + if (Util.SDK_INT >= 23) { + registerNetworkCallbackV23(); + } else { + filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + } + } + if (requirements.isChargingRequired()) { + filter.addAction(Intent.ACTION_POWER_CONNECTED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); + } + if (requirements.isIdleRequired()) { + if (Util.SDK_INT >= 23) { + filter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + } else { + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + } + } + receiver = new DeviceStatusChangeReceiver(); + context.registerReceiver(receiver, filter, null, new Handler()); + logd(this + " started"); + } + + /** Stops watching for changes. */ + public void stop() { + context.unregisterReceiver(receiver); + receiver = null; + if (networkCallback != null) { + unregisterNetworkCallback(); + } + logd(this + " stopped"); + } + + /** Returns watched {@link Requirements}. */ + public Requirements getRequirements() { + return requirements; + } + + @Override + public String toString() { + if (!Scheduler.DEBUG) { + return super.toString(); + } + return "RequirementsWatcher{" + requirements + '}'; + } + + @TargetApi(23) + private void registerNetworkCallbackV23() { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkRequest request = + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build(); + networkCallback = new CapabilityValidatedCallback(); + connectivityManager.registerNetworkCallback(request, networkCallback); + } + + private void unregisterNetworkCallback() { + if (Util.SDK_INT >= 21) { + ConnectivityManager connectivityManager = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + connectivityManager.unregisterNetworkCallback(networkCallback); + networkCallback = null; + } + } + + private void checkRequirements(boolean force) { + boolean requirementsAreMet = requirements.checkRequirements(context); + if (!force) { + if (requirementsAreMet == requirementsWereMet) { + logd("requirementsAreMet is still " + requirementsAreMet); + return; + } + } + requirementsWereMet = requirementsAreMet; + if (requirementsAreMet) { + logd("start job"); + listener.requirementsMet(this); + } else { + logd("stop job"); + listener.requirementsNotMet(this); + } + } + + private static void logd(String message) { + if (Scheduler.DEBUG) { + Log.d(TAG, message); + } + } + + private class DeviceStatusChangeReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!isInitialStickyBroadcast()) { + logd(RequirementsWatcher.this + " received " + intent.getAction()); + checkRequirements(false); + } + } + } + + @RequiresApi(api = 21) + private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); + checkRequirements(false); + } + + @Override + public void onLost(Network network) { + super.onLost(network); + logd(RequirementsWatcher.this + " NetworkCallback.onLost"); + checkRequirements(false); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java new file mode 100644 index 0000000000..395c4c7090 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java @@ -0,0 +1,39 @@ +/* + * 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.util.scheduler; + +/** + * Implementer of this interface schedules one implementation specific job to be run when some + * requirements are met even if the app isn't running. + */ +public interface Scheduler { + + /*package*/ boolean DEBUG = false; + + /** + * Schedules the job to be run when the requirements are met. + * + * @return Whether the job scheduled successfully. + */ + boolean schedule(); + + /** + * Cancels any previous schedule. + * + * @return Whether the job cancelled successfully. + */ + boolean cancel(); +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java new file mode 100644 index 0000000000..6b22126725 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -0,0 +1,252 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ActionFileTest { + + private File tempFile; + + @Before + public void setUp() throws Exception { + tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + } + + @After + public void tearDown() throws Exception { + tempFile.delete(); + } + + @Test + public void testLoadNoDataThrowsIOException() throws Exception { + try { + loadActions(new Object[] {}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadIncompleteHeaderThrowsIOException() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadCompleteHeaderZeroAction() throws Exception { + DownloadAction[] actions = + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0}); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(0); + } + + @Test + public void testLoadAction() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321}, + new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(1); + assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadActions() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123, + /*action 2*/"type2", 321}, // Action 2 + new FakeDeserializer("type1"), new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(2); + assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123); + assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadNotSupportedVersion() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type2")); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadNotSupportedType() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type1")); + Assert.fail(); + } catch (DownloadException e) { + // Expected exception. + } + } + + @Test + public void testStoreAndLoadNoActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[0]); + } + + @Test + public void testStoreAndLoadActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[] { + new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123), + new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321), + }, new FakeDeserializer("type1"), new FakeDeserializer("type2")); + } + + private void doTestSerializationRoundTrip(DownloadAction[] actions, + Deserializer... deserializers) throws IOException { + ActionFile actionFile = new ActionFile(tempFile); + actionFile.store(actions); + assertThat(actionFile.load(deserializers)).isEqualTo(actions); + } + + private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) + throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + try { + for (Object value : values) { + if (value instanceof Integer) { + dataOutputStream.writeInt((Integer) value); // Action count + } else if (value instanceof String) { + dataOutputStream.writeUTF((String) value); // Action count + } else { + throw new IllegalArgumentException(); + } + } + } finally { + dataOutputStream.close(); + } + return new ActionFile(tempFile).load(deserializers); + } + + private static void assertAction(DownloadAction action, String type, int version, int data) { + assertThat(action).isInstanceOf(FakeDownloadAction.class); + assertThat(action.getType()).isEqualTo(type); + assertThat(((FakeDownloadAction) action).version).isEqualTo(version); + assertThat(((FakeDownloadAction) action).data).isEqualTo(data); + } + + private static class FakeDeserializer implements Deserializer { + final String type; + + FakeDeserializer(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } + + @Override + public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { + return new FakeDownloadAction(type, version, input.readInt()); + } + } + + private static class FakeDownloadAction extends DownloadAction { + final String type; + final int version; + final int data; + + private FakeDownloadAction(String type, int version, int data) { + super(null); + this.type = type; + this.version = version; + this.data = data; + } + + @Override + protected String getType() { + return type; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(data); + } + + @Override + public boolean isRemoveAction() { + return false; + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + return false; + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return null; + } + + // auto generated code + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FakeDownloadAction that = (FakeDownloadAction) o; + return version == that.version && data == that.data && type.equals(that.type); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + version; + result = 31 * result + data; + return result; + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java new file mode 100644 index 0000000000..62f9cf3d7f --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -0,0 +1,142 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ProgressiveDownloadActionTest { + + @Test + public void testDownloadActionIsNotRemoveAction() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); + assertThat(action.isRemoveAction()).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() throws Exception { + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null); + assertThat(action2.isRemoveAction()).isTrue(); + } + + @Test + public void testCreateDownloader() throws Exception { + MockitoAnnotations.initMocks(this); + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false, null); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true, null); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false, null); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @Test + public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true, null); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false, null); + assertThat(action5.isSameMedia(action6)).isTrue(); + } + + @Test + public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false, null); + assertThat(action7.isSameMedia(action8)).isFalse(); + } + + @Test + public void testEquals() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true, null); + assertThat(action1.equals(action1)).isTrue(); + + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true, null); + assertThat(action2.equals(action3)).isTrue(); + + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false, null); + assertThat(action4.equals(action5)).isFalse(); + + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true, null); + assertThat(action6.equals(action7)).isFalse(); + + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true, null); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true, null); + assertThat(action8.equals(action9)).isFalse(); + + ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true, null); + ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true, null); + assertThat(action10.equals(action11)).isFalse(); + } + + @Test + public void testSerializerGetType() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false, null); + assertThat(action.getType()).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false, null)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true, null)); + } + + private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + action1.writeToStream(output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = + ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); + + assertThat(action2).isEqualTo(action1); + } + +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java new file mode 100644 index 0000000000..996dad3d2b --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -0,0 +1,243 @@ +/* + * 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.dash.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.ConditionVariable; +import android.test.InstrumentationTestCase; +import android.test.UiThreadTest; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.MockitoUtil; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Util; +import java.io.File; + +/** + * Tests {@link DownloadManager}. + */ +public class DownloadManagerDashTest extends InstrumentationTestCase { + + private static final int ASSERT_TRUE_TIMEOUT = 1000; + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private DownloadManager downloadManager; + private RepresentationKey fakeRepresentationKey1; + private RepresentationKey fakeRepresentationKey2; + private TestDownloadListener downloadListener; + private File actionFile; + + @UiThreadTest + @Override + public void setUp() throws Exception { + super.setUp(); + Context context = getInstrumentation().getContext(); + tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); + File cacheFolder = new File(tempFolder, "cache"); + cacheFolder.mkdir(); + cache = new SimpleCache(cacheFolder, new NoOpCacheEvictor()); + MockitoUtil.setUpMockito(this); + fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + + fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); + fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + actionFile = new File(tempFolder, "actionFile"); + createDownloadManager(); + } + + @UiThreadTest + @Override + public void tearDown() throws Exception { + downloadManager.release(); + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + // Disabled due to flakiness. + public void disabledTestSaveAndLoadActionFile() throws Throwable { + // Configure fakeDataSet to block until interrupted when TEST_MPD is read. + fakeDataSet.newData(TEST_MPD_URI) + .appendReadAction(new Runnable() { + @SuppressWarnings("InfiniteLoopStatement") + @Override + public void run() { + try { + // Wait until interrupted. + while (true) { + Thread.sleep(100000); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + }) + .appendReadData(TEST_MPD) + .endData(); + + // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded + // actions. + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + // Setup an Action and immediately release the DM. + handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + downloadManager.release(); + + assertThat(actionFile.exists()).isTrue(); + assertThat(actionFile.length()).isGreaterThan(0L); + + assertCacheEmpty(cache); + + // Revert fakeDataSet to normal. + fakeDataSet.setData(TEST_MPD_URI, TEST_MPD); + + createDownloadManager(); + } + }); + + // Block on the test thread. + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + public void testHandleDownloadAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + public void testHandleMultipleDownloadAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + handleDownloadAction(fakeRepresentationKey2); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + public void testHandleInterferingDownloadAction() throws Throwable { + fakeDataSet + .newData("audio_segment_2") + .appendReadAction( + new Runnable() { + @Override + public void run() { + handleDownloadAction(fakeRepresentationKey2); + } + }) + .appendReadData(TestUtil.buildTestData(5)) + .endData(); + + handleDownloadAction(fakeRepresentationKey1); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + assertCachedData(cache, fakeDataSet); + } + + public void testHandleRemoveAction() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + // Disabled due to flakiness. + public void disabledTestHandleRemoveActionBeforeDownloadFinish() throws Throwable { + handleDownloadAction(fakeRepresentationKey1); + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + public void testHandleInterferingRemoveAction() throws Throwable { + final ConditionVariable downloadInProgressCondition = new ConditionVariable(); + fakeDataSet.newData("audio_segment_2") + .appendReadAction(new Runnable() { + @Override + public void run() { + downloadInProgressCondition.open(); + } + }) + .appendReadData(TestUtil.buildTestData(5)) + .endData(); + + handleDownloadAction(fakeRepresentationKey1); + + assertThat(downloadInProgressCondition.block(ASSERT_TRUE_TIMEOUT)).isTrue(); + + handleRemoveAction(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + downloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void handleDownloadAction(RepresentationKey... keys) { + downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, false, null, keys)); + } + + private void handleRemoveAction() { + downloadManager.handleAction(new DashDownloadAction(TEST_MPD_URI, true, null)); + } + + private void createDownloadManager() { + Factory fakeDataSourceFactory = new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + 1, + 3, + actionFile.getAbsolutePath(), + DashDownloadAction.DESERIALIZER); + + downloadListener = new TestDownloadListener(downloadManager, this); + downloadManager.addListener(downloadListener); + downloadManager.startDownloads(); + } + +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java new file mode 100644 index 0000000000..8da54ff89e --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -0,0 +1,212 @@ +/* + * 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.dash.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; + +import android.content.Context; +import android.content.Intent; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadService; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ConditionVariable; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.util.scheduler.Requirements; +import com.google.android.exoplayer2.util.scheduler.Scheduler; +import java.io.File; + +/** + * Unit tests for {@link DownloadService}. + */ +public class DownloadServiceDashTest extends InstrumentationTestCase { + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private RepresentationKey fakeRepresentationKey1; + private RepresentationKey fakeRepresentationKey2; + private Context context; + private DownloadService dashDownloadService; + private ConditionVariable pauseDownloadCondition; + private TestDownloadListener testDownloadListener; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + + Runnable pauseAction = new Runnable() { + @Override + public void run() { + if (pauseDownloadCondition != null) { + try { + pauseDownloadCondition.block(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + }; + fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .newData("audio_init_data") + .appendReadAction(pauseAction) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + DataSource.Factory fakeDataSourceFactory = new FakeDataSource.Factory(null) + .setFakeDataSet(fakeDataSet); + fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); + fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); + + context = getInstrumentation().getContext(); + + File actionFile = Util.createTempFile(context, "ExoPlayerTest"); + actionFile.delete(); + final DownloadManager dashDownloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + 1, + 3, + actionFile.getAbsolutePath(), + DashDownloadAction.DESERIALIZER); + testDownloadListener = new TestDownloadListener(dashDownloadManager, this); + dashDownloadManager.addListener(testDownloadListener); + dashDownloadManager.startDownloads(); + + try { + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + dashDownloadService = + new DownloadService(101010) { + + @Override + protected DownloadManager getDownloadManager() { + return dashDownloadManager; + } + + @Override + protected String getNotificationChannelId() { + return null; + } + + @Override + protected Scheduler getScheduler() { + return null; + } + + @Override + protected Requirements getRequirements() { + return null; + } + }; + dashDownloadService.onCreate(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + @Override + public void tearDown() throws Exception { + try { + runTestOnUiThread(new Runnable() { + @Override + public void run() { + dashDownloadService.onDestroy(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testMultipleDownloadAction() throws Throwable { + downloadKeys(fakeRepresentationKey1); + downloadKeys(fakeRepresentationKey2); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCachedData(cache, fakeDataSet); + } + + public void testRemoveAction() throws Throwable { + downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + removeAll(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + public void testRemoveBeforeDownloadComplete() throws Throwable { + pauseDownloadCondition = new ConditionVariable(); + downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); + + removeAll(); + + testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); + + assertCacheEmpty(cache); + } + + private void removeAll() throws Throwable { + callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, true, null)); + } + + private void downloadKeys(RepresentationKey... keys) throws Throwable { + callDownloadServiceOnStart(new DashDownloadAction(TEST_MPD_URI, false, null, keys)); + } + + private void callDownloadServiceOnStart(final DashDownloadAction action) throws Throwable { + runTestOnUiThread( + new Runnable() { + @Override + public void run() { + Intent startIntent = + DownloadService.createAddDownloadActionIntent( + context, DownloadService.class, action); + dashDownloadService.onStartCommand(startIntent, 0, 0); + } + }); + } + +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java new file mode 100644 index 0000000000..2e6688fe07 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java @@ -0,0 +1,74 @@ +/* + * 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.dash.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; + +/** A {@link DownloadListener} for testing. */ +/*package*/ final class TestDownloadListener implements DownloadListener { + + private static final int TIMEOUT = 1000; + + private final DownloadManager downloadManager; + private final InstrumentationTestCase testCase; + private final android.os.ConditionVariable downloadFinishedCondition; + private Throwable downloadError; + + public TestDownloadListener(DownloadManager downloadManager, InstrumentationTestCase testCase) { + this.downloadManager = downloadManager; + this.testCase = testCase; + this.downloadFinishedCondition = new android.os.ConditionVariable(); + } + + @Override + public void onStateChange(DownloadManager downloadManager, DownloadState downloadState) { + if (downloadState.state == DownloadState.STATE_ERROR && downloadError == null) { + downloadError = downloadState.error; + } + } + + @Override + public void onIdle(DownloadManager downloadManager) { + downloadFinishedCondition.open(); + } + + /** + * Blocks until all remove and download tasks are complete and throws an exception if there was an + * error. + */ + public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + testCase.runTestOnUiThread( + new Runnable() { + @Override + public void run() { + if (downloadManager.isIdle()) { + downloadFinishedCondition.open(); + } else { + downloadFinishedCondition.close(); + } + } + }); + assertThat(downloadFinishedCondition.block(TIMEOUT)).isTrue(); + if (downloadError != null) { + throw new Exception(downloadError); + } + } +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java new file mode 100644 index 0000000000..ed87a835cf --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadAction.java @@ -0,0 +1,85 @@ +/* + * 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.dash.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded DASH streams. */ +public final class DashDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected RepresentationKey readKey(DataInputStream input) throws IOException { + return new RepresentationKey(input.readInt(), input.readInt(), input.readInt()); + } + + @Override + protected RepresentationKey[] createKeyArray(int keyCount) { + return new RepresentationKey[keyCount]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String data, RepresentationKey[] keys) { + return new DashDownloadAction(manifestUri, removeAction, data, keys); + } + + }; + + private static final String TYPE = "DashDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + public DashDownloadAction(Uri manifestUri, boolean removeAction, String data, + RepresentationKey... keys) { + super(manifestUri, removeAction, data, keys); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected DashDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + DashDownloader downloader = new DashDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, RepresentationKey key) throws IOException { + output.writeInt(key.periodIndex); + output.writeInt(key.adaptationSetIndex); + output.writeInt(key.representationIndex); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java new file mode 100644 index 0000000000..9787ffb0d6 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -0,0 +1,80 @@ +/* + * 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.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded HLS streams. */ +public final class HlsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected String readKey(DataInputStream input) throws IOException { + return input.readUTF(); + } + + @Override + protected String[] createKeyArray(int keyCount) { + return new String[0]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String data, String[] keys) { + return new HlsDownloadAction(manifestUri, removeAction, data, keys); + } + + }; + + private static final String TYPE = "HlsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + public HlsDownloadAction(Uri manifestUri, boolean removeAction, String data, String... keys) { + super(manifestUri, removeAction, data, keys); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, String key) throws IOException { + output.writeUTF(key); + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java new file mode 100644 index 0000000000..1aabe51237 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -0,0 +1,83 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded SmoothStreaming streams. */ +public final class SsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected TrackKey readKey(DataInputStream input) throws IOException { + return new TrackKey(input.readInt(), input.readInt()); + } + + @Override + protected TrackKey[] createKeyArray(int keyCount) { + return new TrackKey[keyCount]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String data, TrackKey[] keys) { + return new SsDownloadAction(manifestUri, removeAction, data, keys); + } + + }; + + private static final String TYPE = "SsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, String, Object[]) */ + public SsDownloadAction(Uri manifestUri, boolean removeAction, String data, TrackKey... keys) { + super(manifestUri, removeAction, data, keys); + } + + @Override + protected String getType() { + return TYPE; + } + + @Override + protected SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) { + SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + output.writeInt(key.streamElementIndex); + output.writeInt(key.trackIndex); + } + +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java new file mode 100644 index 0000000000..0b7aa0d719 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -0,0 +1,114 @@ +/* + * 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.ui; + +import android.app.Notification; +import android.app.Notification.BigTextStyle; +import android.app.Notification.Builder; +import android.content.Context; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloadManager; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.util.Util; + +/** Helper class to create notifications for downloads using {@link DownloadManager}. */ +public final class DownloadNotificationUtil { + + private DownloadNotificationUtil() {} + + /** + * Returns a notification for the given {@link DownloadState}, or null if no notification should + * be displayed. + * + * @param downloadState State of the download. + * @param context Used to access resources. + * @param smallIcon A small icon for the notifications. + * @param channelId The id of the notification channel to use. Only required for API level 26 and + * above. + * @param errorMessageProvider An optional {@link ErrorMessageProvider} for translating download + * errors into readable error messages. + * @return A notification for the given {@link DownloadState}, or null if no notification should + * be displayed. + */ + public static @Nullable Notification createNotification( + DownloadState downloadState, + Context context, + int smallIcon, + String channelId, + @Nullable ErrorMessageProvider errorMessageProvider) { + DownloadAction downloadAction = downloadState.downloadAction; + if (downloadAction.isRemoveAction() || downloadState.state == DownloadState.STATE_CANCELED) { + return null; + } + + Builder notificationBuilder = new Builder(context); + if (Util.SDK_INT >= 26) { + notificationBuilder.setChannelId(channelId); + } + notificationBuilder.setSmallIcon(smallIcon); + + int titleStringId = getTitleStringId(downloadState); + notificationBuilder.setContentTitle(context.getResources().getString(titleStringId)); + + if (downloadState.isRunning()) { + notificationBuilder.setOngoing(true); + float percentage = downloadState.downloadPercentage; + boolean indeterminate = Float.isNaN(percentage); + notificationBuilder.setProgress(100, indeterminate ? 0 : (int) percentage, indeterminate); + } + + String message; + if (downloadState.error != null && errorMessageProvider != null) { + message = errorMessageProvider.getErrorMessage(downloadState.error).second; + } else { + message = downloadAction.getData(); + } + + if (Util.SDK_INT >= 16) { + notificationBuilder.setStyle(new BigTextStyle().bigText(message)); + } else { + notificationBuilder.setContentText(message); + } + return notificationBuilder.getNotification(); + } + + private static int getTitleStringId(DownloadState downloadState) { + int titleStringId; + switch (downloadState.state) { + case DownloadState.STATE_WAITING: + titleStringId = R.string.exo_download_queued; + break; + case DownloadState.STATE_STARTED: + case DownloadState.STATE_STOPPING: + case DownloadState.STATE_CANCELING: + titleStringId = R.string.exo_downloading; + break; + case DownloadState.STATE_ENDED: + titleStringId = R.string.exo_download_completed; + break; + case DownloadState.STATE_ERROR: + titleStringId = R.string.exo_download_failed; + break; + case DownloadState.STATE_CANCELED: + default: + // Never happens. + throw new IllegalStateException(); + } + return titleStringId; + } +} From e1dbaf26c299fdbf65136efc4877a4734ad25c59 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 25 Jan 2018 05:51:57 -0800 Subject: [PATCH 003/376] Add DownloadNotificationHelper Helper class to create notifications for downloads using DownloadManager. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=183225948 --- .../exoplayer2/offline/DownloadManager.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) 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 2df2069a89..046f0d7593 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 @@ -472,6 +472,16 @@ public final class DownloadManager { /** The task failed. */ public static final int STATE_ERROR = 6; + /** Returns whether the task is running. */ + public static boolean isRunning(int state) { + return state == STATE_STARTED || state == STATE_STOPPING || state == STATE_CANCELING; + } + + /** Returns whether the task is finished. */ + public static boolean isFinished(int state) { + return state == STATE_ERROR || state == STATE_ENDED || state == STATE_CANCELED; + } + /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { switch (state) { @@ -530,7 +540,12 @@ public final class DownloadManager { /** Returns whether the task is finished. */ public boolean isFinished() { - return state == STATE_ERROR || state == STATE_ENDED || state == STATE_CANCELED; + return isFinished(state); + } + + /** Returns whether the task is running. */ + public boolean isRunning() { + return isRunning(state); } } @@ -559,11 +574,6 @@ public final class DownloadManager { id, downloadAction, currentState, getDownloadPercentage(), getDownloadedBytes(), error); } - /** Returns the {@link DownloadAction}. */ - public DownloadAction getDownloadAction() { - return downloadAction; - } - /** Returns the state of the task. */ public @State int getState() { return currentState; @@ -571,15 +581,12 @@ public final class DownloadManager { /** Returns whether the task is finished. */ public boolean isFinished() { - return currentState == STATE_ERROR || currentState == STATE_ENDED - || currentState == STATE_CANCELED; + return DownloadState.isFinished(currentState); } /** Returns whether the task is running. */ public boolean isRunning() { - return currentState == STATE_STARTED - || currentState == STATE_STOPPING - || currentState == STATE_CANCELING; + return DownloadState.isRunning(currentState); } /** From 61ba0f28279678158d43dcf52b55951c1d4ea836 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 26 Jan 2018 08:42:46 -0800 Subject: [PATCH 004/376] Fix FilteringHlsPlaylistParser Only filter HlsMasterPlaylists. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=183394956 --- .../source/hls/offline/HlsDownloadAction.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java index 9787ffb0d6..a5fac7129a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java @@ -26,30 +26,30 @@ import java.io.IOException; /** An action to download or remove downloaded HLS streams. */ public final class HlsDownloadAction extends SegmentDownloadAction { - public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer() { - @Override - public String getType() { - return TYPE; - } + @Override + public String getType() { + return TYPE; + } - @Override - protected String readKey(DataInputStream input) throws IOException { - return input.readUTF(); - } + @Override + protected String readKey(DataInputStream input) throws IOException { + return input.readUTF(); + } - @Override - protected String[] createKeyArray(int keyCount) { - return new String[0]; - } + @Override + protected String[] createKeyArray(int keyCount) { + return new String[keyCount]; + } - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String data, String[] keys) { - return new HlsDownloadAction(manifestUri, removeAction, data, keys); - } - - }; + @Override + protected DownloadAction createDownloadAction( + Uri manifestUri, boolean removeAction, String data, String[] keys) { + return new HlsDownloadAction(manifestUri, removeAction, data, keys); + } + }; private static final String TYPE = "HlsDownloadAction"; From a075b23893ba48e1e043cb9e38b35e150443506c Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 1 Feb 2018 06:23:11 -0800 Subject: [PATCH 005/376] Fix API level 16 method use without guard and refactoring For below API level 16, the logic copied from ConnectivityManagerCompat. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184131406 --- .../util/scheduler/Requirements.java | 87 +++++++++++-------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java index 80895241c2..64b4ccdc31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java @@ -141,45 +141,27 @@ public final class Requirements { if (networkInfo == null || !networkInfo.isConnected()) { logd("No network info or no connection."); return false; - } else if (Util.SDK_INT >= 23) { - // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API - // levels prior to 23. - Network activeNetwork = connectivityManager.getActiveNetwork(); - if (activeNetwork == null) { - logd("No active network."); - return false; - } - NetworkCapabilities networkCapabilities = - connectivityManager.getNetworkCapabilities(activeNetwork); - if (networkCapabilities == null - || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { - logd("Net capability isn't validated."); - return false; - } } - boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered(); - switch (networkRequirement) { - case NETWORK_TYPE_ANY: - return true; - case NETWORK_TYPE_UNMETERED: - if (activeNetworkMetered) { - logd("Network is metered."); - } - return !activeNetworkMetered; - case NETWORK_TYPE_NOT_ROAMING: - boolean roaming = networkInfo.isRoaming(); - if (roaming) { - logd("Roaming."); - } - return !roaming; - case NETWORK_TYPE_METERED: - if (!activeNetworkMetered) { - logd("Network isn't metered."); - } - return activeNetworkMetered; - default: - throw new IllegalStateException(); + if (!checkInternetConnectivity(connectivityManager)) { + return false; } + if (networkRequirement == NETWORK_TYPE_ANY) { + return true; + } + if (networkRequirement == NETWORK_TYPE_NOT_ROAMING) { + boolean roaming = networkInfo.isRoaming(); + logd("Roaming: " + roaming); + return !roaming; + } + boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo); + logd("Metered network: " + activeNetworkMetered); + if (networkRequirement == NETWORK_TYPE_UNMETERED) { + return !activeNetworkMetered; + } + if (networkRequirement == NETWORK_TYPE_METERED) { + return activeNetworkMetered; + } + throw new IllegalStateException(); } private boolean checkChargingRequirement(Context context) { @@ -206,6 +188,37 @@ public final class Requirements { : Util.SDK_INT >= 20 ? !powerManager.isInteractive() : !powerManager.isScreenOn(); } + private static boolean checkInternetConnectivity(ConnectivityManager connectivityManager) { + if (Util.SDK_INT < 23) { + // TODO Check internet connectivity using http://clients3.google.com/generate_204 on API + // levels prior to 23. + return true; + } + Network activeNetwork = connectivityManager.getActiveNetwork(); + if (activeNetwork == null) { + logd("No active network."); + return false; + } + NetworkCapabilities networkCapabilities = + connectivityManager.getNetworkCapabilities(activeNetwork); + boolean validated = + networkCapabilities == null + || !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + logd("Network capability validated: " + validated); + return !validated; + } + + private static boolean isActiveNetworkMetered( + ConnectivityManager connectivityManager, NetworkInfo networkInfo) { + if (Util.SDK_INT >= 16) { + return connectivityManager.isActiveNetworkMetered(); + } + int type = networkInfo.getType(); + return type != ConnectivityManager.TYPE_WIFI + && type != ConnectivityManager.TYPE_BLUETOOTH + && type != ConnectivityManager.TYPE_ETHERNET; + } + private static void logd(String message) { if (Scheduler.DEBUG) { Log.d(TAG, message); From 10033623e78827ba4f29b87db289c7fadbc9b4c7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 1 Feb 2018 09:32:21 -0800 Subject: [PATCH 006/376] Handle repeat mode/shuffle mode changes in MediaPeriodQueue This should be a no-op change. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184150266 --- .../exoplayer2/ExoPlayerImplInternal.java | 65 +++++------------- .../android/exoplayer2/MediaPeriodQueue.java | 67 ++++++++++++++----- 2 files changed, 67 insertions(+), 65 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 977620c5de..c6e0460d54 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 @@ -416,54 +416,29 @@ import java.util.Collections; private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; - queue.setRepeatMode(repeatMode); - validateExistingPeriodHolders(); + if (!queue.updateRepeatMode(repeatMode)) { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } } private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) throws ExoPlaybackException { this.shuffleModeEnabled = shuffleModeEnabled; - queue.setShuffleModeEnabled(shuffleModeEnabled); - validateExistingPeriodHolders(); + if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) { + seekToCurrentPosition(/* sendDiscontinuity= */ true); + } } - private void validateExistingPeriodHolders() throws ExoPlaybackException { - // Find the last existing period holder that matches the new period order. - MediaPeriodHolder lastValidPeriodHolder = queue.getFrontPeriod(); - if (lastValidPeriodHolder == null) { - return; - } - while (true) { - int nextPeriodIndex = playbackInfo.timeline.getNextPeriodIndex( - lastValidPeriodHolder.info.id.periodIndex, period, window, repeatMode, - shuffleModeEnabled); - while (lastValidPeriodHolder.next != null - && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { - lastValidPeriodHolder = lastValidPeriodHolder.next; - } - if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null - || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) { - break; - } - lastValidPeriodHolder = lastValidPeriodHolder.next; - } - - // Release any period holders that don't match the new period order. - boolean readingPeriodRemoved = queue.removeAfter(lastValidPeriodHolder); - - // Update the period info for the last holder, as it may now be the last period in the timeline. - lastValidPeriodHolder.info = queue.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); - - if (readingPeriodRemoved && queue.hasPlayingPeriod()) { - // Renderers may have read from a period that's been removed. Seek back to the current - // position of the playing period to make sure none of the removed period is played. - MediaPeriodId periodId = queue.getPlayingPeriod().info.id; - long newPositionUs = - seekToPeriodPosition( - periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); - if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = - playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException { + // Renderers may have read from a period that's been removed. Seek back to the current + // position of the playing period to make sure none of the removed period is played. + MediaPeriodId periodId = queue.getPlayingPeriod().info.id; + long newPositionUs = + seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = + playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + if (sendDiscontinuity) { playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } @@ -1271,13 +1246,7 @@ import java.util.Collections; // The holder is inconsistent with the new timeline. boolean readingPeriodRemoved = queue.removeAfter(previousPeriodHolder); if (readingPeriodRemoved) { - // Renderers may have read from a period that's been removed. Seek back to the current - // position of the playing period to make sure none of the removed period is played. - MediaPeriodId id = queue.getPlayingPeriod().info.id; - long newPositionUs = - seekToPeriodPosition(id, playbackInfo.positionUs, /* forceDisableRenderers= */ true); - playbackInfo = - playbackInfo.fromNewPosition(id, newPositionUs, playbackInfo.contentPositionUs); + seekToCurrentPosition(/* sendDiscontinuity= */ false); } break; } 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 8bc93ae243..65048795e6 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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.Assertions; * loading media period at the end of the queue, with methods for controlling loading and updating * the queue. Also has a reference to the media period currently being read. */ +@SuppressWarnings("UngroupedOverloads") /* package */ final class MediaPeriodQueue { /** @@ -66,19 +67,21 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period - * information taking into account the new repeat mode. + * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled. + * If not, it is necessary to seek to the current playback position. */ - public void setRepeatMode(@RepeatMode int repeatMode) { + public boolean updateRepeatMode(@RepeatMode int repeatMode) { this.repeatMode = repeatMode; + return updateForPlaybackModeChange(); } /** - * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period - * information taking into account the shuffle mode. + * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully + * handled. If not, it is necessary to seek to the current playback position. */ - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; + return updateForPlaybackModeChange(); } /** Returns whether {@code mediaPeriod} is the current loading media period. */ @@ -286,17 +289,6 @@ import com.google.android.exoplayer2.util.Assertions; length = 0; } - /** - * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into - * account the current timeline. - * - * @param mediaPeriodInfo Media period info for a media period based on an old timeline. - * @return The updated media period info for the current timeline. - */ - public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) { - return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id); - } - /** * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into * account the current timeline, and with the period index updated to {@code newPeriodIndex}. @@ -333,6 +325,47 @@ import com.google.android.exoplayer2.util.Assertions; // Internal methods. + /** + * Updates the queue for any playback mode change, and returns whether the change was fully + * handled. If not, it is necessary to seek to the current playback position. + */ + private boolean updateForPlaybackModeChange() { + // Find the last existing period holder that matches the new period order. + MediaPeriodHolder lastValidPeriodHolder = getFrontPeriod(); + if (lastValidPeriodHolder == null) { + return true; + } + while (true) { + int nextPeriodIndex = + timeline.getNextPeriodIndex( + lastValidPeriodHolder.info.id.periodIndex, + period, + window, + repeatMode, + shuffleModeEnabled); + while (lastValidPeriodHolder.next != null + && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { + lastValidPeriodHolder = lastValidPeriodHolder.next; + } + if (nextPeriodIndex == C.INDEX_UNSET + || lastValidPeriodHolder.next == null + || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) { + break; + } + lastValidPeriodHolder = lastValidPeriodHolder.next; + } + + // Release any period holders that don't match the new period order. + boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); + + // Update the period info for the last holder, as it may now be the last period in the timeline. + lastValidPeriodHolder.info = + getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info, lastValidPeriodHolder.info.id); + + // If renderers may have read from a period that's been removed, it is necessary to restart. + return !readingPeriodRemoved || !hasPlayingPeriod(); + } + /** * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. */ From 451721d4a7ea490c25b6cb0327042f55194f0299 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Feb 2018 05:52:30 -0800 Subject: [PATCH 007/376] Update internal opus build ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184274139 --- .../com/google/android/exoplayer2/ext/opus/OpusLibrary.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 22985ea497..4cb3ce3190 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 @@ -27,7 +27,7 @@ public final class OpusLibrary { ExoPlayerLibraryInfo.registerModule("goog.exo.opus"); } - private static final LibraryLoader LOADER = new LibraryLoader("opus", "opusJNI"); + private static final LibraryLoader LOADER = new LibraryLoader("opusJNI"); private OpusLibrary() {} From 0b2497799f0c225a9c279f5a8a3b369cb3545394 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Feb 2018 09:41:31 -0800 Subject: [PATCH 008/376] Interrupt the test thread for uncaught errors This makes assertion errors in code running on the Looper less easy to miss. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184294290 --- .../com/google/android/exoplayer2/RobolectricUtil.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java b/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java index af2a0ef8c6..0b5740b72c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java @@ -117,7 +117,14 @@ public final class RobolectricUtil { } } if (!isRemoved) { - target.dispatchMessage(pendingMessage.message); + try { + target.dispatchMessage(pendingMessage.message); + } catch (Throwable t) { + // Interrupt the main thread to terminate the test. Robolectric's HandlerThread will + // print the rethrown error to standard output. + Looper.getMainLooper().getThread().interrupt(); + throw t; + } } } if (Util.SDK_INT >= 21) { From 75574928e5d9aba554771fe759bb44ba5db877b9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Feb 2018 10:09:24 -0800 Subject: [PATCH 009/376] Only override gapless data if set in ClippingMediaPeriod This avoids reading a format that is not equal because of switching between NO_VALUE and 0. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184298076 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 3f2c5ec894..f14c0faad4 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 @@ -292,11 +292,13 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { - // Clear gapless playback metadata if the start/end points don't match the media. Format format = formatHolder.format; - int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; - int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; - formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + if (format.encoderDelay != Format.NO_VALUE || format.encoderPadding != Format.NO_VALUE) { + // Clear gapless playback metadata if the start/end points don't match the media. + int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; + int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; + formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + } return C.RESULT_FORMAT_READ; } if (endUs != C.TIME_END_OF_SOURCE From c57210caaf1ecc6a4200a02abdebc6e2130b4140 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Fri, 2 Feb 2018 12:16:54 -0800 Subject: [PATCH 010/376] Reduce Libvpx output buffers to 8, to reduce the chances of out of memory errors. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184317120 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 101f05cd82..d93aa6d39e 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 @@ -98,7 +98,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { * The number of output buffers. The renderer may limit the minimum possible value due to * requiring multiple output buffers to be dequeued at a time for it to make progress. */ - private static final int NUM_OUTPUT_BUFFERS = 16; + private static final int NUM_OUTPUT_BUFFERS = 8; /** * The initial input buffer size. Input buffers are reallocated dynamically if this value is * insufficient. From e437248f4f9f8e9c06d580715503b7d1129f61d7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 5 Feb 2018 02:30:25 -0800 Subject: [PATCH 011/376] Refer to E-AC3 JOC rather than Atmos in MIME type/comments ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184501752 --- .../java/com/google/android/exoplayer2/audio/Ac3Util.java | 4 ++-- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 8 ++++---- .../com/google/android/exoplayer2/util/MimeTypes.java | 6 +++--- .../source/dash/manifest/DashManifestParser.java | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) 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 5797e73740..f45a6a11c6 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 @@ -198,7 +198,7 @@ public final class Ac3Util { if (data.bytesLeft() > 0) { nextByte = data.readUnsignedByte(); if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a - mimeType = MimeTypes.AUDIO_ATMOS; + mimeType = MimeTypes.AUDIO_E_AC3_JOC; } } return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, @@ -385,7 +385,7 @@ public final class Ac3Util { if (data.readBit()) { // addbsie int addbsil = data.readBits(6); if (addbsil == 1 && data.readBits(8) == 1) { // addbsi - mimeType = MimeTypes.AUDIO_ATMOS; + mimeType = MimeTypes.AUDIO_E_AC3_JOC; } } } else /* is AC-3 */ { 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 7ae8eb3cd4..b80780884c 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 @@ -158,8 +158,8 @@ public final class MediaCodecUtil { + ". Assuming: " + decoderInfos.get(0).name); } } - if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) { - // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D. + if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) { + // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D. CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure); ArrayList eac3DecoderInfos = getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); @@ -382,8 +382,8 @@ public final class MediaCodecUtil { return false; } - // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041]. - if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType) + // 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)) { return false; } 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 3e65a754e2..f39f897567 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 @@ -52,7 +52,7 @@ public final class MimeTypes { public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; - public static final String AUDIO_ATMOS = BASE_TYPE_AUDIO + "/eac3-joc"; + public static final String AUDIO_E_AC3_JOC = BASE_TYPE_AUDIO + "/eac3-joc"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -200,7 +200,7 @@ public final class MimeTypes { } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; } else if (codec.startsWith("ec+3")) { - return MimeTypes.AUDIO_ATMOS; + return MimeTypes.AUDIO_E_AC3_JOC; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -258,7 +258,7 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: - case MimeTypes.AUDIO_ATMOS: + case MimeTypes.AUDIO_E_AC3_JOC: return C.ENCODING_E_AC3; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; 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 1b1e24005b..1416e9beeb 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 @@ -1072,7 +1072,7 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = descriptor.schemeIdUri; if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) && "ec+3".equals(descriptor.value)) { - return MimeTypes.AUDIO_ATMOS; + return MimeTypes.AUDIO_E_AC3_JOC; } } return MimeTypes.AUDIO_E_AC3; From de293af3a42b15b1ac809d55d31fde756f7a565d Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 5 Feb 2018 02:36:34 -0800 Subject: [PATCH 012/376] Fix not thread safe static buffer usage DefaultExtractorInput.SCRATCH_SPACE buffer is used to skip data by reading it into this buffer and discarding. Simultaneous use of skip methods corrupts this buffer. Normally the read data is discarded so it doesn't matter but the underlying DataSource may use the buffer too. If it's a CacheDataSource it uses this buffer to read data from upstream then write to cache. Issue: #3762 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184502170 --- .../exoplayer2/extractor/DefaultExtractorInput.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java index 87355a6c78..c3f6304091 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java @@ -30,8 +30,9 @@ public final class DefaultExtractorInput implements ExtractorInput { private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024; private static final int PEEK_MAX_FREE_SPACE = 512 * 1024; - private static final byte[] SCRATCH_SPACE = new byte[4096]; + private static final int SCRATCH_SPACE_SIZE = 4096; + private final byte[] scratchSpace; private final DataSource dataSource; private final long streamLength; @@ -50,6 +51,7 @@ public final class DefaultExtractorInput implements ExtractorInput { this.position = position; this.streamLength = length; peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE]; + scratchSpace = new byte[SCRATCH_SPACE_SIZE]; } @Override @@ -84,7 +86,7 @@ public final class DefaultExtractorInput implements ExtractorInput { int bytesSkipped = skipFromPeekBuffer(length); if (bytesSkipped == 0) { bytesSkipped = - readFromDataSource(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.length), 0, true); + readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true); } commitBytesRead(bytesSkipped); return bytesSkipped; @@ -95,8 +97,9 @@ public final class DefaultExtractorInput implements ExtractorInput { throws IOException, InterruptedException { int bytesSkipped = skipFromPeekBuffer(length); while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) { - bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped, - Math.min(length, bytesSkipped + SCRATCH_SPACE.length), bytesSkipped, allowEndOfInput); + int minLength = Math.min(length, bytesSkipped + scratchSpace.length); + bytesSkipped = + readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput); } commitBytesRead(bytesSkipped); return bytesSkipped != C.RESULT_END_OF_INPUT; From 901dd19e3e494aba8a24c1ccfbe800636209d97a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 5 Feb 2018 05:15:40 -0800 Subject: [PATCH 013/376] Fix media period queue updating for ads Resolve the media period for ad playback when resolving a subsequent period and when receiving a timeline where the playing period in range (but wasn't before). Fix the seek position calculation when a current ad must be skipped and is followed by another ad. Check MediaPeriodInfos match when checking MediaPeriodHolders, to handle cases where a future ad should no longer be played. This may involve playing two content media periods consecutively. Issue: #3584 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184514558 --- .../source/ConcatenatingMediaSourceTest.java | 12 +- .../DynamicConcatenatingMediaSourceTest.java | 12 +- .../exoplayer2/ExoPlayerImplInternal.java | 86 ++++------- .../android/exoplayer2/MediaPeriodInfo.java | 5 +- .../android/exoplayer2/MediaPeriodQueue.java | 142 ++++++++++++------ .../android/exoplayer2/PlaybackInfo.java | 7 +- .../source/ads/AdPlaybackState.java | 4 +- .../android/exoplayer2/ExoPlayerTest.java | 52 +++++++ .../exoplayer2/testutil/FakeTimeline.java | 70 +++++---- 9 files changed, 249 insertions(+), 141 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 14f6563cda..5b3b684ebb 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -212,8 +212,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase { // Create media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); - Timeline timelineWithAds = new FakeTimeline( - new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + Timeline timelineWithAds = + new FakeTimeline( + new TimelineWindowDefinition( + 2, + 222, + true, + false, + 10 * C.MICROS_PER_SECOND, + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index af4b149c98..5198cde72e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -597,8 +597,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Create dynamic media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); - Timeline timelineWithAds = new FakeTimeline( - new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); + Timeline timelineWithAds = + new FakeTimeline( + new TimelineWindowDefinition( + 2, + 222, + true, + false, + 10 * C.MICROS_PER_SECOND, + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); mediaSource.addMediaSource(mediaSourceContentOnly); 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 c6e0460d54..e05068a7b3 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 @@ -1145,8 +1145,9 @@ import java.util.Collections; int periodIndex = periodPosition.first; long positionUs = periodPosition.second; MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, - positionUs); + playbackInfo = + playbackInfo.fromNewPosition( + periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1157,18 +1158,30 @@ import java.util.Collections; int periodIndex = defaultPosition.first; long startPositionUs = defaultPosition.second; MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, - periodId.isAd() ? 0 : startPositionUs, startPositionUs); + playbackInfo = + playbackInfo.fromNewPosition( + periodId, + periodId.isAd() ? 0 : startPositionUs, + /* contentPositionUs= */ startPositionUs); } } return; } int playingPeriodIndex = playbackInfo.periodId.periodIndex; - MediaPeriodHolder periodHolder = queue.getFrontPeriod(); - if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { + long contentPositionUs = playbackInfo.contentPositionUs; + if (oldTimeline.isEmpty()) { + // If the old timeline is empty, the period queue is also empty. + if (!timeline.isEmpty()) { + MediaPeriodId periodId = + queue.resolveMediaPeriodIdForAds(playingPeriodIndex, contentPositionUs); + playbackInfo = + playbackInfo.fromNewPosition( + periodId, periodId.isAd() ? 0 : contentPositionUs, contentPositionUs); + } return; } + MediaPeriodHolder periodHolder = queue.getFrontPeriod(); Object playingPeriodUid = periodHolder == null ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); @@ -1185,7 +1198,8 @@ import java.util.Collections; Pair defaultPosition = getPeriodPosition(timeline, timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); newPeriodIndex = defaultPosition.first; - long newPositionUs = defaultPosition.second; + contentPositionUs = defaultPosition.second; + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs); timeline.getPeriod(newPeriodIndex, period, true); if (periodHolder != null) { // Clear the index of each holder that doesn't contain the default position. If a holder @@ -1202,9 +1216,8 @@ import java.util.Collections; } } // Actually do the seek. - MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); - newPositionUs = seekToPeriodPosition(periodId, newPositionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET); + long seekPositionUs = seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs); + playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs); return; } @@ -1213,53 +1226,20 @@ import java.util.Collections; playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } - if (playbackInfo.periodId.isAd()) { - // Check that the playing ad hasn't been marked as played. If it has, skip forward. - MediaPeriodId periodId = - queue.resolveMediaPeriodIdForAds(periodIndex, playbackInfo.contentPositionUs); - if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { - long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); - long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs); + MediaPeriodId playingPeriodId = playbackInfo.periodId; + if (playingPeriodId.isAd()) { + MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs); + if (!periodId.equals(playingPeriodId)) { + // The previously playing ad should no longer be played, so skip it. + long seekPositionUs = + seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs); + playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs); return; } } - if (periodHolder == null) { - // We don't have any period holders, so we're done. - return; - } - - // Update the holder indices. If we find a subsequent holder that's inconsistent with the new - // timeline then take appropriate action. - periodHolder = updatePeriodInfo(periodHolder, periodIndex); - while (periodHolder.next != null) { - MediaPeriodHolder previousPeriodHolder = periodHolder; - periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, - shuffleModeEnabled); - if (periodIndex != C.INDEX_UNSET - && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { - // The holder is consistent with the new timeline. Update its index and continue. - periodHolder = updatePeriodInfo(periodHolder, periodIndex); - } else { - // The holder is inconsistent with the new timeline. - boolean readingPeriodRemoved = queue.removeAfter(previousPeriodHolder); - if (readingPeriodRemoved) { - seekToCurrentPosition(/* sendDiscontinuity= */ false); - } - break; - } - } - } - - private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { - while (true) { - periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); - if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) { - return periodHolder; - } - periodHolder = periodHolder.next; + if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) { + seekToCurrentPosition(/* sendDiscontinuity= */ false); } } 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 a415f9f0a7..fce1780b71 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 @@ -36,8 +36,9 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; */ public final long contentPositionUs; /** - * The duration of the media to play within the media period, in microseconds, or {@link - * C#TIME_UNSET} if not known. + * The duration of the media period, like {@link #endPositionUs} but with {@link + * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if + * the end position is not known. */ public final long durationUs; /** 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 65048795e6..0c643ec120 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 @@ -59,8 +59,8 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information - * taking into account the new timeline. + * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the + * queued media periods to take into account the new timeline. */ public void setTimeline(Timeline timeline) { this.timeline = timeline; @@ -121,8 +121,7 @@ import com.google.android.exoplayer2.util.Assertions; long rendererPositionUs, PlaybackInfo playbackInfo) { return loading == null ? getFirstMediaPeriodInfo(playbackInfo) - : getFollowingMediaPeriodInfo( - loading.info, loading.getRendererOffset(), rendererPositionUs); + : getFollowingMediaPeriodInfo(loading, rendererPositionUs); } /** @@ -289,6 +288,61 @@ import com.google.android.exoplayer2.util.Assertions; length = 0; } + /** + * Updates media periods in the queue to take into account the latest timeline, and returns + * whether the timeline change has been fully handled. If not, it is necessary to seek to the + * current playback position. + * + * @param playingPeriodId The current playing media period identifier. + * @param rendererPositionUs The current renderer position in microseconds. + * @return Whether the timeline change has been handled completely. + */ + public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) { + // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline + // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be + // handled here. + int periodIndex = playingPeriodId.periodIndex; + // The front period is either playing now, or is being loaded and will become the playing + // period. + MediaPeriodHolder previousPeriodHolder = null; + MediaPeriodHolder periodHolder = getFrontPeriod(); + while (periodHolder != null) { + if (previousPeriodHolder == null) { + periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + } else { + // Check this period holder still follows the previous one, based on the new timeline. + MediaPeriodInfo periodInfo = + getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); + if (periodInfo == null) { + // We've loaded a next media period that is not in the new timeline. + return !removeAfter(previousPeriodHolder); + } + // Update the period index. + periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); + // Check the media period information matches the new timeline. + if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) { + return !removeAfter(previousPeriodHolder); + } + } + + if (periodHolder.info.isLastInTimelinePeriod) { + // Move on to the next timeline period, if there is one. + periodIndex = + timeline.getNextPeriodIndex( + periodIndex, period, window, repeatMode, shuffleModeEnabled); + if (periodIndex == C.INDEX_UNSET + || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { + // The holder is inconsistent with the new timeline. + return previousPeriodHolder == null || !removeAfter(previousPeriodHolder); + } + } + + previousPeriodHolder = periodHolder; + periodHolder = periodHolder.next; + } + return true; + } + /** * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into * account the current timeline, and with the period index updated to {@code newPeriodIndex}. @@ -325,6 +379,17 @@ import com.google.android.exoplayer2.util.Assertions; // Internal methods. + /** + * Returns whether {@code periodHolder} can be kept for playing the media period described by + * {@code info}. + */ + private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) { + MediaPeriodInfo periodHolderInfo = periodHolder.info; + return periodHolderInfo.startPositionUs == info.startPositionUs + && periodHolderInfo.endPositionUs == info.endPositionUs + && periodHolderInfo.id.equals(info.id); + } + /** * Updates the queue for any playback mode change, and returns whether the change was fully * handled. If not, it is necessary to seek to the current playback position. @@ -375,28 +440,25 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}. + * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s + * media period. * - * @param currentMediaPeriodInfo The current media period info. - * @param rendererOffsetUs The current renderer offset in microseconds. + * @param mediaPeriodHolder The media period holder. * @param rendererPositionUs The current renderer position in microseconds. - * @return The following media period info, or {@code null} if it is not yet possible to get the + * @return The following media period's info, or {@code null} if it is not yet possible to get the * next media period info. */ - private MediaPeriodInfo getFollowingMediaPeriodInfo( - MediaPeriodInfo currentMediaPeriodInfo, long rendererOffsetUs, long rendererPositionUs) { + private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo( + MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) { // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod // but if the timeline is not ready to provide the next period it can't return a non-null value // until the timeline is updated. Store whether the next timeline period is ready when the // timeline is updated, to avoid repeatedly checking the same timeline. - if (currentMediaPeriodInfo.isLastInTimelinePeriod) { + MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info; + if (mediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex( - currentMediaPeriodInfo.id.periodIndex, - period, - window, - repeatMode, - shuffleModeEnabled); + mediaPeriodInfo.id.periodIndex, period, window, repeatMode, shuffleModeEnabled); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -411,7 +473,7 @@ import com.google.android.exoplayer2.util.Assertions; // interruptions). Hence we project the default start position forward by the duration of // the buffer, and start buffering from this point. long defaultPositionProjectionUs = - rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs; + mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs; Pair defaultPosition = timeline.getPeriodPosition( window, @@ -431,10 +493,10 @@ import com.google.android.exoplayer2.util.Assertions; return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); } - MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id; + MediaPeriodId currentPeriodId = mediaPeriodInfo.id; + timeline.getPeriod(currentPeriodId.periodIndex, period); if (currentPeriodId.isAd()) { int currentAdGroupIndex = currentPeriodId.adGroupIndex; - timeline.getPeriod(currentPeriodId.periodIndex, period); int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { return null; @@ -448,29 +510,24 @@ import com.google.android.exoplayer2.util.Assertions; currentPeriodId.periodIndex, currentAdGroupIndex, nextAdIndexInAdGroup, - currentMediaPeriodInfo.contentPositionUs); + mediaPeriodInfo.contentPositionUs); } else { // Play content from the ad group position. - int nextAdGroupIndex = - period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs); - long endUs = - nextAdGroupIndex == C.INDEX_UNSET - ? C.TIME_END_OF_SOURCE - : period.getAdGroupTimeUs(nextAdGroupIndex); return getMediaPeriodInfoForContent( - currentPeriodId.periodIndex, currentMediaPeriodInfo.contentPositionUs, endUs); + currentPeriodId.periodIndex, mediaPeriodInfo.contentPositionUs); } - } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { + } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { // Play the next ad group if it's available. - int nextAdGroupIndex = - period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs); + int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs); + if (nextAdGroupIndex == C.INDEX_UNSET) { + // The next ad group can't be played. Play content from the ad group position instead. + return getMediaPeriodInfoForContent( + currentPeriodId.periodIndex, mediaPeriodInfo.endPositionUs); + } return !period.isAdAvailable(nextAdGroupIndex, 0) ? null : getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, - nextAdGroupIndex, - 0, - currentMediaPeriodInfo.endPositionUs); + currentPeriodId.periodIndex, nextAdGroupIndex, 0, mediaPeriodInfo.endPositionUs); } else { // Check if the postroll ad should be played. int adGroupCount = period.getAdGroupCount(); @@ -516,12 +573,7 @@ import com.google.android.exoplayer2.util.Assertions; return getMediaPeriodInfoForAd( id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs); } else { - int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); - long endUs = - nextAdGroupIndex == C.INDEX_UNSET - ? C.TIME_END_OF_SOURCE - : period.getAdGroupTimeUs(nextAdGroupIndex); - return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs); + return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs); } } @@ -548,12 +600,16 @@ import com.google.android.exoplayer2.util.Assertions; isLastInTimeline); } - private MediaPeriodInfo getMediaPeriodInfoForContent( - int periodIndex, long startPositionUs, long endUs) { + private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs) { MediaPeriodId id = new MediaPeriodId(periodIndex); + timeline.getPeriod(id.periodIndex, period); + int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + long endUs = + nextAdGroupIndex == C.INDEX_UNSET + ? C.TIME_END_OF_SOURCE + : period.getAdGroupTimeUs(nextAdGroupIndex); boolean isLastInPeriod = isLastInPeriod(id, endUs); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); - timeline.getPeriod(id.periodIndex, period); long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs; return new MediaPeriodInfo( id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline); 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 65392ba269..bb39bf3d0b 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 @@ -70,11 +70,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this.trackSelectorResult = trackSelectorResult; } - public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs, - long contentPositionUs) { - return fromNewPosition(new MediaPeriodId(periodIndex), startPositionUs, contentPositionUs); - } - public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { return new PlaybackInfo( @@ -82,7 +77,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; manifest, periodId, startPositionUs, - contentPositionUs, + periodId.isAd() ? contentPositionUs : C.TIME_UNSET, playbackState, isLoading, trackSelectorResult); 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 0bd6c9f29f..7b06098d45 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 @@ -211,7 +211,7 @@ public final class AdPlaybackState { public static final int AD_STATE_ERROR = 4; /** Ad playback state with no ads. */ - public static final AdPlaybackState NONE = new AdPlaybackState(new long[0]); + public static final AdPlaybackState NONE = new AdPlaybackState(); /** The number of ad groups. */ public final int adGroupCount; @@ -233,7 +233,7 @@ public final class AdPlaybackState { * @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value * {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad. */ - public AdPlaybackState(long[] adGroupTimesUs) { + public AdPlaybackState(long... adGroupTimesUs) { int count = adGroupTimesUs.length; adGroupCount = count; this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count); 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 3855d5ed2a..f10e889390 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; @@ -384,6 +385,57 @@ public final class ExoPlayerTest { assertThat(renderer.isEnded).isTrue(); } + @Test + public void testAdGroupWithLoadErrorIsSkipped() throws Exception { + AdPlaybackState initialAdPlaybackState = + FakeTimeline.createAdPlaybackState( + /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 5 * C.MICROS_PER_SECOND); + Timeline fakeTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.MICROS_PER_SECOND, + initialAdPlaybackState)); + AdPlaybackState errorAdPlaybackState = initialAdPlaybackState.withAdLoadError(0, 0); + final Timeline adErrorTimeline = + new FakeTimeline( + new TimelineWindowDefinition( + /* periodCount= */ 1, + /* id= */ 0, + /* isSeekable= */ true, + /* isDynamic= */ false, + /* durationUs= */ C.MICROS_PER_SECOND, + errorAdPlaybackState)); + final FakeMediaSource fakeMediaSource = + new FakeMediaSource(fakeTimeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testAdGroupWithLoadErrorIsSkipped") + .pause() + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable( + new Runnable() { + @Override + public void run() { + fakeMediaSource.setNewSourceInfo(adErrorTimeline, null); + } + }) + .waitForTimelineChanged(adErrorTimeline) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(fakeMediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + // There is still one discontinuity from content to content for the failed ad insertion. + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_AD_INSERTION); + } + @Test public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); 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 d0aa4761a4..7b27d3bd80 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,8 +40,7 @@ public final class FakeTimeline extends Timeline { public final boolean isSeekable; public final boolean isDynamic; public final long durationUs; - public final int adGroupsPerPeriodCount; - public final int adsPerAdGroupCount; + public final AdPlaybackState adPlaybackState; /** * Creates a seekable, non-dynamic window definition with one period with a duration of @@ -86,7 +85,7 @@ public final class FakeTimeline extends Timeline { */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { - this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); + this(periodCount, id, isSeekable, isDynamic, durationUs, AdPlaybackState.NONE); } /** @@ -98,19 +97,21 @@ public final class FakeTimeline extends Timeline { * @param isSeekable Whether the window is seekable. * @param isDynamic Whether the window is dynamic. * @param durationUs The duration of the window in microseconds. - * @param adGroupsCountPerPeriod The number of ad groups in each period. The position of the ad - * groups is equally distributed in each period starting. - * @param adsPerAdGroupCount The number of ads in each ad group. + * @param adPlaybackState The ad playback state. */ - public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, - boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { + public TimelineWindowDefinition( + int periodCount, + Object id, + boolean isSeekable, + boolean isDynamic, + long durationUs, + AdPlaybackState adPlaybackState) { this.periodCount = periodCount; this.id = id; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.durationUs = durationUs; - this.adGroupsPerPeriodCount = adGroupsCountPerPeriod; - this.adsPerAdGroupCount = adsPerAdGroupCount; + this.adPlaybackState = adPlaybackState; } } @@ -120,6 +121,27 @@ public final class FakeTimeline extends Timeline { private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; + /** + * Returns an ad playback state with the specified number of ads in each of the specified ad + * groups, each ten seconds long. + * + * @param adsPerAdGroup The number of ads per ad group. + * @param adGroupTimesUs The times of ad groups, in microseconds. + * @return The ad playback state. + */ + public static AdPlaybackState createAdPlaybackState(int adsPerAdGroup, long... adGroupTimesUs) { + int adGroupCount = adGroupTimesUs.length; + AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs); + long[][] adDurationsUs = new long[adGroupCount][]; + for (int i = 0; i < adGroupCount; i++) { + adPlaybackState = adPlaybackState.withAdCount(i, adsPerAdGroup); + adDurationsUs[i] = new long[adsPerAdGroup]; + Arrays.fill(adDurationsUs[i], AD_DURATION_US); + } + adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); + return adPlaybackState; + } + /** * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. @@ -173,27 +195,13 @@ public final class FakeTimeline extends Timeline { Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; - if (windowDefinition.adGroupsPerPeriodCount == 0) { - return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs); - } else { - int adGroups = windowDefinition.adGroupsPerPeriodCount; - long[] adGroupTimesUs = new long[adGroups]; - long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0; - for (int i = 0; i < adGroups; i++) { - adGroupTimesUs[i] = i * adGroupOffset; - } - AdPlaybackState adPlaybackState = new AdPlaybackState(adGroupTimesUs); - long[][] adDurationsUs = new long[adGroups][]; - for (int i = 0; i < adGroups; i++) { - int adCount = windowDefinition.adsPerAdGroupCount; - adPlaybackState = adPlaybackState.withAdCount(i, adCount); - adDurationsUs[i] = new long[adCount]; - Arrays.fill(adDurationsUs[i], AD_DURATION_US); - } - adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs); - return period.set( - id, uid, windowIndex, periodDurationUs, positionInWindowUs, adPlaybackState); - } + return period.set( + id, + uid, + windowIndex, + periodDurationUs, + positionInWindowUs, + windowDefinition.adPlaybackState); } @Override From aa63ad3af03a10c63809996e92b4461c6c1bf1b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 5 Feb 2018 05:38:54 -0800 Subject: [PATCH 014/376] Handle LOG AdEvents for ad group load errors. If IMA loads an empty VAST document for an ad group it notifies via a LOG AdEvent. Handle the event by updating the AdPlaybackState accordingly. The error state will be handled in ExoPlayerImplInternal in a separate change. Issue: #3584 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184516585 --- RELEASENOTES.md | 2 + demos/main/src/main/assets/media.exolist.json | 10 ++++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 49 +++++++++++++------ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1171d35411..1c05a2375f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -81,6 +81,8 @@ * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. * IMA extension: + * Fix the player getting stuck when an ad group fails to load + ([#3584](https://github.com/google/ExoPlayer/issues/3584)). * Work around loadAd not being called beore the LOADED AdEvent arrives ([#3552](https://github.com/google/ExoPlayer/issues/3552)). * Add support for playing non-Extractor content MediaSources in diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 15183a4a8b..7052e7c436 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -566,6 +566,16 @@ "name": "VMAP pre-roll single ad, mid-roll standard pods with 5 ads every 10 seconds for 1:40, post-roll single ad", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpostlongpod&cmsid=496&vid=short_tencue&correlator=" + }, + { + "name": "VMAP empty midroll", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll" + }, + { + "name": "VMAP full, empty, full midrolls", + "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", + "ad_tag_uri": "http://vastsynthesizer.appspot.com/empty-midroll-2" } ] } 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 b632e7ba84..493deed4ad 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 @@ -516,6 +516,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A case LOG: Map adData = adEvent.getAdData(); Log.i(TAG, "Log AdEvent: " + adData); + if ("adLoadError".equals(adData.get("type"))) { + handleAdGroupLoadError(); + } break; case ALL_ADS_COMPLETED: default: @@ -894,6 +897,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } + private void handleAdGroupLoadError() { + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[expectedAdGroupIndex]; + // Ad group load error can be notified more than once, so check if it was already handled. + // TODO: Update the expected ad group index based on the position returned by + // getContentProgress so that it's possible to detect when more than one ad group fails to load + // consecutively. + if (adGroup.count == C.LENGTH_UNSET + || adGroup.states[0] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + if (DEBUG) { + Log.d(TAG, "Removing ad group " + expectedAdGroupIndex + " as it failed to load"); + } + adPlaybackState = adPlaybackState.withAdCount(expectedAdGroupIndex, 1); + adPlaybackState = adPlaybackState.withAdLoadError(expectedAdGroupIndex, 0); + updateAdPlaybackState(); + } + } + private void checkForContentComplete() { if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs @@ -939,6 +959,21 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); } + /** + * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all + * ads in the ad group have loaded. + */ + private int getAdIndexInAdGroupToLoad(int adGroupIndex) { + @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; + int adIndexInAdGroup = 0; + // IMA loads ads in order. + while (adIndexInAdGroup < states.length + && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { + adIndexInAdGroup++; + } + return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. @@ -955,18 +990,4 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } - /** - * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all - * ads in the ad group have loaded. - */ - private int getAdIndexInAdGroupToLoad(int adGroupIndex) { - @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states; - int adIndexInAdGroup = 0; - // IMA loads ads in order. - while (adIndexInAdGroup < states.length - && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) { - adIndexInAdGroup++; - } - return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; - } } From 4437de4818c7a52ef17e0f80cfbfb8afb8571495 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Feb 2018 06:48:48 -0800 Subject: [PATCH 015/376] Fix potential media source release before media period release. This could happen when a media source is removed from a DynamicConcatenatingMediaSource and one of its media periods is still active. This media period is only removed by the player after the player received a timeline update and thus we shouldn't release the removed child source as long as it has active media periods. Issue:#3796 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184522836 --- .../DynamicConcatenatingMediaSourceTest.java | 12 +++++++++++ .../DynamicConcatenatingMediaSource.java | 21 +++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 5198cde72e..1a1c07fd61 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -627,6 +627,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { + FakeMediaSource childSource = createFakeMediaSource(); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + MediaPeriod mediaPeriod = testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0)); + mediaSource.removeMediaSource(/* index= */ 0); + testRunner.assertTimelineChangeBlocking(); + testRunner.releasePeriod(mediaPeriod); + childSource.assertReleased(); + testRunner.releaseSource(); + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { 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 index a638992501..7ed8af5954 100644 --- 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 @@ -56,7 +56,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< // Accessed on the playback thread. private final List mediaSourceHolders; private final MediaSourceHolder query; - private final Map mediaSourceByMediaPeriod; + private final Map mediaSourceByMediaPeriod; private final List deferredMediaPeriods; private ExoPlayer player; @@ -355,19 +355,23 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< } else { mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); } - mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource); + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + holder.activeMediaPeriods++; return mediaPeriod; } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod); - mediaSourceByMediaPeriod.remove(mediaPeriod); + MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod); if (mediaPeriod instanceof DeferredMediaPeriod) { deferredMediaPeriods.remove(mediaPeriod); ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } else { - mediaSource.releasePeriod(mediaPeriod); + holder.mediaSource.releasePeriod(mediaPeriod); + } + holder.activeMediaPeriods--; + if (holder.activeMediaPeriods == 0 && holder.isRemoved) { + releaseChildSource(holder); } } @@ -520,7 +524,10 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /* childIndexUpdate= */ -1, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount()); - releaseChildSource(holder); + holder.isRemoved = true; + if (holder.activeMediaPeriods == 0) { + releaseChildSource(holder); + } } private void moveMediaSourceInternal(int currentIndex, int newIndex) { @@ -573,6 +580,8 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< public int firstWindowIndexInChild; public int firstPeriodIndexInChild; public boolean isPrepared; + public boolean isRemoved; + public int activeMediaPeriods; public MediaSourceHolder( MediaSource mediaSource, From 81eed7c2c3930ddd6e7ab43a872c1815adc35428 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 5 Feb 2018 07:34:31 -0800 Subject: [PATCH 016/376] Support isAtomic flag in DynamicConcatenatingMediaSource. This feature is supported in the ConcatenatingMediaSource and is easily copied to this media source. Also adding tests to check whether the atomic property works in normal concatenation and in also in nested use. Also fixes a bug where timeline methods of the DeferredTimeline were not correctly forwarded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184526881 --- .../DynamicConcatenatingMediaSourceTest.java | 89 +++++++++++- .../source/AbstractConcatenatedTimeline.java | 22 ++- .../source/ConcatenatingMediaSource.java | 33 +---- .../DynamicConcatenatingMediaSource.java | 131 ++++++++++++------ .../exoplayer2/source/LoopingMediaSource.java | 2 +- 5 files changed, 197 insertions(+), 80 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 1a1c07fd61..38ac324e69 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -47,7 +47,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { @Override public void setUp() throws Exception { super.setUp(); - mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); + mediaSource = + new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); testRunner = new MediaSourceTestRunner(mediaSource, null); } @@ -627,6 +628,92 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } + public void testAtomicTimelineWindowOrder() throws IOException { + // Release default test runner with non-atomic media source and replace with new test runner. + testRunner.release(); + DynamicConcatenatingMediaSource mediaSource = + new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + } + + public void testNestedTimeline() throws IOException { + DynamicConcatenatingMediaSource nestedSource1 = + new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + DynamicConcatenatingMediaSource nestedSource2 = + new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + mediaSource.addMediaSource(nestedSource1); + mediaSource.addMediaSource(nestedSource2); + testRunner.prepareSource(); + FakeMediaSource[] childSources = createMediaSources(4); + nestedSource1.addMediaSource(childSources[0]); + testRunner.assertTimelineChangeBlocking(); + nestedSource1.addMediaSource(childSources[1]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[2]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[3]); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); + } + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); 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 35234753b0..696a6f6fad 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 @@ -27,14 +27,18 @@ import com.google.android.exoplayer2.Timeline; private final int childCount; private final ShuffleOrder shuffleOrder; + private final boolean isAtomic; /** * Sets up a concatenated timeline with a shuffle order of child timelines. * + * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a + * single item for repeating and shuffling. * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must * match the number of elements in the shuffle order. */ - public AbstractConcatenatedTimeline(ShuffleOrder shuffleOrder) { + public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) { + this.isAtomic = isAtomic; this.shuffleOrder = shuffleOrder; this.childCount = shuffleOrder.getLength(); } @@ -42,6 +46,11 @@ import com.google.android.exoplayer2.Timeline; @Override 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; + shuffleModeEnabled = false; + } // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); @@ -71,6 +80,11 @@ import com.google.android.exoplayer2.Timeline; @Override 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; + shuffleModeEnabled = false; + } // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); @@ -103,6 +117,9 @@ import com.google.android.exoplayer2.Timeline; if (childCount == 0) { return C.INDEX_UNSET; } + if (isAtomic) { + shuffleModeEnabled = false; + } // Find last non-empty child. int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { @@ -121,6 +138,9 @@ import com.google.android.exoplayer2.Timeline; if (childCount == 0) { return C.INDEX_UNSET; } + if (isAtomic) { + shuffleModeEnabled = false; + } // Find first non-empty child. int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { 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 48de7de364..c29367e109 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.source; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -173,10 +172,9 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource mediaSourceByMediaPeriod; private final List deferredMediaPeriods; + private final boolean isAtomic; private ExoPlayer player; private Listener listener; @@ -70,22 +71,35 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< * Creates a new dynamic concatenating media source. */ public DynamicConcatenatingMediaSource() { - this(new DefaultShuffleOrder(0)); + this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new dynamic concatenating media source. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + */ + public DynamicConcatenatingMediaSource(boolean isAtomic) { + this(isAtomic, new DefaultShuffleOrder(0)); } /** * Creates a new dynamic concatenating media source with a custom shuffle order. * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. * This shuffle order must be empty. */ - public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) { + public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { this.shuffleOrder = shuffleOrder; this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); this.deferredMediaPeriods = new ArrayList<>(1); this.query = new MediaSourceHolder(null, null, -1, -1, -1); + this.isAtomic = isAtomic; } /** @@ -446,8 +460,10 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { if (!preventListenerNotification) { - listener.onSourceInfoRefreshed(this, - new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), + listener.onSourceInfoRefreshed( + this, + new ConcatenatedTimeline( + mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), null); if (actionOnCompletion != null) { player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); @@ -652,9 +668,13 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private final int[] uids; private final SparseIntArray childIndexByUid; - public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, - int periodCount, ShuffleOrder shuffleOrder) { - super(shuffleOrder); + 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(); @@ -728,61 +748,39 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the * DeferredTimeline is used to keep the originally assigned first period ID. */ - private static final class DeferredTimeline extends Timeline { + private static final class DeferredTimeline extends ForwardingTimeline { private static final Object DUMMY_ID = new Object(); private static final Period period = new Period(); + private static final DummyTimeline dummyTimeline = new DummyTimeline(); - private final Timeline timeline; - private final Object replacedID; + private final Object replacedId; public DeferredTimeline() { - timeline = null; - replacedID = null; + this(dummyTimeline, /* replacedId= */ null); } - private DeferredTimeline(Timeline timeline, Object replacedID) { - this.timeline = timeline; - this.replacedID = replacedID; + private DeferredTimeline(Timeline timeline, Object replacedId) { + super(timeline); + this.replacedId = replacedId; } public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { - return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0 - ? timeline.getPeriod(0, period, true).uid : replacedID); + return new DeferredTimeline( + timeline, + replacedId == null && timeline.getPeriodCount() > 0 + ? timeline.getPeriod(0, period, true).uid + : replacedId); } public Timeline getTimeline() { return timeline; } - @Override - public int getWindowCount() { - return timeline == null ? 1 : timeline.getWindowCount(); - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return timeline == null - // Dynamic window to indicate pending timeline updates. - ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0, - C.TIME_UNSET, 0, 0, 0) - : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return timeline == null ? 1 : timeline.getPeriodCount(); - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - if (timeline == null) { - return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET, - C.TIME_UNSET); - } timeline.getPeriod(periodIndex, period, setIds); - if (period.uid == replacedID) { + if (period.uid == replacedId) { period.uid = DUMMY_ID; } return period; @@ -790,11 +788,54 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Override public int getIndexOfPeriod(Object uid) { - return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET) - : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid); + return timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedId : uid); } - } + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + // Dynamic window to indicate pending timeline updates. + return window.set( + /* id= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* 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= */ null, + /* uid= */ null, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ C.TIME_UNSET); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == null ? 0 : C.INDEX_UNSET; + } + } } 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 5eea1aa1cc..e2ef4eb5fa 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 @@ -106,7 +106,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { - super(new UnshuffledShuffleOrder(loopCount)); + super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount)); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); From 8f81abee868b43e667481212b410e65f4d6dc232 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 6 Feb 2018 05:47:07 -0800 Subject: [PATCH 017/376] Prevent "unexpected read attempt" illegal state exception When using cronet data source, calling read after the end of input has been read will trigger this. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184667794 --- .../google/android/exoplayer2/ext/cronet/CronetDataSource.java | 1 + 1 file changed, 1 insertion(+) 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 536155a70f..29bc874cd8 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 @@ -369,6 +369,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou throw new HttpDataSourceException(exception, currentDataSpec, HttpDataSourceException.TYPE_READ); } else 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. From 165fce4c64d547e01ef4e5f6fbb23ebc06354f2b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 7 Feb 2018 02:44:23 -0800 Subject: [PATCH 018/376] Get the next ad index to play in MediaPeriodQueue The ad index in the ad group may need to skip over ads that failed to load, so it can't just be incremented any more. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184812556 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../android/exoplayer2/MediaPeriodQueue.java | 41 ++++++++++++------- .../google/android/exoplayer2/Timeline.java | 24 ++++++++--- .../source/ads/AdPlaybackState.java | 24 ++++++++--- .../source/ads/AdPlaybackStateTest.java | 22 +++++++--- 5 files changed, 81 insertions(+), 32 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 493deed4ad..987a58d3f7 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 @@ -887,7 +887,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void stopAdInternal() { Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); imaAdState = IMA_AD_STATE_NONE; - int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].nextAdIndexToPlay; + int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. adPlaybackState = adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0); 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 0c643ec120..4b6ef1807e 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 @@ -372,7 +372,7 @@ import com.google.android.exoplayer2.util.Assertions; if (adGroupIndex == C.INDEX_UNSET) { return new MediaPeriodId(periodIndex); } else { - int adIndexInAdGroup = period.getNextAdIndexToPlay(adGroupIndex); + int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); } } @@ -496,19 +496,20 @@ import com.google.android.exoplayer2.util.Assertions; MediaPeriodId currentPeriodId = mediaPeriodInfo.id; timeline.getPeriod(currentPeriodId.periodIndex, period); if (currentPeriodId.isAd()) { - int currentAdGroupIndex = currentPeriodId.adGroupIndex; - int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex); + int adGroupIndex = currentPeriodId.adGroupIndex; + int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex); if (adCountInCurrentAdGroup == C.LENGTH_UNSET) { return null; } - int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1; + int nextAdIndexInAdGroup = + period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup); if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) { // Play the next ad in the ad group if it's available. - return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) + return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup) ? null : getMediaPeriodInfoForAd( currentPeriodId.periodIndex, - currentAdGroupIndex, + adGroupIndex, nextAdIndexInAdGroup, mediaPeriodInfo.contentPositionUs); } else { @@ -524,22 +525,32 @@ import com.google.android.exoplayer2.util.Assertions; return getMediaPeriodInfoForContent( currentPeriodId.periodIndex, mediaPeriodInfo.endPositionUs); } - return !period.isAdAvailable(nextAdGroupIndex, 0) + int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); + return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) ? null : getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, nextAdGroupIndex, 0, mediaPeriodInfo.endPositionUs); + currentPeriodId.periodIndex, + nextAdGroupIndex, + adIndexInAdGroup, + mediaPeriodInfo.endPositionUs); } else { // Check if the postroll ad should be played. int adGroupCount = period.getAdGroupCount(); - if (adGroupCount == 0 - || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE - || period.hasPlayedAdGroup(adGroupCount - 1) - || !period.isAdAvailable(adGroupCount - 1, 0)) { + if (adGroupCount == 0) { + return null; + } + int adGroupIndex = adGroupCount - 1; + if (period.getAdGroupTimeUs(adGroupIndex) != C.TIME_END_OF_SOURCE + || period.hasPlayedAdGroup(adGroupIndex)) { + return null; + } + int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); + if (!period.isAdAvailable(adGroupIndex, adIndexInAdGroup)) { return null; } long contentDurationUs = period.getDurationUs(); return getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, adGroupCount - 1, 0, contentDurationUs); + currentPeriodId.periodIndex, adGroupIndex, adIndexInAdGroup, contentDurationUs); } } @@ -587,7 +598,7 @@ import com.google.android.exoplayer2.util.Assertions; .getPeriod(id.periodIndex, period) .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup); long startPositionUs = - adIndexInAdGroup == period.getNextAdIndexToPlay(adGroupIndex) + adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex) ? period.getAdResumePositionUs() : 0; return new MediaPeriodInfo( @@ -636,7 +647,7 @@ import com.google.android.exoplayer2.util.Assertions; boolean isLastAd = isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1; - return isLastAd || (!isAd && period.getNextAdIndexToPlay(lastAdGroupIndex) == postrollAdCount); + return isLastAd || (!isAd && period.getFirstAdIndexToPlay(lastAdGroupIndex) == postrollAdCount); } private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { 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 26c2cc3e83..5906ffb8e7 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 @@ -381,15 +381,29 @@ public abstract class Timeline { } /** - * Returns the index of the next ad to play in the specified ad group, or the number of ads in - * the ad group if the ad group does not have any ads remaining to play. + * Returns the index of the first ad in the specified ad group that should be played, or the + * number of ads in the ad group if no ads should be played. * * @param adGroupIndex The ad group index. + * @return The index of the first ad that should be played, or the number of ads in the ad group + * if no ads should be played. + */ + public int getFirstAdIndexToPlay(int adGroupIndex) { + return adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + } + + /** + * Returns the index of the next ad in the specified ad group that should be played after + * playing {@code adIndexInAdGroup}, or the number of ads in the ad group if no later ads should + * be played. + * + * @param adGroupIndex The ad group index. + * @param lastPlayedAdIndex The last played ad index in the ad group. * @return The index of the next ad that should be played, or the number of ads in the ad group * if the ad group does not have any ads remaining to play. */ - public int getNextAdIndexToPlay(int adGroupIndex) { - return adPlaybackState.adGroups[adGroupIndex].nextAdIndexToPlay; + public int getNextAdIndexToPlay(int adGroupIndex, int lastPlayedAdIndex) { + return adPlaybackState.adGroups[adGroupIndex].getNextAdIndexToPlay(lastPlayedAdIndex); } /** @@ -400,7 +414,7 @@ public abstract class Timeline { */ public boolean hasPlayedAdGroup(int adGroupIndex) { AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - return adGroup.nextAdIndexToPlay == adGroup.count; + return adGroup.getFirstAdIndexToPlay() == adGroup.count; } /** 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 7b06098d45..02bd67dbd8 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 @@ -49,8 +49,6 @@ public final class AdPlaybackState { public final @AdState int[] states; /** The durations of each ad in the ad group, in microseconds. */ public final long[] durationsUs; - /** The index of the next ad that should be played, or {@link #count} if all ads were played. */ - public final int nextAdIndexToPlay; /** Creates a new ad group with an unspecified number of ads. */ public AdGroup() { @@ -67,14 +65,30 @@ public final class AdPlaybackState { this.states = states; this.uris = uris; this.durationsUs = durationsUs; - int nextAdIndexToPlay; - for (nextAdIndexToPlay = 0; nextAdIndexToPlay < states.length; nextAdIndexToPlay++) { + } + + /** + * Returns the index of the first ad in the ad group that should be played, or {@link #count} if + * no ads should be played. + */ + public int getFirstAdIndexToPlay() { + return getNextAdIndexToPlay(-1); + } + + /** + * Returns the index of the next ad in the ad group that should be played after playing {@code + * lastPlayedAdIndex}, or {@link #count} if no later ads should be played. + */ + public int getNextAdIndexToPlay(int lastPlayedAdIndex) { + int nextAdIndexToPlay = lastPlayedAdIndex + 1; + while (nextAdIndexToPlay < states.length) { if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) { break; } + nextAdIndexToPlay++; } - this.nextAdIndexToPlay = nextAdIndexToPlay; + return nextAdIndexToPlay; } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index 95f492f17f..ca8bf5d393 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -70,29 +70,29 @@ public final class AdPlaybackStateTest { } @Test - public void testInitialNextAdIndexToPlay() { + public void testGetFirstAdIndexToPlayIsZero() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI); - assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(0); + assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(0); } @Test - public void testNextAdIndexToPlayWithPlayedAd() { + public void testGetFirstAdIndexToPlaySkipsPlayedAd() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI); state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); - assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(1); + assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(1); assertThat(state.adGroups[0].states[1]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE); assertThat(state.adGroups[0].states[2]).isEqualTo(AdPlaybackState.AD_STATE_AVAILABLE); } @Test - public void testNextAdIndexToPlaySkipsErrorAds() { + public void testGetFirstAdIndexToPlaySkipsErrorAds() { state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI); state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 2, TEST_URI); @@ -100,7 +100,17 @@ public final class AdPlaybackStateTest { state = state.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0); state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); - assertThat(state.adGroups[0].nextAdIndexToPlay).isEqualTo(2); + assertThat(state.adGroups[0].getFirstAdIndexToPlay()).isEqualTo(2); + } + + @Test + public void testGetNextAdIndexToPlaySkipsErrorAds() { + state = state.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 3); + state = state.withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1, TEST_URI); + + state = state.withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1); + + assertThat(state.adGroups[0].getNextAdIndexToPlay(0)).isEqualTo(2); } @Test From 6fc70149b56d05262349402979d9951c1238bbb8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 7 Feb 2018 06:45:52 -0800 Subject: [PATCH 019/376] Fix ad group skipping Allow skipping an ad group when requested by IMA, even if we aren't currently playing one, to handle cases where no ads in an ad group will load (so IMA requests resuming content but we never managed to start playing an ad). Use the known ad group index (rather than the expected one) when handling ad group load errors. This ensures we skip the right ad group if we notify IMA of playback errors for every ad in the ad group, then IMA notifies that the ad group is empty via a load error. Also make some other miscellaneous small fixes to ads code: - Avoid warning about unexpected ad group indices more than once. - Output a warning if the ad count in an ad group decreases. - Remove unnecessary assertion. - Fix getting the ad duration for ad indices that haven't loaded yet. - Allow setting an ad group state to its current value. - Fix javadoc for setting the ad resume position. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184831495 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 28 +++++++++++++------ .../google/android/exoplayer2/Timeline.java | 3 +- .../source/ads/AdPlaybackState.java | 4 ++- .../exoplayer2/source/ads/AdsLoader.java | 20 ++++++------- 4 files changed, 34 insertions(+), 21 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 987a58d3f7..4f20535ff1 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 @@ -477,8 +477,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); + int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; + if (adCount != oldAdCount) { + if (oldAdCount == C.LENGTH_UNSET) { + adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); + updateAdPlaybackState(); + } else { + // IMA sometimes unexpectedly decreases the ad count in an ad group. + Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); + } + } if (adGroupIndex != expectedAdGroupIndex) { Log.w( TAG, @@ -486,6 +494,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A + expectedAdGroupIndex + ", actual ad group index " + adGroupIndex); + expectedAdGroupIndex = adGroupIndex; } break; case CONTENT_PAUSE_REQUESTED: @@ -536,7 +545,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (adsManager == null) { // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; - adPlaybackState = new AdPlaybackState(new long[0]); + adPlaybackState = new AdPlaybackState(); updateAdPlaybackState(); } if (pendingAdErrorEvent == null) { @@ -866,7 +875,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); } } - if (playingAd && adGroupIndex != C.INDEX_UNSET) { + if (adGroupIndex != C.INDEX_UNSET) { adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex); adGroupIndex = C.INDEX_UNSET; updateAdPlaybackState(); @@ -885,7 +894,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } private void stopAdInternal() { - Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); imaAdState = IMA_AD_STATE_NONE; int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); // TODO: Handle the skipped event so the ad can be marked as skipped rather than played. @@ -898,7 +906,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } private void handleAdGroupLoadError() { - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[expectedAdGroupIndex]; + int adGroupIndex = + this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; // Ad group load error can be notified more than once, so check if it was already handled. // TODO: Update the expected ad group index based on the position returned by // getContentProgress so that it's possible to detect when more than one ad group fails to load @@ -906,10 +916,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (adGroup.count == C.LENGTH_UNSET || adGroup.states[0] == AdPlaybackState.AD_STATE_UNAVAILABLE) { if (DEBUG) { - Log.d(TAG, "Removing ad group " + expectedAdGroupIndex + " as it failed to load"); + Log.d(TAG, "Removing ad group " + adGroupIndex + " as it failed to load"); } - adPlaybackState = adPlaybackState.withAdCount(expectedAdGroupIndex, 1); - adPlaybackState = adPlaybackState.withAdLoadError(expectedAdGroupIndex, 0); + adPlaybackState = + adPlaybackState.withAdCount(adGroupIndex, 1).withAdLoadError(adGroupIndex, 0); updateAdPlaybackState(); } } 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 5906ffb8e7..4b58a1075c 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 @@ -495,7 +495,8 @@ public abstract class Timeline { * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. */ public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { - return adPlaybackState.adGroups[adGroupIndex].durationsUs[adIndexInAdGroup]; + AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; + return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET; } /** 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 02bd67dbd8..2f6f81d6b9 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 @@ -140,7 +140,9 @@ public final class AdPlaybackState { Assertions.checkArgument(count == C.LENGTH_UNSET || index < count); @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1); Assertions.checkArgument( - states[index] == AD_STATE_UNAVAILABLE || states[index] == AD_STATE_AVAILABLE); + states[index] == AD_STATE_UNAVAILABLE + || states[index] == AD_STATE_AVAILABLE + || states[index] == state); long[] durationsUs = this.durationsUs.length == states.length ? this.durationsUs 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 99feccd2f3..c2dfd91301 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 @@ -22,22 +22,22 @@ import java.io.IOException; /** * Interface for loaders of ads, which can be used with {@link AdsMediaSource}. - *

- * Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In + * + *

Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)} * with a new copy of the current {@link AdPlaybackState} whenever further information about ads * becomes known (for example, when an ad media URI is available, or an ad has played to the end). - *

- * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} will be called when the ads media + * + *

{@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} will be called when the ads media * source first initializes, at which point the loader can request ads. If the player enters the * background, {@link #detachPlayer()} will be called. Loaders should maintain any ad playback state * in preparation for a later call to {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. If - * an ad is playing when the player is detached, store the current playback position via - * {@link AdPlaybackState#setAdResumePositionUs(long)}. - *

- * If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the implementation - * of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the same listener to - * provide the existing playback state to the new player. + * an ad is playing when the player is detached, update the ad playback state with the current + * playback position using {@link AdPlaybackState#withAdResumePositionUs(long)}. + * + *

If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the + * implementation of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the + * same listener to provide the existing playback state to the new player. */ public interface AdsLoader { From 0384ff07ef7c30cd095dfae84282fbf75b58d32c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 7 Feb 2018 08:59:32 -0800 Subject: [PATCH 020/376] Add issue link to release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184844870 --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1c05a2375f..bd863fb2f8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -94,7 +94,8 @@ * Fix potential NPE when removing media sources from a DynamicConcatenatingMediaSource ([#3796](https://github.com/google/ExoPlayer/issues/3796)). -* Open source DownloadService, DownloadManager and related classes. +* Open source DownloadService, DownloadManager and related classes + ([#2643](https://github.com/google/ExoPlayer/issues/2643)). ### 2.6.1 ### From f432b52111710fed647762b0efd9d471d3bab1a1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 8 Feb 2018 01:57:46 -0800 Subject: [PATCH 021/376] Link libopus statically with libopusJNI We now build one .so file for the opus extension in the internal build, so make the external build work the same way. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=184962896 --- extensions/opus/src/main/jni/Android.mk | 4 ++-- extensions/opus/src/main/jni/libopus.mk | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk index 2ceb8fc4f7..9d1e4fe726 100644 --- a/extensions/opus/src/main/jni/Android.mk +++ b/extensions/opus/src/main/jni/Android.mk @@ -17,7 +17,7 @@ WORKING_DIR := $(call my-dir) include $(CLEAR_VARS) -# build libopus.so +# build libopus.a LOCAL_PATH := $(WORKING_DIR) include libopus.mk @@ -29,5 +29,5 @@ LOCAL_ARM_MODE := arm LOCAL_CPP_EXTENSION := .cc LOCAL_SRC_FILES := opus_jni.cc LOCAL_LDLIBS := -llog -lz -lm -LOCAL_SHARED_LIBRARIES := libopus +LOCAL_STATIC_LIBRARIES := libopus include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/opus/src/main/jni/libopus.mk b/extensions/opus/src/main/jni/libopus.mk index 0a5dd15b5a..672df600c0 100644 --- a/extensions/opus/src/main/jni/libopus.mk +++ b/extensions/opus/src/main/jni/libopus.mk @@ -47,4 +47,4 @@ endif LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include -include $(BUILD_SHARED_LIBRARY) +include $(BUILD_STATIC_LIBRARY) From 0e51a77839d6f0776365b8fc60644bd33fb0e6cc Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 8 Feb 2018 08:59:21 -0800 Subject: [PATCH 022/376] Add message parameter to DownloadNotificationUtil.createNotification DownloadNotificationUtil should not use getData(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185002149 --- .../ui/DownloadNotificationUtil.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index 0b7aa0d719..af09e1c128 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -20,7 +20,6 @@ import android.app.Notification.BigTextStyle; import android.app.Notification.Builder; import android.content.Context; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -40,8 +39,10 @@ public final class DownloadNotificationUtil { * @param smallIcon A small icon for the notifications. * @param channelId The id of the notification channel to use. Only required for API level 26 and * above. + * @param message An optional message to display on the notification. * @param errorMessageProvider An optional {@link ErrorMessageProvider} for translating download - * errors into readable error messages. + * errors into readable error messages. If not null and there is a download error then the + * error message is displayed instead of {@code message}. * @return A notification for the given {@link DownloadState}, or null if no notification should * be displayed. */ @@ -50,9 +51,10 @@ public final class DownloadNotificationUtil { Context context, int smallIcon, String channelId, + @Nullable String message, @Nullable ErrorMessageProvider errorMessageProvider) { - DownloadAction downloadAction = downloadState.downloadAction; - if (downloadAction.isRemoveAction() || downloadState.state == DownloadState.STATE_CANCELED) { + if (downloadState.downloadAction.isRemoveAction() + || downloadState.state == DownloadState.STATE_CANCELED) { return null; } @@ -72,17 +74,15 @@ public final class DownloadNotificationUtil { notificationBuilder.setProgress(100, indeterminate ? 0 : (int) percentage, indeterminate); } - String message; if (downloadState.error != null && errorMessageProvider != null) { message = errorMessageProvider.getErrorMessage(downloadState.error).second; - } else { - message = downloadAction.getData(); } - - if (Util.SDK_INT >= 16) { - notificationBuilder.setStyle(new BigTextStyle().bigText(message)); - } else { - notificationBuilder.setContentText(message); + if (message != null) { + if (Util.SDK_INT >= 16) { + notificationBuilder.setStyle(new BigTextStyle().bigText(message)); + } else { + notificationBuilder.setContentText(message); + } } return notificationBuilder.getNotification(); } From 34a318d348b6ff8184780017627539ba6e471dae Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Feb 2018 03:06:43 -0800 Subject: [PATCH 023/376] Check sys.display-size on Philips ATVs Device models are from https://support.google.com/googleplay/answer/1727131?hl=en. It looks like among these devices Build.MANUFACTURER can be set to either "PHILIPS" or "Philips", based on looking at internal bug reports. Issue: #3807 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185121182 --- RELEASENOTES.md | 2 ++ .../main/java/com/google/android/exoplayer2/util/Util.java | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bd863fb2f8..8e8b4c8d1f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -96,6 +96,8 @@ ([#3796](https://github.com/google/ExoPlayer/issues/3796)). * Open source DownloadService, DownloadManager and related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). +* Check `sys.display-size` on Philips ATVs + ([#3807](https://github.com/google/ExoPlayer/issues/3807)). ### 2.6.1 ### 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 f4637ebde6..cd643f2df4 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 @@ -1334,7 +1334,11 @@ public final class Util { if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA") && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { return new Point(3840, 2160); - } else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL.contains("SHIELD")) { + } else if (("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL.contains("SHIELD")) + || ("philips".equals(Util.toLowerInvariant(Util.MANUFACTURER)) + && (Util.MODEL.startsWith("QM1") + || Util.MODEL.equals("QV151E") + || Util.MODEL.equals("TPM171E")))) { // Attempt to read sys.display-size. String sysDisplaySize = null; try { From a9e11e4107b2fdd4ceb4bbe8cd150130758da054 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 9 Feb 2018 03:57:13 -0800 Subject: [PATCH 024/376] Extend support of 608 captions in SEI messages Issue:#3816 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185124378 --- .../android/exoplayer2/text/cea/CeaUtil.java | 92 +++++++++---------- 1 file changed, 42 insertions(+), 50 deletions(-) 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 ddb3804c3e..0022d37d6c 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,18 +19,19 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; -/** - * Utility methods for handling CEA-608/708 messages. - */ +/** 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"; private static final int PAYLOAD_TYPE_CC = 4; private static final int COUNTRY_CODE = 0xB5; - private static final int PROVIDER_CODE = 0x31; - private static final int USER_ID = 0x47413934; // "GA94" + private static final int PROVIDER_CODE_ATSC = 0x31; + private static final int PROVIDER_CODE_DIRECTV = 0x2F; + private static final int USER_ID_GA94 = Util.getIntegerCodeForString("GA94"); + private static final int USER_ID_DTG1 = Util.getIntegerCodeForString("DTG1"); private static final int USER_DATA_TYPE_CODE = 0x3; /** @@ -46,33 +47,49 @@ public final class CeaUtil { while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) { int payloadType = readNon255TerminatedValue(seiBuffer); int payloadSize = readNon255TerminatedValue(seiBuffer); + int nextPayloadPosition = seiBuffer.getPosition() + payloadSize; // Process the payload. if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { // This might occur if we're trying to read an encrypted SEI NAL unit. Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); seiBuffer.setPosition(seiBuffer.limit()); - } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) { - // Ignore country_code (1) + provider_code (2) + user_identifier (4) - // + user_data_type_code (1). - seiBuffer.skipBytes(8); - // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). - int ccCount = seiBuffer.readUnsignedByte() & 0x1F; - // Ignore em_data (1) - seiBuffer.skipBytes(1); - // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) - // + cc_data_1 (8) + cc_data_2 (8). - int sampleLength = ccCount * 3; - int sampleStartPosition = seiBuffer.getPosition(); - for (TrackOutput output : outputs) { - seiBuffer.setPosition(sampleStartPosition); - output.sampleData(seiBuffer, sampleLength); - output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) { + int countryCode = seiBuffer.readUnsignedByte(); + int providerCode = seiBuffer.readUnsignedShort(); + int userIdentifier = 0; + if (providerCode == PROVIDER_CODE_ATSC) { + userIdentifier = seiBuffer.readInt(); + } + int userDataTypeCode = seiBuffer.readUnsignedByte(); + if (providerCode == PROVIDER_CODE_DIRECTV) { + seiBuffer.skipBytes(1); // user_data_length. + } + boolean messageIsSupportedCeaCaption = + countryCode == COUNTRY_CODE + && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV) + && userDataTypeCode == USER_DATA_TYPE_CODE; + if (providerCode == PROVIDER_CODE_ATSC) { + messageIsSupportedCeaCaption &= + userIdentifier == USER_ID_GA94 || userIdentifier == USER_ID_DTG1; + } + if (messageIsSupportedCeaCaption) { + // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1). + int ccCount = seiBuffer.readUnsignedByte() & 0x1F; + // Ignore em_data (1) + seiBuffer.skipBytes(1); + // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2) + // + cc_data_1 (8) + cc_data_2 (8). + int sampleLength = ccCount * 3; + int sampleStartPosition = seiBuffer.getPosition(); + for (TrackOutput output : outputs) { + seiBuffer.setPosition(sampleStartPosition); + output.sampleData(seiBuffer, sampleLength); + output.sampleMetadata( + presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null); + } } - // Ignore trailing information in SEI, if any. - seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3)); - } else { - seiBuffer.skipBytes(payloadSize); } + seiBuffer.setPosition(nextPayloadPosition); } } @@ -97,31 +114,6 @@ public final class CeaUtil { return value; } - /** - * Inspects an sei message to determine whether it contains CEA-608. - *

- * The position of {@code payload} is left unchanged. - * - * @param payloadType The payload type of the message. - * @param payloadLength The length of the payload. - * @param payload A {@link ParsableByteArray} containing the payload. - * @return Whether the sei message contains CEA-608. - */ - private static boolean isSeiMessageCea608(int payloadType, int payloadLength, - ParsableByteArray payload) { - if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) { - return false; - } - int startPosition = payload.getPosition(); - int countryCode = payload.readUnsignedByte(); - int providerCode = payload.readUnsignedShort(); - int userIdentifier = payload.readInt(); - int userDataTypeCode = payload.readUnsignedByte(); - payload.setPosition(startPosition); - return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE - && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE; - } - private CeaUtil() {} } From 4916baabf039d6f9cbcc901aebdc02cfb4ea56be Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 9 Feb 2018 05:21:52 -0800 Subject: [PATCH 025/376] Fix bug in media period queue update at dynamic timeline changes. If the period uid doesn't match, the update procedure currently doesn't remove the correct periods. This may cause the player to get stuck or to play the wrong periods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185129503 --- .../android/exoplayer2/MediaPeriodQueue.java | 15 ++++--- .../DynamicConcatenatingMediaSource.java | 4 +- .../android/exoplayer2/ExoPlayerTest.java | 45 +++++++++++++++++++ .../exoplayer2/testutil/FakeMediaSource.java | 6 +++ 4 files changed, 61 insertions(+), 9 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 4b6ef1807e..208a235777 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 @@ -291,7 +291,8 @@ import com.google.android.exoplayer2.util.Assertions; /** * Updates media periods in the queue to take into account the latest timeline, and returns * whether the timeline change has been fully handled. If not, it is necessary to seek to the - * current playback position. + * current playback position. The method assumes that the first media period in the queue is still + * consistent with the new timeline. * * @param playingPeriodId The current playing media period identifier. * @param rendererPositionUs The current renderer position in microseconds. @@ -311,6 +312,11 @@ import com.google.android.exoplayer2.util.Assertions; periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex); } else { // Check this period holder still follows the previous one, based on the new timeline. + if (periodIndex == C.INDEX_UNSET + || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { + // The holder uid is inconsistent with the new timeline. + return !removeAfter(previousPeriodHolder); + } MediaPeriodInfo periodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); if (periodInfo == null) { @@ -326,15 +332,10 @@ import com.google.android.exoplayer2.util.Assertions; } if (periodHolder.info.isLastInTimelinePeriod) { - // Move on to the next timeline period, if there is one. + // Move on to the next timeline period index, if there is one. periodIndex = timeline.getNextPeriodIndex( periodIndex, period, window, repeatMode, shuffleModeEnabled); - if (periodIndex == C.INDEX_UNSET - || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { - // The holder is inconsistent with the new timeline. - return previousPeriodHolder == null || !removeAfter(previousPeriodHolder); - } } previousPeriodHolder = periodHolder; 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 index 4deb97879c..f52c1bfd0f 100644 --- 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 @@ -780,7 +780,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { timeline.getPeriod(periodIndex, period, setIds); - if (period.uid == replacedId) { + if (Util.areEqual(period.uid, replacedId)) { period.uid = DUMMY_ID; } return period; @@ -788,7 +788,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Override public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedId : uid); + return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid); } } 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 f10e889390..ec5a8ccfce 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -1765,6 +1766,50 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); } + @Test + public void testTimelineUpdateDropsPrebufferedPeriods() throws Exception { + Timeline timeline1 = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2)); + final Timeline timeline2 = + 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); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testTimelineUpdateDropsPeriods") + .pause() + .waitForPlaybackState(Player.STATE_READY) + // Ensure next period is pre-buffered by playing until end of first period. + .playUntilPosition( + /* windowIndex= */ 0, + /* positionMs= */ C.usToMs(TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US)) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline2, /* newManifest= */ null); + } + }) + .waitForTimelineChanged(timeline2) + .play() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1); + // Assert that the second period was re-created from the new timeline. + assertThat(mediaSource.getCreatedMediaPeriods()) + .containsExactly(new MediaPeriodId(0), new MediaPeriodId(1), new MediaPeriodId(1)) + .inOrder(); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { 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 17613ce519..da81bbb62c 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 @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; +import java.util.List; /** * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a @@ -160,6 +161,11 @@ public class FakeMediaSource implements MediaSource { assertThat(createdMediaPeriods).contains(mediaPeriodId); } + /** Returns a list of {@link MediaPeriodId}s, with one element for each created media period. */ + public List getCreatedMediaPeriods() { + return createdMediaPeriods; + } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { return new FakeMediaPeriod(trackGroupArray); From 2c2aaf0a4bcb18d67da074aaa2401e30eb0c8d27 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 9 Feb 2018 06:36:19 -0800 Subject: [PATCH 026/376] Make SegmentDownloadAction constructor keys parameter simpler Removed option to pass null keys parameter. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185134822 --- .../offline/SegmentDownloadAction.java | 41 ++++++++----------- .../exoplayer2/offline/SegmentDownloader.java | 4 +- .../dash/offline/DashDownloaderTest.java | 5 --- .../source/hls/offline/HlsDownloaderTest.java | 5 --- 4 files changed, 19 insertions(+), 36 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java index b77ac5bad8..6d689d53b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloadAction.java @@ -40,16 +40,11 @@ public abstract class SegmentDownloadAction extends DownloadAction { public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { Uri manifestUri = Uri.parse(input.readUTF()); String data = input.readUTF(); + boolean removeAction = input.readBoolean(); int keyCount = input.readInt(); - boolean removeAction = keyCount == -1; - K[] keys; - if (removeAction) { - keys = null; - } else { - keys = createKeyArray(keyCount); - for (int i = 0; i < keyCount; i++) { - keys[i] = readKey(input); - } + K[] keys = createKeyArray(keyCount); + for (int i = 0; i < keyCount; i++) { + keys[i] = readKey(input); } return createDownloadAction(manifestUri, removeAction, data, keys); } @@ -74,12 +69,15 @@ public abstract class SegmentDownloadAction extends DownloadAction { * @param manifestUri The {@link Uri} of the manifest to be downloaded. * @param removeAction Whether the data will be removed. If {@code false} it will be downloaded. * @param data Optional custom data for this action. If null, an empty string is used. - * @param keys Keys of representations to be downloaded. If empty or null, all representations are - * downloaded. If {@code removeAction} is true, this is ignored. + * @param keys Keys of representations to be downloaded. If empty, all representations are + * downloaded. If {@code removeAction} is true, {@code keys} should be an empty array. */ protected SegmentDownloadAction(Uri manifestUri, boolean removeAction, String data, K[] keys) { super(data); - Assertions.checkArgument(!removeAction || keys == null || keys.length == 0); + Assertions.checkNotNull(keys); + if (removeAction) { + Assertions.checkArgument(keys.length == 0); + } this.manifestUri = manifestUri; this.keys = keys; this.removeAction = removeAction; @@ -94,13 +92,10 @@ public abstract class SegmentDownloadAction extends DownloadAction { public final void writeToStream(DataOutputStream output) throws IOException { output.writeUTF(manifestUri.toString()); output.writeUTF(getData()); - if (isRemoveAction()) { - output.writeInt(-1); - } else { - output.writeInt(keys.length); - for (K key : keys) { - writeKey(output, key); - } + output.writeBoolean(removeAction); + output.writeInt(keys.length); + for (K key : keys) { + writeKey(output, key); } } @@ -124,11 +119,9 @@ public abstract class SegmentDownloadAction extends DownloadAction { } SegmentDownloadAction that = (SegmentDownloadAction) o; return manifestUri.equals(that.manifestUri) - && (keys == null || keys.length == 0 - ? (that.keys == null || that.keys.length == 0) - : (that.keys != null - && that.keys.length == keys.length - && Arrays.asList(keys).containsAll(Arrays.asList(that.keys)))); + && removeAction == that.removeAction + && keys.length == that.keys.length + && Arrays.asList(keys).containsAll(Arrays.asList(that.keys)); } @Override 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 6abb950254..63414dc39e 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 @@ -101,10 +101,10 @@ public abstract class SegmentDownloader implements Downloader { /** * Selects multiple representations pointed to by the keys for downloading, checking status. Any - * previous selection is cleared. If keys are null or empty, all representations are downloaded. + * previous selection is cleared. If keys array is empty, all representations are downloaded. */ public final void selectRepresentations(K[] keys) { - this.keys = (keys != null && keys.length > 0) ? keys.clone() : null; + this.keys = keys.length > 0 ? keys.clone() : null; resetCounters(); } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 5e7ce43ebf..868166673a 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -184,11 +184,6 @@ public class DashDownloaderTest extends InstrumentationTestCase { // select something random dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); // clear selection - dashDownloader.selectRepresentations(null); - dashDownloader.download(null); - assertCachedData(cache, fakeDataSet); - dashDownloader.remove(); - dashDownloader.selectRepresentations(new RepresentationKey[0]); dashDownloader.download(null); assertCachedData(cache, fakeDataSet); diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index c068a8182b..13083dc01c 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -156,11 +156,6 @@ public class HlsDownloaderTest extends InstrumentationTestCase { // select something random hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); // clear selection - hlsDownloader.selectRepresentations(null); - hlsDownloader.download(null); - assertCachedData(cache, fakeDataSet); - hlsDownloader.remove(); - hlsDownloader.selectRepresentations(new String[0]); hlsDownloader.download(null); assertCachedData(cache, fakeDataSet); From ac2f6cbf98af801280e641767b4d251a16721ae7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Feb 2018 07:12:51 -0800 Subject: [PATCH 027/376] Move two further methods from Timeline to AdPlaybackState ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185138158 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 19 +------- .../google/android/exoplayer2/Timeline.java | 37 +++------------- .../source/ads/AdPlaybackState.java | 43 +++++++++++++++++++ 3 files changed, 51 insertions(+), 48 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 4f20535ff1..a8573f31e3 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 @@ -797,7 +797,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); int adGroupIndexForPosition = - getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(pendingContentPositionMs)); if (adGroupIndexForPosition == 0) { podIndexOffset = 0; } else if (adGroupIndexForPosition == C.INDEX_UNSET) { @@ -952,23 +952,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - /** - * Returns the index of the ad group that should be played before playing the content at {@code - * playbackPositionUs} when starting playback for the first time. This is the latest ad group at - * or before the specified playback position. If the first ad is after the playback position, - * returns {@link C#INDEX_UNSET}. - */ - private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) { - for (int i = 0; i < adGroupTimesUs.length; i++) { - long adGroupTimeUs = adGroupTimesUs[i]; - // A postroll ad is after any position in the content. - if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) { - return i == 0 ? C.INDEX_UNSET : (i - 1); - } - } - return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); - } - /** * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all * ads in the ad group have loaded. 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 4b58a1075c..50a3e66880 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 @@ -413,53 +413,30 @@ public abstract class Timeline { * @return Whether the ad group at index {@code adGroupIndex} has been played. */ public boolean hasPlayedAdGroup(int adGroupIndex) { - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - return adGroup.getFirstAdIndexToPlay() == adGroup.count; + return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds(); } /** * Returns the index of the ad group at or before {@code positionUs}, if that ad group is - * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been - * played, or if there is no such ad group. + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has + * no ads remaining to be played, or if there is no such ad group. * * @param positionUs The position at or before which to find an ad group, in microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexForPositionUs(long positionUs) { - long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; - if (adGroupTimesUs == null) { - return C.INDEX_UNSET; - } - // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. - // In practice we expect there to be few ad groups so the search shouldn't be expensive. - int index = adGroupTimesUs.length - 1; - while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE - || adGroupTimesUs[index] > positionUs)) { - index--; - } - return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET; + return adPlaybackState.getAdGroupIndexForPositionUs(positionUs); } /** - * Returns the index of the next unplayed ad group after {@code positionUs}. Returns - * {@link C#INDEX_UNSET} if there is no such ad group. + * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be + * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. * * @param positionUs The position after which to find an ad group, in microseconds. * @return The index of the ad group, or {@link C#INDEX_UNSET}. */ public int getAdGroupIndexAfterPositionUs(long positionUs) { - long[] adGroupTimesUs = adPlaybackState.adGroupTimesUs; - if (adGroupTimesUs == null) { - return C.INDEX_UNSET; - } - // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. - // In practice we expect there to be few ad groups so the search shouldn't be expensive. - int index = 0; - while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE - && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) { - index++; - } - return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs); } /** 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 2f6f81d6b9..8654e94bdb 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 @@ -91,6 +91,11 @@ public final class AdPlaybackState { return nextAdIndexToPlay; } + /** Returns whether the ad group has at least one ad that still needs to be played. */ + public boolean hasUnplayedAds() { + return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count; + } + /** * Returns a new instance with the ad count set to {@code count}. This method may only be called * if this instance's ad count has not yet been specified. @@ -270,6 +275,44 @@ public final class AdPlaybackState { this.contentDurationUs = contentDurationUs; } + /** + * Returns the index of the ad group at or before {@code positionUs}, if that ad group is + * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no + * ads remaining to be played, or if there is no such ad group. + * + * @param positionUs The position at or before which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexForPositionUs(long positionUs) { + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = adGroupTimesUs.length - 1; + while (index >= 0 + && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE || adGroupTimesUs[index] > positionUs)) { + index--; + } + return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET; + } + + /** + * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be + * played. Returns {@link C#INDEX_UNSET} if there is no such ad group. + * + * @param positionUs The position after which to find an ad group, in microseconds. + * @return The index of the ad group, or {@link C#INDEX_UNSET}. + */ + public int getAdGroupIndexAfterPositionUs(long positionUs) { + // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE. + // In practice we expect there to be few ad groups so the search shouldn't be expensive. + int index = 0; + while (index < adGroupTimesUs.length + && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE + && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) { + index++; + } + return index < adGroupTimesUs.length ? index : C.INDEX_UNSET; + } + /** * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}. * The ad count must be greater than zero. From b6a46fd3eeedf82300e0527abe30e08ffe22187a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 9 Feb 2018 07:24:45 -0800 Subject: [PATCH 028/376] Set the expected ad group based on the content position ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185139106 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 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 a8573f31e3..d5e120afe7 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 @@ -558,22 +558,27 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public VideoProgressUpdate getContentProgress() { - boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; - long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; if (player == null) { return lastContentProgress; - } else if (pendingContentPositionMs != C.TIME_UNSET) { + } + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentPositionMs; + if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; - return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs); + contentPositionMs = pendingContentPositionMs; } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; - long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; - return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; + contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; + } else if (imaAdState == IMA_AD_STATE_NONE && hasContentDuration) { + contentPositionMs = player.getCurrentPosition(); } else { - return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } + // Keep track of the ad group index that IMA will load for the current content position. + expectedAdGroupIndex = + adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; + return new VideoProgressUpdate(contentPositionMs, contentDurationMs); } // VideoAdPlayer implementation. @@ -607,11 +612,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); adPlaybackState = adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); - if (getAdIndexInAdGroupToLoad(adGroupIndex) == C.INDEX_UNSET) { - // Keep track of the expected ad group index to use as a fallback if the LOADED event is - // unexpectedly not triggered. - expectedAdGroupIndex++; - } updateAdPlaybackState(); } @@ -774,7 +774,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A timeline.getPeriod(0, period); int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)); if (newAdGroupIndex != C.INDEX_UNSET) { - expectedAdGroupIndex = newAdGroupIndex; sentPendingContentPositionMs = false; pendingContentPositionMs = positionMs; } @@ -819,7 +818,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // We're removing one or more ads, which means that the earliest ad (if any) will be a // midroll/postroll. Midroll pod indices start at 1. podIndexOffset = adGroupIndexForPosition - 1; - expectedAdGroupIndex = adGroupIndexForPosition; } // Start ad playback. @@ -910,9 +908,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; // Ad group load error can be notified more than once, so check if it was already handled. - // TODO: Update the expected ad group index based on the position returned by - // getContentProgress so that it's possible to detect when more than one ad group fails to load - // consecutively. if (adGroup.count == C.LENGTH_UNSET || adGroup.states[0] == AdPlaybackState.AD_STATE_UNAVAILABLE) { if (DEBUG) { From fc7a6d259657121ad6893a6beaab28e64b67504b Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 12 Feb 2018 04:08:59 -0800 Subject: [PATCH 029/376] Increase character limit for download notification strings This is requested for Romanian translation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185359071 --- library/ui/src/main/res/values/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index d92c615ad0..c5967a260a 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -38,12 +38,12 @@ Shuffle Fullscreen mode - + Download queued - + Downloading - + Download completed - + Download failed From c14ef7503716b6b52b1af45116a4f0d59cd7fb34 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 12 Feb 2018 05:41:57 -0800 Subject: [PATCH 030/376] Move (almost all) remaining core library instrumentation tests to Robolectric. There are 4 tests which can't currently be moved: - DownloadManagerTest explicitly uses the main looper which isn't easily supported because the test runs on this thread. - ContentDataSourceTest uses an AssetFileDescriptor which wraps a ParcelFileDescriptor. It seems Robolectric doesn't correctly forward the inner wrapped file descriptor leading to NPE. - CacheContentIndexTest and SimpleCacheSpanTest both work fine with Gradle but fail with seemingly valid test failures on Blaze. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185366678 --- .../ext/flac/FlacExtractorTest.java | 15 +- .../src/androidTest/assets/ssa/typical_format | 1 - library/core/src/test/AndroidManifest.xml | 23 ++ .../assets/binary/1024_incrementing_bytes.mp3 | Bin 0 -> 1024 bytes .../assets/flv/sample.flv | Bin .../assets/flv/sample.flv.0.dump | 0 .../assets/mkv/sample.mkv | Bin .../assets/mkv/sample.mkv.0.dump | 0 .../assets/mkv/sample.mkv.1.dump | 0 .../assets/mkv/sample.mkv.2.dump | 0 .../assets/mkv/sample.mkv.3.dump | 0 .../mkv/subsample_encrypted_altref.webm | Bin .../subsample_encrypted_altref.webm.0.dump | 0 .../mkv/subsample_encrypted_noaltref.webm | Bin .../subsample_encrypted_noaltref.webm.0.dump | 0 .../{androidTest => test}/assets/mp3/bear.mp3 | Bin .../assets/mp3/bear.mp3.0.dump | 0 .../assets/mp3/bear.mp3.1.dump | 0 .../assets/mp3/bear.mp3.2.dump | 0 .../assets/mp3/bear.mp3.3.dump | 0 .../assets/mp3/play-trimmed.mp3 | Bin .../assets/mp3/play-trimmed.mp3.0.dump | 0 .../assets/mp3/play-trimmed.mp3.1.dump | 0 .../assets/mp3/play-trimmed.mp3.2.dump | 0 .../assets/mp3/play-trimmed.mp3.3.dump | 0 .../assets/mp3/play-trimmed.mp3.unklen.dump | 0 .../assets/mp4/sample.mp4 | Bin .../assets/mp4/sample.mp4.0.dump | 0 .../assets/mp4/sample.mp4.1.dump | 0 .../assets/mp4/sample.mp4.2.dump | 0 .../assets/mp4/sample.mp4.3.dump | 0 .../assets/mp4/sample_fragmented.mp4 | Bin .../assets/mp4/sample_fragmented.mp4.0.dump | 0 .../assets/mp4/sample_fragmented_sei.mp4 | Bin .../mp4/sample_fragmented_sei.mp4.0.dump | 0 .../assets/ogg/bear.opus | Bin .../assets/ogg/bear.opus.0.dump | 0 .../assets/ogg/bear.opus.1.dump | 0 .../assets/ogg/bear.opus.2.dump | 0 .../assets/ogg/bear.opus.3.dump | 0 .../assets/ogg/bear.opus.unklen.dump | 0 .../assets/ogg/bear_flac.ogg | Bin .../assets/ogg/bear_flac.ogg.0.dump | 0 .../assets/ogg/bear_flac.ogg.1.dump | 0 .../assets/ogg/bear_flac.ogg.2.dump | 0 .../assets/ogg/bear_flac.ogg.3.dump | 0 .../assets/ogg/bear_flac.ogg.unklen.dump | 0 .../assets/ogg/bear_flac_noseektable.ogg | Bin .../ogg/bear_flac_noseektable.ogg.0.dump | 0 .../ogg/bear_flac_noseektable.ogg.1.dump | 0 .../ogg/bear_flac_noseektable.ogg.2.dump | 0 .../ogg/bear_flac_noseektable.ogg.3.dump | 0 .../ogg/bear_flac_noseektable.ogg.unklen.dump | 0 .../assets/ogg/bear_vorbis.ogg | Bin .../assets/ogg/bear_vorbis.ogg.0.dump | 0 .../assets/ogg/bear_vorbis.ogg.1.dump | 0 .../assets/ogg/bear_vorbis.ogg.2.dump | 0 .../assets/ogg/bear_vorbis.ogg.3.dump | 0 .../assets/ogg/bear_vorbis.ogg.unklen.dump | 0 .../assets/rawcc/sample.rawcc | Bin .../assets/rawcc/sample.rawcc.0.dump | 0 .../{androidTest => test}/assets/ssa/empty | 0 .../assets/ssa/invalid_timecodes | 2 +- .../assets/ssa/no_end_timecodes | 2 +- .../{androidTest => test}/assets/ssa/typical | 2 +- .../assets/ssa/typical_dialogue | 2 +- .../core/src/test/assets/ssa/typical_format | 1 + .../assets/ssa/typical_header | 2 +- .../{androidTest => test}/assets/subrip/empty | 0 .../assets/subrip/no_end_timecodes | 2 +- .../assets/subrip/typical | 2 +- .../assets/subrip/typical_extra_blank_line | 2 +- .../assets/subrip/typical_missing_sequence | 2 +- .../assets/subrip/typical_missing_timecode | 2 +- .../assets/subrip/typical_negative_timestamps | 0 .../assets/subrip/typical_unexpected_end | 2 +- .../subrip/typical_with_byte_order_mark | 2 +- .../assets/ts/sample.ac3 | Bin .../assets/ts/sample.ac3.0.dump | 0 .../assets/ts/sample.adts | Bin .../assets/ts/sample.adts.0.dump | 0 .../{androidTest => test}/assets/ts/sample.ps | Bin .../assets/ts/sample.ps.0.dump | 0 .../{androidTest => test}/assets/ts/sample.ts | Bin .../assets/ts/sample.ts.0.dump | 0 .../assets/ts/sample_with_sdt.ts | Bin .../assets/ttml/chain_multiple_styles.xml | 0 .../assets/ttml/font_size.xml | 0 .../assets/ttml/font_size_empty.xml | 0 .../assets/ttml/font_size_invalid.xml | 0 .../assets/ttml/font_size_no_unit.xml | 0 .../assets/ttml/frame_rate.xml | 0 .../ttml/inherit_and_override_style.xml | 0 .../assets/ttml/inherit_global_and_parent.xml | 0 .../assets/ttml/inherit_multiple_styles.xml | 0 .../assets/ttml/inherit_style.xml | 0 .../assets/ttml/inline_style_attributes.xml | 0 .../assets/ttml/multiple_regions.xml | 0 .../assets/ttml/no_underline_linethrough.xml | 0 .../assets/tx3g/initialization | Bin .../assets/tx3g/initialization_all_defaults | Bin .../assets/tx3g/no_subtitle | Bin .../assets/tx3g/sample_just_text | Bin .../assets/tx3g/sample_utf16_be_no_styl | Bin .../assets/tx3g/sample_utf16_le_no_styl | Bin .../assets/tx3g/sample_with_multiple_styl | Bin .../assets/tx3g/sample_with_other_extension | Bin .../assets/tx3g/sample_with_styl | Bin .../assets/tx3g/sample_with_styl_all_defaults | Bin .../assets/tx3g/sample_with_tbox | Bin .../assets/wav/sample.wav | Bin .../assets/wav/sample.wav.0.dump | 0 .../assets/wav/sample.wav.1.dump | 0 .../assets/wav/sample.wav.2.dump | 0 .../assets/wav/sample.wav.3.dump | 0 .../assets/webm/vorbis_codec_private | Bin .../{androidTest => test}/assets/webvtt/empty | 0 .../assets/webvtt/typical | 0 .../assets/webvtt/typical_with_bad_timestamps | 0 .../assets/webvtt/typical_with_comments | 0 .../assets/webvtt/typical_with_identifiers | 0 .../assets/webvtt/with_bad_cue_header | 0 .../assets/webvtt/with_css_complex_selectors | 0 .../assets/webvtt/with_css_styles | 0 .../assets/webvtt/with_positioning | 0 .../assets/webvtt/with_tags | 0 .../com/google/android/exoplayer2/CTest.java | 2 - .../exoplayer2/DefaultMediaClockTest.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 6 +- .../google/android/exoplayer2/FormatTest.java | 2 - .../android/exoplayer2/TimelineTest.java | 22 +- .../audio/SimpleDecoderAudioRendererTest.java | 1 - .../audio/SonicAudioProcessorTest.java | 2 - .../exoplayer2/drm/ClearKeyUtilTest.java | 9 +- .../exoplayer2/drm/DrmInitDataTest.java | 2 - .../drm/OfflineLicenseHelperTest.java | 66 +++-- .../extractor/DefaultExtractorInputTest.java | 2 - .../exoplayer2/extractor/ExtractorTest.java | 2 - .../extractor/flv/FlvExtractorTest.java | 27 +- .../extractor/mkv/DefaultEbmlReaderTest.java | 2 - .../extractor/mkv/MatroskaExtractorTest.java | 57 +++-- .../extractor/mkv/VarintReaderTest.java | 2 - .../extractor/mp3/Mp3ExtractorTest.java | 42 +-- .../extractor/mp3/XingSeekerTest.java | 2 - .../extractor/mp4/AtomParsersTest.java | 2 - .../mp4/FragmentedMp4ExtractorTest.java | 27 +- .../extractor/mp4/Mp4ExtractorTest.java | 27 +- .../extractor/mp4/PsshAtomUtilTest.java | 2 - .../extractor/ogg/DefaultOggSeekerTest.java | 40 ++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 2 - .../extractor/ogg/OggExtractorTest.java | 56 ++-- .../extractor/ogg/OggPacketTest.java | 142 ++++++----- .../extractor/ogg/OggPageHeaderTest.java | 2 - .../exoplayer2/extractor/ogg/OggTestFile.java | 21 +- .../extractor/ogg/VorbisBitArrayTest.java | 2 - .../extractor/ogg/VorbisReaderTest.java | 2 - .../extractor/ogg/VorbisUtilTest.java | 2 - .../extractor/rawcc/RawCcExtractorTest.java | 27 +- .../extractor/ts/Ac3ExtractorTest.java | 27 +- .../extractor/ts/AdtsExtractorTest.java | 27 +- .../extractor/ts/AdtsReaderTest.java | 88 ++++--- .../extractor/ts/PsExtractorTest.java | 27 +- .../extractor/ts/SectionReaderTest.java | 2 - .../extractor/ts/TsExtractorTest.java | 102 ++++---- .../extractor/wav/WavExtractorTest.java | 27 +- .../emsg/EventMessageDecoderTest.java | 2 - .../emsg/EventMessageEncoderTest.java | 2 - .../metadata/emsg/EventMessageTest.java | 2 - .../metadata/id3/ChapterFrameTest.java | 2 - .../metadata/id3/ChapterTocFrameTest.java | 2 - .../metadata/id3/Id3DecoderTest.java | 2 - .../scte35/SpliceInfoDecoderTest.java | 2 - .../exoplayer2/offline/ActionFileTest.java | 2 - .../ProgressiveDownloadActionTest.java | 2 - .../source/ClippingMediaSourceTest.java | 69 +++-- .../CompositeSequenceableLoaderTest.java | 2 - .../source/ConcatenatingMediaSourceTest.java | 161 ++++++------ .../DynamicConcatenatingMediaSourceTest.java | 241 ++++++++++-------- .../source/LoopingMediaSourceTest.java | 85 +++--- .../source/MergingMediaSourceTest.java | 33 ++- .../exoplayer2/source/SampleQueueTest.java | 2 - .../exoplayer2/source/ShuffleOrderTest.java | 2 - .../source/SinglePeriodTimelineTest.java | 2 - .../source/ads/AdPlaybackStateTest.java | 2 - .../exoplayer2/text/ssa/SsaDecoderTest.java | 32 ++- .../text/subrip/SubripDecoderTest.java | 42 +-- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 224 ++++++++++++---- .../text/ttml/TtmlRenderUtilTest.java | 2 - .../exoplayer2/text/ttml/TtmlStyleTest.java | 2 - .../exoplayer2/text/tx3g/Tx3gDecoderTest.java | 64 +++-- .../exoplayer2/text/webvtt/CssParserTest.java | 2 - .../text/webvtt/Mp4WebvttDecoderTest.java | 2 - .../text/webvtt/WebvttCueParserTest.java | 2 - .../text/webvtt/WebvttDecoderTest.java | 162 +++++++++--- .../text/webvtt/WebvttSubtitleTest.java | 2 - .../AdaptiveTrackSelectionTest.java | 2 - .../DefaultTrackSelectorTest.java | 2 - .../MappingTrackSelectorTest.java | 2 - .../upstream/AssetDataSourceTest.java | 33 ++- .../upstream/ByteArrayDataSourceTest.java | 2 - .../upstream/DataSchemeDataSourceTest.java | 2 - .../upstream/DataSourceInputStreamTest.java | 2 - .../upstream/cache/CacheDataSourceTest.java | 2 - .../upstream/cache/CacheDataSourceTest2.java | 2 - .../upstream/cache/CacheUtilTest.java | 2 - .../cache/CachedRegionTrackerTest.java | 96 ++++--- .../LeastRecentlyUsedCacheEvictorTest.java | 2 - .../upstream/cache/SimpleCacheTest.java | 2 - .../crypto/AesFlushingCipherTest.java | 2 - .../exoplayer2/util/AtomicFileTest.java | 2 - .../exoplayer2/util/ColorParserTest.java | 2 - .../exoplayer2/util/NalUnitUtilTest.java | 2 - .../exoplayer2/util/ParsableBitArrayTest.java | 2 - .../util/ParsableByteArrayTest.java | 2 - .../util/ParsableNalUnitBitArrayTest.java | 2 - .../ReusableBufferedOutputStreamTest.java | 2 - .../android/exoplayer2/util/UriUtilTest.java | 2 - .../android/exoplayer2/util/UtilTest.java | 2 - .../exoplayer2/testutil/ExtractorAsserts.java | 158 ++++++++---- .../testutil/FakeExtractorOutput.java | 14 +- .../testutil/MediaSourceTestRunner.java | 62 +++-- .../android/exoplayer2/testutil/TestUtil.java | 5 +- 222 files changed, 1488 insertions(+), 1044 deletions(-) delete mode 100644 library/core/src/androidTest/assets/ssa/typical_format create mode 100644 library/core/src/test/AndroidManifest.xml create mode 100644 library/core/src/test/assets/binary/1024_incrementing_bytes.mp3 rename library/core/src/{androidTest => test}/assets/flv/sample.flv (100%) rename library/core/src/{androidTest => test}/assets/flv/sample.flv.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/sample.mkv (100%) rename library/core/src/{androidTest => test}/assets/mkv/sample.mkv.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/sample.mkv.1.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/sample.mkv.2.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/sample.mkv.3.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/subsample_encrypted_altref.webm (100%) rename library/core/src/{androidTest => test}/assets/mkv/subsample_encrypted_altref.webm.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mkv/subsample_encrypted_noaltref.webm (100%) rename library/core/src/{androidTest => test}/assets/mkv/subsample_encrypted_noaltref.webm.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/bear.mp3 (100%) rename library/core/src/{androidTest => test}/assets/mp3/bear.mp3.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/bear.mp3.1.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/bear.mp3.2.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/bear.mp3.3.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3 (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3.1.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3.2.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3.3.dump (100%) rename library/core/src/{androidTest => test}/assets/mp3/play-trimmed.mp3.unklen.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample.mp4 (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample.mp4.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample.mp4.1.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample.mp4.2.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample.mp4.3.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample_fragmented.mp4 (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample_fragmented.mp4.0.dump (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample_fragmented_sei.mp4 (100%) rename library/core/src/{androidTest => test}/assets/mp4/sample_fragmented_sei.mp4.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus.1.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus.2.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus.3.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear.opus.unklen.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg.1.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg.2.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg.3.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac.ogg.unklen.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg.1.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg.2.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg.3.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_flac_noseektable.ogg.unklen.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg.1.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg.2.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg.3.dump (100%) rename library/core/src/{androidTest => test}/assets/ogg/bear_vorbis.ogg.unklen.dump (100%) rename library/core/src/{androidTest => test}/assets/rawcc/sample.rawcc (100%) rename library/core/src/{androidTest => test}/assets/rawcc/sample.rawcc.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ssa/empty (100%) rename library/core/src/{androidTest => test}/assets/ssa/invalid_timecodes (96%) rename library/core/src/{androidTest => test}/assets/ssa/no_end_timecodes (96%) rename library/core/src/{androidTest => test}/assets/ssa/typical (96%) rename library/core/src/{androidTest => test}/assets/ssa/typical_dialogue (91%) create mode 100644 library/core/src/test/assets/ssa/typical_format rename library/core/src/{androidTest => test}/assets/ssa/typical_header (85%) rename library/core/src/{androidTest => test}/assets/subrip/empty (100%) rename library/core/src/{androidTest => test}/assets/subrip/no_end_timecodes (87%) rename library/core/src/{androidTest => test}/assets/subrip/typical (87%) rename library/core/src/{androidTest => test}/assets/subrip/typical_extra_blank_line (87%) rename library/core/src/{androidTest => test}/assets/subrip/typical_missing_sequence (86%) rename library/core/src/{androidTest => test}/assets/subrip/typical_missing_timecode (85%) rename library/core/src/{androidTest => test}/assets/subrip/typical_negative_timestamps (100%) rename library/core/src/{androidTest => test}/assets/subrip/typical_unexpected_end (98%) rename library/core/src/{androidTest => test}/assets/subrip/typical_with_byte_order_mark (87%) rename library/core/src/{androidTest => test}/assets/ts/sample.ac3 (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.ac3.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.adts (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.adts.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.ps (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.ps.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.ts (100%) rename library/core/src/{androidTest => test}/assets/ts/sample.ts.0.dump (100%) rename library/core/src/{androidTest => test}/assets/ts/sample_with_sdt.ts (100%) rename library/core/src/{androidTest => test}/assets/ttml/chain_multiple_styles.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/font_size.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/font_size_empty.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/font_size_invalid.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/font_size_no_unit.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/frame_rate.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/inherit_and_override_style.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/inherit_global_and_parent.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/inherit_multiple_styles.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/inherit_style.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/inline_style_attributes.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/multiple_regions.xml (100%) rename library/core/src/{androidTest => test}/assets/ttml/no_underline_linethrough.xml (100%) rename library/core/src/{androidTest => test}/assets/tx3g/initialization (100%) rename library/core/src/{androidTest => test}/assets/tx3g/initialization_all_defaults (100%) rename library/core/src/{androidTest => test}/assets/tx3g/no_subtitle (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_just_text (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_utf16_be_no_styl (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_utf16_le_no_styl (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_with_multiple_styl (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_with_other_extension (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_with_styl (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_with_styl_all_defaults (100%) rename library/core/src/{androidTest => test}/assets/tx3g/sample_with_tbox (100%) rename library/core/src/{androidTest => test}/assets/wav/sample.wav (100%) rename library/core/src/{androidTest => test}/assets/wav/sample.wav.0.dump (100%) rename library/core/src/{androidTest => test}/assets/wav/sample.wav.1.dump (100%) rename library/core/src/{androidTest => test}/assets/wav/sample.wav.2.dump (100%) rename library/core/src/{androidTest => test}/assets/wav/sample.wav.3.dump (100%) rename library/core/src/{androidTest => test}/assets/webm/vorbis_codec_private (100%) rename library/core/src/{androidTest => test}/assets/webvtt/empty (100%) rename library/core/src/{androidTest => test}/assets/webvtt/typical (100%) rename library/core/src/{androidTest => test}/assets/webvtt/typical_with_bad_timestamps (100%) rename library/core/src/{androidTest => test}/assets/webvtt/typical_with_comments (100%) rename library/core/src/{androidTest => test}/assets/webvtt/typical_with_identifiers (100%) rename library/core/src/{androidTest => test}/assets/webvtt/with_bad_cue_header (100%) rename library/core/src/{androidTest => test}/assets/webvtt/with_css_complex_selectors (100%) rename library/core/src/{androidTest => test}/assets/webvtt/with_css_styles (100%) rename library/core/src/{androidTest => test}/assets/webvtt/with_positioning (100%) rename library/core/src/{androidTest => test}/assets/webvtt/with_tags (100%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/TimelineTest.java (83%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java (74%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java (53%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java (59%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java (71%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java (69%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java (84%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java (79%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java (68%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java (91%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java (69%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java (69%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java (79%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java (77%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java (70%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java (86%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java (61%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java (76%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java (83%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java (84%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java (83%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java (84%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java (75%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java (58%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java (59%) 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 57ce487ac7..c5f1f5c146 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 @@ -34,11 +34,14 @@ public class FlacExtractorTest extends InstrumentationTestCase { } public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new FlacExtractor(); - } - }, "bear.flac", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new FlacExtractor(); + } + }, + "bear.flac", + getInstrumentation().getContext()); } } diff --git a/library/core/src/androidTest/assets/ssa/typical_format b/library/core/src/androidTest/assets/ssa/typical_format deleted file mode 100644 index 0cc5f1690f..0000000000 --- a/library/core/src/androidTest/assets/ssa/typical_format +++ /dev/null @@ -1 +0,0 @@ -Format: Layer, Start, End, Style, Name, Text \ No newline at end of file diff --git a/library/core/src/test/AndroidManifest.xml b/library/core/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..660c33c636 --- /dev/null +++ b/library/core/src/test/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/library/core/src/test/assets/binary/1024_incrementing_bytes.mp3 b/library/core/src/test/assets/binary/1024_incrementing_bytes.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c8b49c8cd518e58491924bfc364ff26e01a85009 GIT binary patch literal 1024 zcmZQzWMXDvWn<^yMC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH zG%_|ZH8Z!cw6eCbwX=6{baHlab#wRd^z!!c_45x13RUz zF>}`JIdkXDU$Ah|;w4L$Enl&6)#^2C*R9{Mant54TeofBv2)k%J$v` -Or to the end of the media. \ No newline at end of file +Or to the end of the media. diff --git a/library/core/src/androidTest/assets/subrip/typical b/library/core/src/test/assets/subrip/typical similarity index 87% rename from library/core/src/androidTest/assets/subrip/typical rename to library/core/src/test/assets/subrip/typical index 1c8ce4dd43..1331f75651 100644 --- a/library/core/src/androidTest/assets/subrip/typical +++ b/library/core/src/test/assets/subrip/typical @@ -9,4 +9,4 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 -This is the third subtitle. \ No newline at end of file +This is the third subtitle. diff --git a/library/core/src/androidTest/assets/subrip/typical_extra_blank_line b/library/core/src/test/assets/subrip/typical_extra_blank_line similarity index 87% rename from library/core/src/androidTest/assets/subrip/typical_extra_blank_line rename to library/core/src/test/assets/subrip/typical_extra_blank_line index 83508dd733..f5882a1d68 100644 --- a/library/core/src/androidTest/assets/subrip/typical_extra_blank_line +++ b/library/core/src/test/assets/subrip/typical_extra_blank_line @@ -10,4 +10,4 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 -This is the third subtitle. \ No newline at end of file +This is the third subtitle. diff --git a/library/core/src/androidTest/assets/subrip/typical_missing_sequence b/library/core/src/test/assets/subrip/typical_missing_sequence similarity index 86% rename from library/core/src/androidTest/assets/subrip/typical_missing_sequence rename to library/core/src/test/assets/subrip/typical_missing_sequence index 9318ba3239..56d49ac63c 100644 --- a/library/core/src/androidTest/assets/subrip/typical_missing_sequence +++ b/library/core/src/test/assets/subrip/typical_missing_sequence @@ -8,4 +8,4 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 -This is the third subtitle. \ No newline at end of file +This is the third subtitle. diff --git a/library/core/src/androidTest/assets/subrip/typical_missing_timecode b/library/core/src/test/assets/subrip/typical_missing_timecode similarity index 85% rename from library/core/src/androidTest/assets/subrip/typical_missing_timecode rename to library/core/src/test/assets/subrip/typical_missing_timecode index b9c999ada9..2c6fe69b6f 100644 --- a/library/core/src/androidTest/assets/subrip/typical_missing_timecode +++ b/library/core/src/test/assets/subrip/typical_missing_timecode @@ -8,4 +8,4 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 -This is the third subtitle. \ No newline at end of file +This is the third subtitle. diff --git a/library/core/src/androidTest/assets/subrip/typical_negative_timestamps b/library/core/src/test/assets/subrip/typical_negative_timestamps similarity index 100% rename from library/core/src/androidTest/assets/subrip/typical_negative_timestamps rename to library/core/src/test/assets/subrip/typical_negative_timestamps diff --git a/library/core/src/androidTest/assets/subrip/typical_unexpected_end b/library/core/src/test/assets/subrip/typical_unexpected_end similarity index 98% rename from library/core/src/androidTest/assets/subrip/typical_unexpected_end rename to library/core/src/test/assets/subrip/typical_unexpected_end index 8e2949b8db..91e82b1174 100644 --- a/library/core/src/androidTest/assets/subrip/typical_unexpected_end +++ b/library/core/src/test/assets/subrip/typical_unexpected_end @@ -7,4 +7,4 @@ This is the first subtitle. This is the second subtitle. Second subtitle with second line. -3 \ No newline at end of file +3 diff --git a/library/core/src/androidTest/assets/subrip/typical_with_byte_order_mark b/library/core/src/test/assets/subrip/typical_with_byte_order_mark similarity index 87% rename from library/core/src/androidTest/assets/subrip/typical_with_byte_order_mark rename to library/core/src/test/assets/subrip/typical_with_byte_order_mark index 9601c99bfb..4f5b32f4d7 100644 --- a/library/core/src/androidTest/assets/subrip/typical_with_byte_order_mark +++ b/library/core/src/test/assets/subrip/typical_with_byte_order_mark @@ -9,4 +9,4 @@ Second subtitle with second line. 3 00:00:04,567 --> 00:00:08,901 -This is the third subtitle. \ No newline at end of file +This is the third subtitle. diff --git a/library/core/src/androidTest/assets/ts/sample.ac3 b/library/core/src/test/assets/ts/sample.ac3 similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ac3 rename to library/core/src/test/assets/ts/sample.ac3 diff --git a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/test/assets/ts/sample.ac3.0.dump similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ac3.0.dump rename to library/core/src/test/assets/ts/sample.ac3.0.dump diff --git a/library/core/src/androidTest/assets/ts/sample.adts b/library/core/src/test/assets/ts/sample.adts similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.adts rename to library/core/src/test/assets/ts/sample.adts diff --git a/library/core/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/test/assets/ts/sample.adts.0.dump similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.adts.0.dump rename to library/core/src/test/assets/ts/sample.adts.0.dump diff --git a/library/core/src/androidTest/assets/ts/sample.ps b/library/core/src/test/assets/ts/sample.ps similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ps rename to library/core/src/test/assets/ts/sample.ps diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/test/assets/ts/sample.ps.0.dump similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ps.0.dump rename to library/core/src/test/assets/ts/sample.ps.0.dump diff --git a/library/core/src/androidTest/assets/ts/sample.ts b/library/core/src/test/assets/ts/sample.ts similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ts rename to library/core/src/test/assets/ts/sample.ts diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/test/assets/ts/sample.ts.0.dump similarity index 100% rename from library/core/src/androidTest/assets/ts/sample.ts.0.dump rename to library/core/src/test/assets/ts/sample.ts.0.dump diff --git a/library/core/src/androidTest/assets/ts/sample_with_sdt.ts b/library/core/src/test/assets/ts/sample_with_sdt.ts similarity index 100% rename from library/core/src/androidTest/assets/ts/sample_with_sdt.ts rename to library/core/src/test/assets/ts/sample_with_sdt.ts diff --git a/library/core/src/androidTest/assets/ttml/chain_multiple_styles.xml b/library/core/src/test/assets/ttml/chain_multiple_styles.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/chain_multiple_styles.xml rename to library/core/src/test/assets/ttml/chain_multiple_styles.xml diff --git a/library/core/src/androidTest/assets/ttml/font_size.xml b/library/core/src/test/assets/ttml/font_size.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/font_size.xml rename to library/core/src/test/assets/ttml/font_size.xml diff --git a/library/core/src/androidTest/assets/ttml/font_size_empty.xml b/library/core/src/test/assets/ttml/font_size_empty.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/font_size_empty.xml rename to library/core/src/test/assets/ttml/font_size_empty.xml diff --git a/library/core/src/androidTest/assets/ttml/font_size_invalid.xml b/library/core/src/test/assets/ttml/font_size_invalid.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/font_size_invalid.xml rename to library/core/src/test/assets/ttml/font_size_invalid.xml diff --git a/library/core/src/androidTest/assets/ttml/font_size_no_unit.xml b/library/core/src/test/assets/ttml/font_size_no_unit.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/font_size_no_unit.xml rename to library/core/src/test/assets/ttml/font_size_no_unit.xml diff --git a/library/core/src/androidTest/assets/ttml/frame_rate.xml b/library/core/src/test/assets/ttml/frame_rate.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/frame_rate.xml rename to library/core/src/test/assets/ttml/frame_rate.xml diff --git a/library/core/src/androidTest/assets/ttml/inherit_and_override_style.xml b/library/core/src/test/assets/ttml/inherit_and_override_style.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/inherit_and_override_style.xml rename to library/core/src/test/assets/ttml/inherit_and_override_style.xml diff --git a/library/core/src/androidTest/assets/ttml/inherit_global_and_parent.xml b/library/core/src/test/assets/ttml/inherit_global_and_parent.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/inherit_global_and_parent.xml rename to library/core/src/test/assets/ttml/inherit_global_and_parent.xml diff --git a/library/core/src/androidTest/assets/ttml/inherit_multiple_styles.xml b/library/core/src/test/assets/ttml/inherit_multiple_styles.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/inherit_multiple_styles.xml rename to library/core/src/test/assets/ttml/inherit_multiple_styles.xml diff --git a/library/core/src/androidTest/assets/ttml/inherit_style.xml b/library/core/src/test/assets/ttml/inherit_style.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/inherit_style.xml rename to library/core/src/test/assets/ttml/inherit_style.xml diff --git a/library/core/src/androidTest/assets/ttml/inline_style_attributes.xml b/library/core/src/test/assets/ttml/inline_style_attributes.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/inline_style_attributes.xml rename to library/core/src/test/assets/ttml/inline_style_attributes.xml diff --git a/library/core/src/androidTest/assets/ttml/multiple_regions.xml b/library/core/src/test/assets/ttml/multiple_regions.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/multiple_regions.xml rename to library/core/src/test/assets/ttml/multiple_regions.xml diff --git a/library/core/src/androidTest/assets/ttml/no_underline_linethrough.xml b/library/core/src/test/assets/ttml/no_underline_linethrough.xml similarity index 100% rename from library/core/src/androidTest/assets/ttml/no_underline_linethrough.xml rename to library/core/src/test/assets/ttml/no_underline_linethrough.xml diff --git a/library/core/src/androidTest/assets/tx3g/initialization b/library/core/src/test/assets/tx3g/initialization similarity index 100% rename from library/core/src/androidTest/assets/tx3g/initialization rename to library/core/src/test/assets/tx3g/initialization diff --git a/library/core/src/androidTest/assets/tx3g/initialization_all_defaults b/library/core/src/test/assets/tx3g/initialization_all_defaults similarity index 100% rename from library/core/src/androidTest/assets/tx3g/initialization_all_defaults rename to library/core/src/test/assets/tx3g/initialization_all_defaults diff --git a/library/core/src/androidTest/assets/tx3g/no_subtitle b/library/core/src/test/assets/tx3g/no_subtitle similarity index 100% rename from library/core/src/androidTest/assets/tx3g/no_subtitle rename to library/core/src/test/assets/tx3g/no_subtitle diff --git a/library/core/src/androidTest/assets/tx3g/sample_just_text b/library/core/src/test/assets/tx3g/sample_just_text similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_just_text rename to library/core/src/test/assets/tx3g/sample_just_text diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl b/library/core/src/test/assets/tx3g/sample_utf16_be_no_styl similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_utf16_be_no_styl rename to library/core/src/test/assets/tx3g/sample_utf16_be_no_styl diff --git a/library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl b/library/core/src/test/assets/tx3g/sample_utf16_le_no_styl similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_utf16_le_no_styl rename to library/core/src/test/assets/tx3g/sample_utf16_le_no_styl diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl b/library/core/src/test/assets/tx3g/sample_with_multiple_styl similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_with_multiple_styl rename to library/core/src/test/assets/tx3g/sample_with_multiple_styl diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_other_extension b/library/core/src/test/assets/tx3g/sample_with_other_extension similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_with_other_extension rename to library/core/src/test/assets/tx3g/sample_with_other_extension diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_styl b/library/core/src/test/assets/tx3g/sample_with_styl similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_with_styl rename to library/core/src/test/assets/tx3g/sample_with_styl diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults b/library/core/src/test/assets/tx3g/sample_with_styl_all_defaults similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_with_styl_all_defaults rename to library/core/src/test/assets/tx3g/sample_with_styl_all_defaults diff --git a/library/core/src/androidTest/assets/tx3g/sample_with_tbox b/library/core/src/test/assets/tx3g/sample_with_tbox similarity index 100% rename from library/core/src/androidTest/assets/tx3g/sample_with_tbox rename to library/core/src/test/assets/tx3g/sample_with_tbox diff --git a/library/core/src/androidTest/assets/wav/sample.wav b/library/core/src/test/assets/wav/sample.wav similarity index 100% rename from library/core/src/androidTest/assets/wav/sample.wav rename to library/core/src/test/assets/wav/sample.wav diff --git a/library/core/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/test/assets/wav/sample.wav.0.dump similarity index 100% rename from library/core/src/androidTest/assets/wav/sample.wav.0.dump rename to library/core/src/test/assets/wav/sample.wav.0.dump diff --git a/library/core/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/test/assets/wav/sample.wav.1.dump similarity index 100% rename from library/core/src/androidTest/assets/wav/sample.wav.1.dump rename to library/core/src/test/assets/wav/sample.wav.1.dump diff --git a/library/core/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/test/assets/wav/sample.wav.2.dump similarity index 100% rename from library/core/src/androidTest/assets/wav/sample.wav.2.dump rename to library/core/src/test/assets/wav/sample.wav.2.dump diff --git a/library/core/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/test/assets/wav/sample.wav.3.dump similarity index 100% rename from library/core/src/androidTest/assets/wav/sample.wav.3.dump rename to library/core/src/test/assets/wav/sample.wav.3.dump diff --git a/library/core/src/androidTest/assets/webm/vorbis_codec_private b/library/core/src/test/assets/webm/vorbis_codec_private similarity index 100% rename from library/core/src/androidTest/assets/webm/vorbis_codec_private rename to library/core/src/test/assets/webm/vorbis_codec_private diff --git a/library/core/src/androidTest/assets/webvtt/empty b/library/core/src/test/assets/webvtt/empty similarity index 100% rename from library/core/src/androidTest/assets/webvtt/empty rename to library/core/src/test/assets/webvtt/empty diff --git a/library/core/src/androidTest/assets/webvtt/typical b/library/core/src/test/assets/webvtt/typical similarity index 100% rename from library/core/src/androidTest/assets/webvtt/typical rename to library/core/src/test/assets/webvtt/typical diff --git a/library/core/src/androidTest/assets/webvtt/typical_with_bad_timestamps b/library/core/src/test/assets/webvtt/typical_with_bad_timestamps similarity index 100% rename from library/core/src/androidTest/assets/webvtt/typical_with_bad_timestamps rename to library/core/src/test/assets/webvtt/typical_with_bad_timestamps diff --git a/library/core/src/androidTest/assets/webvtt/typical_with_comments b/library/core/src/test/assets/webvtt/typical_with_comments similarity index 100% rename from library/core/src/androidTest/assets/webvtt/typical_with_comments rename to library/core/src/test/assets/webvtt/typical_with_comments diff --git a/library/core/src/androidTest/assets/webvtt/typical_with_identifiers b/library/core/src/test/assets/webvtt/typical_with_identifiers similarity index 100% rename from library/core/src/androidTest/assets/webvtt/typical_with_identifiers rename to library/core/src/test/assets/webvtt/typical_with_identifiers diff --git a/library/core/src/androidTest/assets/webvtt/with_bad_cue_header b/library/core/src/test/assets/webvtt/with_bad_cue_header similarity index 100% rename from library/core/src/androidTest/assets/webvtt/with_bad_cue_header rename to library/core/src/test/assets/webvtt/with_bad_cue_header diff --git a/library/core/src/androidTest/assets/webvtt/with_css_complex_selectors b/library/core/src/test/assets/webvtt/with_css_complex_selectors similarity index 100% rename from library/core/src/androidTest/assets/webvtt/with_css_complex_selectors rename to library/core/src/test/assets/webvtt/with_css_complex_selectors diff --git a/library/core/src/androidTest/assets/webvtt/with_css_styles b/library/core/src/test/assets/webvtt/with_css_styles similarity index 100% rename from library/core/src/androidTest/assets/webvtt/with_css_styles rename to library/core/src/test/assets/webvtt/with_css_styles diff --git a/library/core/src/androidTest/assets/webvtt/with_positioning b/library/core/src/test/assets/webvtt/with_positioning similarity index 100% rename from library/core/src/androidTest/assets/webvtt/with_positioning rename to library/core/src/test/assets/webvtt/with_positioning diff --git a/library/core/src/androidTest/assets/webvtt/with_tags b/library/core/src/test/assets/webvtt/with_tags similarity index 100% rename from library/core/src/androidTest/assets/webvtt/with_tags rename to library/core/src/test/assets/webvtt/with_tags diff --git a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java index ff4756f5ed..f56510f654 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java @@ -22,13 +22,11 @@ import android.media.MediaCodec; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link C}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class CTest { @SuppressLint("InlinedApi") 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 53872c59f0..ece22dc02a 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 @@ -30,13 +30,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link DefaultMediaClock}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class DefaultMediaClockTest { private static final long TEST_POSITION_US = 123456789012345678L; @@ -377,6 +375,7 @@ public class DefaultMediaClockTest { assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs); } + @SuppressWarnings("HidingField") private static class MediaClockRenderer extends FakeMediaClockRenderer { private final boolean playbackParametersAreMutable; 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 ec5a8ccfce..7faa349705 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 @@ -55,11 +55,7 @@ import org.robolectric.annotation.Config; /** Unit test for {@link ExoPlayer}. */ @RunWith(RobolectricTestRunner.class) -@Config( - sdk = Config.TARGET_SDK, - manifest = Config.NONE, - shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class} -) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public final class ExoPlayerTest { /** 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 33e1a673bd..eb51485a36 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 @@ -39,13 +39,11 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link Format}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class FormatTest { private static final List INIT_DATA; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java similarity index 83% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index f5c33843a1..b5457555ab 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -18,23 +18,26 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link Timeline}. - */ -public class TimelineTest extends TestCase { +/** Unit test for {@link Timeline}. */ +@RunWith(RobolectricTestRunner.class) +public class TimelineTest { + @Test public void testEmptyTimeline() { TimelineAsserts.assertEmpty(Timeline.EMPTY); } + @Test public void testSinglePeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); @@ -42,12 +45,13 @@ public class TimelineTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } + @Test public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); 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 2c10490e1f..9d5533e8ab 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 @@ -46,7 +46,6 @@ import org.robolectric.annotation.Config; * Unit test for {@link SimpleDecoderAudioRenderer}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class SimpleDecoderAudioRendererTest { private static final Format FORMAT = Format.createSampleFormat(null, MimeTypes.AUDIO_RAW, 0); 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 a4f02f8257..d060ba3f16 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 @@ -23,13 +23,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link SonicAudioProcessor}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class SonicAudioProcessorTest { private SonicAudioProcessor sonicAudioProcessor; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java index 01ab9ea9aa..c84ca6182c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/ClearKeyUtilTest.java @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import java.nio.charset.Charset; -import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -32,7 +31,7 @@ import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) public final class ClearKeyUtilTest { - @Config(sdk = 26, manifest = Config.NONE) + @Config(sdk = 26) @Test public void testAdjustResponseDataV26() { byte[] data = ("{\"keys\":[{" @@ -47,10 +46,10 @@ public final class ClearKeyUtilTest { + "\"kid\":\"ab\\/cde+f\"}]," + "\"type\":\"abc_def-" + "\"}").getBytes(Charset.forName(C.UTF8_NAME)); - assertThat(Arrays.equals(expected, ClearKeyUtil.adjustResponseData(data))).isTrue(); + assertThat(ClearKeyUtil.adjustResponseData(data)).isEqualTo(expected); } - @Config(sdk = 26, manifest = Config.NONE) + @Config(sdk = 26) @Test public void testAdjustRequestDataV26() { byte[] data = "{\"kids\":[\"abc+def/\",\"ab+cde/f\"],\"type\":\"abc+def/\"}" @@ -58,7 +57,7 @@ public final class ClearKeyUtilTest { // We expect "+" and "/" to be replaced with "-" and "_" respectively, for "kids". byte[] expected = "{\"kids\":[\"abc-def_\",\"ab-cde_f\"],\"type\":\"abc+def/\"}" .getBytes(Charset.forName(C.UTF8_NAME)); - assertThat(Arrays.equals(expected, ClearKeyUtil.adjustRequestData(data))).isTrue(); + assertThat(ClearKeyUtil.adjustRequestData(data)).isEqualTo(expected); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 9fc6e801d3..2b3bdd6a2f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -30,13 +30,11 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link DrmInitData}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class DrmInitDataTest { private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, VIDEO_MP4, diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java similarity index 74% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 8dde1ed828..f67301f017 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -16,42 +16,48 @@ package com.google.android.exoplayer2.drm; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; -import android.test.InstrumentationTestCase; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.testutil.MockitoUtil; import java.util.HashMap; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Tests {@link OfflineLicenseHelper}. - */ -public class OfflineLicenseHelperTest extends InstrumentationTestCase { +/** Tests {@link OfflineLicenseHelper}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class OfflineLicenseHelperTest { private OfflineLicenseHelper offlineLicenseHelper; @Mock private MediaDrmCallback mediaDrmCallback; @Mock private ExoMediaDrm mediaDrm; - @Override - protected void setUp() throws Exception { - super.setUp(); - MockitoUtil.setUpMockito(this); + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); - offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, - null); + offlineLicenseHelper = + new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { offlineLicenseHelper.release(); offlineLicenseHelper = null; - super.tearDown(); } + @Test public void testDownloadRenewReleaseKey() throws Exception { setStubLicenseAndPlaybackDurationValues(1000, 200); @@ -72,6 +78,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { offlineLicenseHelper.releaseLicense(offlineLicenseKeySetId2); } + @Test public void testDownloadLicenseFailsIfNullInitData() throws Exception { try { offlineLicenseHelper.downloadLicense(null); @@ -81,6 +88,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } } + @Test public void testDownloadLicenseFailsIfNoKeySetIdIsReturned() throws Exception { setStubLicenseAndPlaybackDurationValues(1000, 200); @@ -89,6 +97,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { assertThat(offlineLicenseKeySetId).isNull(); } + @Test public void testDownloadLicenseDoesNotFailIfDurationNotAvailable() throws Exception { setDefaultStubKeySetId(); @@ -97,6 +106,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { assertThat(offlineLicenseKeySetId).isNotNull(); } + @Test public void testGetLicenseDurationRemainingSec() throws Exception { long licenseDuration = 1000; int playbackDuration = 200; @@ -105,13 +115,14 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); - Pair licenseDurationRemainingSec = offlineLicenseHelper - .getLicenseDurationRemainingSec(offlineLicenseKeySetId); + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); assertThat(licenseDurationRemainingSec.first).isEqualTo(licenseDuration); assertThat(licenseDurationRemainingSec.second).isEqualTo(playbackDuration); } + @Test public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception { long licenseDuration = 0; int playbackDuration = 0; @@ -120,8 +131,8 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { byte[] offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(newDrmInitData()); - Pair licenseDurationRemainingSec = offlineLicenseHelper - .getLicenseDurationRemainingSec(offlineLicenseKeySetId); + Pair licenseDurationRemainingSec = + offlineLicenseHelper.getLicenseDurationRemainingSec(offlineLicenseKeySetId); assertThat(licenseDurationRemainingSec.first).isEqualTo(licenseDuration); assertThat(licenseDurationRemainingSec.second).isEqualTo(playbackDuration); @@ -143,19 +154,18 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { assertThat(actualKeySetId).isEqualTo(expectedKeySetId); } - private void setStubLicenseAndPlaybackDurationValues(long licenseDuration, - long playbackDuration) { + private void setStubLicenseAndPlaybackDurationValues( + long licenseDuration, long playbackDuration) { HashMap keyStatus = new HashMap<>(); - keyStatus.put(WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING, - String.valueOf(licenseDuration)); - keyStatus.put(WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING, - String.valueOf(playbackDuration)); + keyStatus.put( + WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING, String.valueOf(licenseDuration)); + keyStatus.put( + WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING, String.valueOf(playbackDuration)); when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus); } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", - new byte[] {1, 4, 7, 0, 3, 6})); + return new DrmInitData( + new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 8e27c4f7ca..a96dfaf2f8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -31,13 +31,11 @@ import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link DefaultExtractorInput}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class DefaultExtractorInputTest { private static final String TEST_URI = "http://www.google.com"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java index fc31a7be73..3271e1ddf6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java @@ -21,13 +21,11 @@ import com.google.android.exoplayer2.C; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link Extractor}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ExtractorTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java index fc8d181eac..5a093988dd 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/flv/FlvExtractorTest.java @@ -15,23 +15,26 @@ */ package com.google.android.exoplayer2.extractor.flv; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link FlvExtractor}. - */ -public final class FlvExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link FlvExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class FlvExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new FlvExtractor(); - } - }, "flv/sample.flv", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new FlvExtractor(); + } + }, + "flv/sample.flv"); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java index ee359ffa82..2fec5c7cab 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java @@ -27,13 +27,11 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests {@link DefaultEbmlReader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class DefaultEbmlReaderTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java similarity index 53% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java index 624a5ccb7e..4a0f87a80a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractorTest.java @@ -15,41 +15,50 @@ */ package com.google.android.exoplayer2.extractor.mkv; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Tests for {@link MatroskaExtractor}. - */ -public final class MatroskaExtractorTest extends InstrumentationTestCase { +/** Tests for {@link MatroskaExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class MatroskaExtractorTest { + @Test public void testMkvSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, "mkv/sample.mkv", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new MatroskaExtractor(); + } + }, + "mkv/sample.mkv"); } + @Test public void testWebmSubsampleEncryption() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, "mkv/subsample_encrypted_noaltref.webm", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new MatroskaExtractor(); + } + }, + "mkv/subsample_encrypted_noaltref.webm"); } + @Test public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new MatroskaExtractor(); - } - }, "mkv/subsample_encrypted_altref.webm", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new MatroskaExtractor(); + } + }, + "mkv/subsample_encrypted_altref.webm"); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java index bda93db812..a13a185b3e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java @@ -29,13 +29,11 @@ import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link VarintReader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class VarintReaderTest { private static final byte MAX_BYTE = (byte) 0xFF; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java index 0f98624d69..b977766a1c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/Mp3ExtractorTest.java @@ -15,32 +15,38 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link Mp3Extractor}. - */ -public final class Mp3ExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link Mp3Extractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class Mp3ExtractorTest { + @Test public void testMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp3Extractor(); - } - }, "mp3/bear.mp3", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new Mp3Extractor(); + } + }, + "mp3/bear.mp3"); } + @Test public void testTrimmedMp3Sample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp3Extractor(); - } - }, "mp3/play-trimmed.mp3", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new Mp3Extractor(); + } + }, + "mp3/play-trimmed.mp3"); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index 46cd7a2451..6df40b5dcc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -27,13 +27,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link XingSeeker}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class XingSeekerTest { // Xing header/payload from http://storage.googleapis.com/exoplayer-test-media-0/play.mp3. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index b0c37ee452..9c7f0e8acc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -22,13 +22,11 @@ import com.google.android.exoplayer2.util.Util; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link AtomParsers}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class AtomParsersTest { private static final String ATOM_HEADER = "000000000000000000000000"; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java similarity index 71% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java index d24788f74a..f5b0f48592 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; @@ -23,23 +22,28 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Collections; import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link FragmentedMp4Extractor}. - */ -public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link FragmentedMp4Extractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class FragmentedMp4ExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.emptyList()), - "mp4/sample_fragmented.mp4", getInstrumentation()); + ExtractorAsserts.assertBehavior( + getExtractorFactory(Collections.emptyList()), "mp4/sample_fragmented.mp4"); } + @Test public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList( - Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); - ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4", - getInstrumentation()); + ExtractorFactory extractorFactory = + getExtractorFactory( + Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4"); } private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { @@ -50,5 +54,4 @@ public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { } }; } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java similarity index 69% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index 5e327e5502..f1812a69c4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -16,24 +16,27 @@ package com.google.android.exoplayer2.extractor.mp4; import android.annotation.TargetApi; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Tests for {@link Mp4Extractor}. - */ +/** Tests for {@link Mp4Extractor}. */ @TargetApi(16) -public final class Mp4ExtractorTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +public final class Mp4ExtractorTest { + @Test public void testMp4Sample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new Mp4Extractor(); - } - }, "mp4/sample.mp4", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new Mp4Extractor(); + } + }, + "mp4/sample.mp4"); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java index 4d7931cc02..d7a13ab061 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java @@ -27,13 +27,11 @@ import java.util.UUID; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link PsshAtomUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class PsshAtomUtilTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java similarity index 84% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java index d0f666d72a..993bb86b48 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerTest.java @@ -17,19 +17,22 @@ 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 com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link DefaultOggSeeker}. - */ -public final class DefaultOggSeekerTest extends TestCase { +/** Unit test for {@link DefaultOggSeeker}. */ +@RunWith(RobolectricTestRunner.class) +public final class DefaultOggSeekerTest { + @Test public void testSetupWithUnsetEndPositionFails() { try { new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader(), 1, 1); @@ -39,6 +42,7 @@ public final class DefaultOggSeekerTest extends TestCase { } } + @Test public void testSeeking() throws IOException, InterruptedException { Random random = new Random(0); for (int i = 0; i < 100; i++) { @@ -50,8 +54,13 @@ public final class DefaultOggSeekerTest extends TestCase { OggTestFile testFile = OggTestFile.generate(random, 1000); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); TestStreamReader streamReader = new TestStreamReader(); - DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader, - testFile.firstPayloadPageSize, testFile.firstPayloadPageGranulePosition); + DefaultOggSeeker oggSeeker = + new DefaultOggSeeker( + 0, + testFile.data.length, + streamReader, + testFile.firstPayloadPageSize, + testFile.firstPayloadPageGranulePosition); OggPageHeader pageHeader = new OggPageHeader(); while (true) { @@ -119,14 +128,19 @@ public final class DefaultOggSeekerTest extends TestCase { 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."); + fail( + "granuleDiff (" + + granuleDiff + + ") or positionDiff (" + + positionDiff + + ") is more than allowed."); } } } - private long seekTo(FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, - int initialPosition) throws IOException, InterruptedException { + private long seekTo( + FakeExtractorInput input, DefaultOggSeeker oggSeeker, long targetGranule, int initialPosition) + throws IOException, InterruptedException { long nextSeekPosition = initialPosition; int count = 0; oggSeeker.resetSeeking(); @@ -150,8 +164,8 @@ public final class DefaultOggSeekerTest extends TestCase { } @Override - protected boolean readHeaders(ParsableByteArray packet, long position, - SetupData setupData) throws IOException, InterruptedException { + protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) + throws IOException, InterruptedException { 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 a3f7e9a548..be771ac3b9 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 @@ -28,13 +28,11 @@ import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link DefaultOggSeeker} utility methods. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DefaultOggSeekerUtilMethodsTest { private final Random random = new Random(0); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java similarity index 79% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java index fdab480167..20808f73f2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; @@ -25,38 +24,43 @@ 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; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link OggExtractor}. - */ -public final class OggExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link OggExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class OggExtractorTest { - private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = new ExtractorFactory() { - @Override - public Extractor create() { - return new OggExtractor(); - } - }; + private static final ExtractorFactory OGG_EXTRACTOR_FACTORY = + new ExtractorFactory() { + @Override + public Extractor create() { + return new OggExtractor(); + } + }; + @Test public void testOpus() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus"); } + @Test public void testFlac() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", - getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg"); } + @Test public void testFlacNoSeektable() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", - getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg"); } + @Test public void testVorbis() throws Exception { - ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", - getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg"); } + @Test public void testSniffVorbis() throws Exception { byte[] data = TestUtil.joinByteArrays( @@ -66,6 +70,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { assertThat(sniff(data)).isTrue(); } + @Test public void testSniffFlac() throws Exception { byte[] data = TestUtil.joinByteArrays( @@ -75,6 +80,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { assertThat(sniff(data)).isTrue(); } + @Test public void testSniffFailsOpusFile() throws Exception { byte[] data = TestUtil.joinByteArrays( @@ -82,11 +88,13 @@ public final class OggExtractorTest extends InstrumentationTestCase { assertThat(sniff(data)).isFalse(); } + @Test public void testSniffFailsInvalidOggHeader() throws Exception { byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00); assertThat(sniff(data)).isFalse(); } + @Test public void testSniffInvalidHeader() throws Exception { byte[] data = TestUtil.joinByteArrays( @@ -96,16 +104,20 @@ public final class OggExtractorTest extends InstrumentationTestCase { assertThat(sniff(data)).isFalse(); } + @Test public void testSniffFailsEOF() throws Exception { byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00); assertThat(sniff(data)).isFalse(); } private boolean sniff(byte[] data) throws InterruptedException, IOException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) - .setSimulateIOErrors(true).setSimulateUnknownLength(true).setSimulatePartialReads(true) - .build(); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(true) + .setSimulatePartialReads(true) + .build(); return TestUtil.sniffTestData(OGG_EXTRACTOR_FACTORY.create(), input); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java similarity index 68% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java index ca511adb7e..e9af630f83 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.ogg; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; @@ -25,24 +24,28 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; import java.util.Arrays; import java.util.Random; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link OggPacket}. - */ -public final class OggPacketTest extends InstrumentationTestCase { +/** Unit test for {@link OggPacket}. */ +@RunWith(RobolectricTestRunner.class) +public final class OggPacketTest { private static final String TEST_FILE = "ogg/bear.opus"; private Random random; private OggPacket oggPacket; - @Override + @Before public void setUp() throws Exception { - super.setUp(); random = new Random(0); oggPacket = new OggPacket(); } + @Test public void testReadPacketsWithEmptyPage() throws Exception { byte[] firstPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(272, random); @@ -107,35 +110,41 @@ public final class OggPacketTest extends InstrumentationTestCase { assertReadEof(input); } + @Test public void testReadPacketWithZeroSizeTerminator() throws Exception { byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] secondPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = OggTestData.createInput( - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x06, 0, 1000, 0x04), - TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. - firstPacket, - secondPacket), true); + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + OggTestData.buildOggHeader(0x06, 0, 1000, 0x04), + TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. + firstPacket, + secondPacket), + true); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); assertReadEof(input); } + @Test public void testReadContinuedPacketOverTwoPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(518); - FakeExtractorInput input = OggTestData.createInput( - TestUtil.joinByteArrays( - // First page. - OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - OggTestData.buildOggHeader(0x05, 10, 1001, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + // First page. + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), + TestUtil.createByteArray(0xFF, 0xFF), // Laces. + Arrays.copyOf(firstPacket, 510), + // Second page (continued packet). + OggTestData.buildOggHeader(0x05, 10, 1001, 0x01), + TestUtil.createByteArray(0x08), // Laces. + Arrays.copyOfRange(firstPacket, 510, 510 + 8)), + true); assertReadPacket(input, firstPacket); assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue(); @@ -145,27 +154,30 @@ public final class OggPacketTest extends InstrumentationTestCase { assertReadEof(input); } + @Test public void testReadContinuedPacketOverFourPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(1028); - FakeExtractorInput input = OggTestData.createInput( - TestUtil.joinByteArrays( - // First page. - OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), - TestUtil.createByteArray(0xFF, 0xFF), // Laces. - Arrays.copyOf(firstPacket, 510), - // Second page (continued packet). - OggTestData.buildOggHeader(0x01, 10, 1001, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510, 510 + 255), - // Third page (continued packet). - OggTestData.buildOggHeader(0x01, 10, 1002, 0x01), - TestUtil.createByteArray(0xFF), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), - // Fourth page (continued packet). - OggTestData.buildOggHeader(0x05, 10, 1003, 0x01), - TestUtil.createByteArray(0x08), // Laces. - Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + // First page. + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), + TestUtil.createByteArray(0xFF, 0xFF), // Laces. + Arrays.copyOf(firstPacket, 510), + // Second page (continued packet). + OggTestData.buildOggHeader(0x01, 10, 1001, 0x01), + TestUtil.createByteArray(0xFF), // Laces. + Arrays.copyOfRange(firstPacket, 510, 510 + 255), + // Third page (continued packet). + OggTestData.buildOggHeader(0x01, 10, 1002, 0x01), + TestUtil.createByteArray(0xFF), // Laces. + Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), + // Fourth page (continued packet). + OggTestData.buildOggHeader(0x05, 10, 1003, 0x01), + TestUtil.createByteArray(0x08), // Laces. + Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), + true); assertReadPacket(input, firstPacket); assertThat((oggPacket.getPageHeader().type & 0x04) == 0x04).isTrue(); @@ -175,37 +187,43 @@ public final class OggPacketTest extends InstrumentationTestCase { assertReadEof(input); } + @Test public void testReadDiscardContinuedPacketAtStart() throws Exception { byte[] pageBody = TestUtil.buildTestData(256 + 8); - FakeExtractorInput input = OggTestData.createInput( - TestUtil.joinByteArrays( - // Page with a continued packet at start. - OggTestData.buildOggHeader(0x01, 10, 1001, 0x03), - TestUtil.createByteArray(255, 1, 8), // Laces. - pageBody), true); + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + // Page with a continued packet at start. + OggTestData.buildOggHeader(0x01, 10, 1001, 0x03), + TestUtil.createByteArray(255, 1, 8), // Laces. + pageBody), + true); // Expect the first partial packet to be discarded. assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8)); assertReadEof(input); } + @Test public void testReadZeroSizedPacketsAtEndOfStream() throws Exception { byte[] firstPacket = TestUtil.buildTestData(8, random); byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] thirdPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = OggTestData.createInput( - TestUtil.joinByteArrays( - OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), - TestUtil.createByteArray(0x08), // Laces. - firstPacket, - OggTestData.buildOggHeader(0x04, 0, 1001, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - secondPacket, - OggTestData.buildOggHeader(0x04, 0, 1002, 0x03), - TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. - thirdPacket), true); + FakeExtractorInput input = + OggTestData.createInput( + TestUtil.joinByteArrays( + OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), + TestUtil.createByteArray(0x08), // Laces. + firstPacket, + OggTestData.buildOggHeader(0x04, 0, 1001, 0x03), + TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. + secondPacket, + OggTestData.buildOggHeader(0x04, 0, 1002, 0x03), + TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. + thirdPacket), + true); assertReadPacket(input, firstPacket); assertReadPacket(input, secondPacket); @@ -213,9 +231,9 @@ public final class OggPacketTest extends InstrumentationTestCase { assertReadEof(input); } - + @Test public void testParseRealFile() throws IOException, InterruptedException { - byte[] data = TestUtil.getByteArray(getInstrumentation(), TEST_FILE); + byte[] data = TestUtil.getByteArray(RuntimeEnvironment.application, TEST_FILE); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); int packetCounter = 0; while (readPacket(input)) { @@ -236,8 +254,7 @@ public final class OggPacketTest extends InstrumentationTestCase { assertThat(readPacket(extractorInput)).isFalse(); } - private boolean readPacket(FakeExtractorInput input) - throws InterruptedException, IOException { + private boolean readPacket(FakeExtractorInput input) throws InterruptedException, IOException { while (true) { try { return oggPacket.populate(input); @@ -246,5 +263,4 @@ public final class OggPacketTest extends InstrumentationTestCase { } } } - } 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 c8bcffde3c..930d067d2b 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 @@ -25,13 +25,11 @@ import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link OggPageHeader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class OggPageHeaderTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java similarity index 91% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java index 4803c28a54..e5512dda36 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggTestFile.java @@ -22,9 +22,7 @@ import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; import java.util.Random; -/** - * Generates test data. - */ +/** Generates test data. */ /* package */ final class OggTestFile { private static final int MAX_PACKET_LENGTH = 2048; @@ -38,8 +36,13 @@ import java.util.Random; public final int firstPayloadPageSize; public final long firstPayloadPageGranulePosition; - private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount, - int firstPayloadPageSize, long firstPayloadPageGranulePosition) { + private OggTestFile( + byte[] data, + long lastGranule, + int packetCount, + int pageCount, + int firstPayloadPageSize, + long firstPayloadPageGranulePosition) { this.data = data; this.lastGranule = lastGranule; this.packetCount = packetCount; @@ -110,7 +113,12 @@ import java.util.Random; System.arraycopy(data, 0, file, position, data.length); position += data.length; } - return new OggTestFile(file, granule, packetCount, pageCount, firstPayloadPageSize, + return new OggTestFile( + file, + granule, + packetCount, + pageCount, + firstPayloadPageSize, firstPayloadPageGranulePosition); } @@ -123,5 +131,4 @@ import java.util.Random; fail(); return -1; } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java index 08b9b12a18..eca94f076e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java @@ -21,13 +21,11 @@ import com.google.android.exoplayer2.testutil.TestUtil; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link VorbisBitArray}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class VorbisBitArrayTest { @Test 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 20a76e83e0..f0361c5395 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 @@ -29,13 +29,11 @@ import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link VorbisReader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class VorbisReaderTest { @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 bdc573f218..5b395771fc 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 @@ -26,13 +26,11 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link VorbisUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class VorbisUtilTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java similarity index 69% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 18050f48a3..9632577e82 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -16,29 +16,38 @@ package com.google.android.exoplayer2.extractor.rawcc; import android.annotation.TargetApi; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Tests for {@link RawCcExtractor}. - */ +/** Tests for {@link RawCcExtractor}. */ @TargetApi(16) -public final class RawCcExtractorTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +public final class RawCcExtractorTest { + @Test public void testRawCcSample() throws Exception { ExtractorAsserts.assertBehavior( new ExtractorFactory() { @Override public Extractor create() { return new RawCcExtractor( - Format.createTextContainerFormat(null, null, MimeTypes.APPLICATION_CEA608, - "cea608", Format.NO_VALUE, 0, null, 1)); + Format.createTextContainerFormat( + null, + null, + MimeTypes.APPLICATION_CEA608, + "cea608", + Format.NO_VALUE, + 0, + null, + 1)); } - }, "rawcc/sample.rawcc", getInstrumentation()); + }, + "rawcc/sample.rawcc"); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java index 31633361db..ec7afeeeab 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/Ac3ExtractorTest.java @@ -15,23 +15,26 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link Ac3Extractor}. - */ -public final class Ac3ExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link Ac3Extractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class Ac3ExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new Ac3Extractor(); - } - }, "ts/sample.ac3", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new Ac3Extractor(); + } + }, + "ts/sample.ac3"); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 9eb65d2091..048a23cd67 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -15,23 +15,26 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link AdtsExtractor}. - */ -public final class AdtsExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link AdtsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class AdtsExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new AdtsExtractor(); - } - }, "ts/sample.adts", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new AdtsExtractor(); + } + }, + "ts/sample.adts"); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java similarity index 69% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index 1a10d24c94..1098ba7563 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -23,41 +23,39 @@ import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Test for {@link AdtsReader}. - */ -public class AdtsReaderTest extends TestCase { +/** Test for {@link AdtsReader}. */ +@RunWith(RobolectricTestRunner.class) +public class AdtsReaderTest { - public static final byte[] ID3_DATA_1 = TestUtil.createByteArray( - 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x54, 0x58, - 0x58, 0x58, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, 0x03, 0x00, 0x20, 0x2a, - 0x2a, 0x2a, 0x20, 0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x54, - 0x69, 0x6d, 0x65, 0x64, 0x20, 0x4d, 0x65, 0x74, 0x61, 0x44, 0x61, 0x74, - 0x61, 0x20, 0x40, 0x20, 0x2d, 0x2d, 0x20, 0x30, 0x30, 0x3a, 0x30, 0x30, - 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x00); + public static final byte[] ID3_DATA_1 = + TestUtil.createByteArray( + 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x54, 0x58, 0x58, 0x58, 0x00, + 0x00, 0x00, 0x33, 0x00, 0x00, 0x03, 0x00, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x54, 0x48, 0x49, + 0x53, 0x20, 0x49, 0x53, 0x20, 0x54, 0x69, 0x6d, 0x65, 0x64, 0x20, 0x4d, 0x65, 0x74, 0x61, + 0x44, 0x61, 0x74, 0x61, 0x20, 0x40, 0x20, 0x2d, 0x2d, 0x20, 0x30, 0x30, 0x3a, 0x30, 0x30, + 0x3a, 0x30, 0x30, 0x2e, 0x30, 0x20, 0x2a, 0x2a, 0x2a, 0x20, 0x00); - public static final byte[] ID3_DATA_2 = TestUtil.createByteArray( - 0x49, - 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x50, 0x52, 0x49, - 0x56, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, - 0x70, 0x70, 0x6c, 0x65, 0x2e, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, - 0x6e, 0x67, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, - 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xbb, 0xa0); + public static final byte[] ID3_DATA_2 = + TestUtil.createByteArray( + 0x49, 0x44, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x50, 0x52, 0x49, 0x56, 0x00, + 0x00, 0x00, 0x35, 0x00, 0x00, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x70, 0x70, 0x6c, 0x65, 0x2e, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xbb, 0xa0); - public static final byte[] ADTS_HEADER = TestUtil.createByteArray( - 0xff, 0xf1, 0x50, 0x80, 0x01, 0xdf, 0xfc); + public static final byte[] ADTS_HEADER = + TestUtil.createByteArray(0xff, 0xf1, 0x50, 0x80, 0x01, 0xdf, 0xfc); - public static final byte[] ADTS_CONTENT = TestUtil.createByteArray( - 0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e); + public static final byte[] ADTS_CONTENT = + TestUtil.createByteArray(0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e); - private static final byte[] TEST_DATA = TestUtil.joinByteArrays( - ID3_DATA_1, - ID3_DATA_2, - ADTS_HEADER, - ADTS_CONTENT); + private static final byte[] TEST_DATA = + TestUtil.joinByteArrays(ID3_DATA_1, ID3_DATA_2, ADTS_HEADER, ADTS_CONTENT); private static final long ADTS_SAMPLE_DURATION = 23219L; @@ -67,9 +65,8 @@ public class AdtsReaderTest extends TestCase { private ParsableByteArray data; private boolean firstFeed; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); @@ -80,6 +77,7 @@ public class AdtsReaderTest extends TestCase { firstFeed = true; } + @Test public void testSkipToNextSample() throws Exception { for (int i = 1; i <= ID3_DATA_1.length + ID3_DATA_2.length; i++) { data.setPosition(i); @@ -90,50 +88,60 @@ public class AdtsReaderTest extends TestCase { } } + @Test public void testSkipToNextSampleResetsState() throws Exception { - data = new ParsableByteArray(TestUtil.joinByteArrays( - ADTS_HEADER, - ADTS_CONTENT, - // Adts sample missing the first sync byte - Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length), - ADTS_CONTENT)); + data = + new ParsableByteArray( + TestUtil.joinByteArrays( + ADTS_HEADER, + ADTS_CONTENT, + // Adts sample missing the first sync byte + Arrays.copyOfRange(ADTS_HEADER, 1, ADTS_HEADER.length), + ADTS_CONTENT)); feed(); assertSampleCounts(0, 1); adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null); } + @Test public void testNoData() throws Exception { feedLimited(0); assertSampleCounts(0, 0); } + @Test public void testNotEnoughDataForIdentifier() throws Exception { feedLimited(3 - 1); assertSampleCounts(0, 0); } + @Test public void testNotEnoughDataForHeader() throws Exception { feedLimited(10 - 1); assertSampleCounts(0, 0); } + @Test public void testNotEnoughDataForWholeId3Packet() throws Exception { feedLimited(ID3_DATA_1.length - 1); assertSampleCounts(0, 0); } + @Test public void testConsumeWholeId3Packet() throws Exception { feedLimited(ID3_DATA_1.length); assertSampleCounts(1, 0); id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null); } + @Test public void testMultiId3Packet() throws Exception { feedLimited(ID3_DATA_1.length + ID3_DATA_2.length - 1); assertSampleCounts(1, 0); id3Output.assertSample(0, ID3_DATA_1, 0, C.BUFFER_FLAG_KEY_FRAME, null); } + @Test public void testMultiId3PacketConsumed() throws Exception { feedLimited(ID3_DATA_1.length + ID3_DATA_2.length); assertSampleCounts(2, 0); @@ -141,6 +149,7 @@ public class AdtsReaderTest extends TestCase { id3Output.assertSample(1, ID3_DATA_2, 0, C.BUFFER_FLAG_KEY_FRAME, null); } + @Test public void testMultiPacketConsumed() throws Exception { for (int i = 0; i < 10; i++) { data.setPosition(0); @@ -156,6 +165,7 @@ public class AdtsReaderTest extends TestCase { } } + @Test public void testAdtsDataOnly() throws ParserException { data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length); feed(); @@ -185,6 +195,4 @@ public class AdtsReaderTest extends TestCase { id3Output.assertSampleCount(id3SampleCount); adtsOutput.assertSampleCount(adtsSampleCount); } - } - diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java index 78ef05a769..798f1ce5e3 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorTest.java @@ -15,23 +15,26 @@ */ package com.google.android.exoplayer2.extractor.ts; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link PsExtractor}. - */ -public final class PsExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link PsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class PsExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new PsExtractor(); - } - }, "ts/sample.ps", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new PsExtractor(); + } + }, + "ts/sample.ps"); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java index 56668d5124..713d986d21 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java @@ -30,13 +30,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link SectionReader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class SectionReaderTest { private byte[] packetPayload; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java similarity index 79% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java index 1ea08df772..8394ed81a5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.ts; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -37,27 +36,34 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.ByteArrayOutputStream; import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link TsExtractor}. - */ -public final class TsExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link TsExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class TsExtractorTest { private static final int TS_PACKET_SIZE = 188; private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new TsExtractor(); - } - }, "ts/sample.ts", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new TsExtractor(); + } + }, + "ts/sample.ts"); } + @Test public void testIncompleteSample() throws Exception { Random random = new Random(0); - byte[] fileData = TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"); + byte[] fileData = TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts"); ByteArrayOutputStream out = new ByteArrayOutputStream(fileData.length * 2); writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); out.write(fileData, 0, TS_PACKET_SIZE * 5); @@ -69,23 +75,30 @@ public final class TsExtractorTest extends InstrumentationTestCase { writeJunkData(out, random.nextInt(TS_PACKET_SIZE - 1) + 1); fileData = out.toByteArray(); - ExtractorAsserts.assertOutput(new ExtractorFactory() { - @Override - public Extractor create() { - return new TsExtractor(); - } - }, "ts/sample.ts", fileData, getInstrumentation()); + ExtractorAsserts.assertOutput( + new ExtractorFactory() { + @Override + public Extractor create() { + return new TsExtractor(); + } + }, + "ts/sample.ts", + fileData, + RuntimeEnvironment.application); } + @Test public void testCustomPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), - factory); - FakeExtractorInput input = new FakeExtractorInput.Builder() - .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) - .setSimulateIOErrors(false) - .setSimulateUnknownLength(false) - .setSimulatePartialReads(false).build(); + TsExtractor tsExtractor = + new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample.ts")) + .setSimulateIOErrors(false) + .setSimulateUnknownLength(false) + .setSimulatePartialReads(false) + .build(); FakeExtractorOutput output = new FakeExtractorOutput(); tsExtractor.init(output); PositionHolder seekPositionHolder = new PositionHolder(); @@ -101,15 +114,18 @@ public final class TsExtractorTest extends InstrumentationTestCase { .isEqualTo(Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0)); } + @Test public void testCustomInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), - factory); - FakeExtractorInput input = new FakeExtractorInput.Builder() - .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) - .setSimulateIOErrors(false) - .setSimulateUnknownLength(false) - .setSimulatePartialReads(false).build(); + TsExtractor tsExtractor = + new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); + FakeExtractorInput input = + new FakeExtractorInput.Builder() + .setData(TestUtil.getByteArray(RuntimeEnvironment.application, "ts/sample_with_sdt.ts")) + .setSimulateIOErrors(false) + .setSimulateUnknownLength(false) + .setSimulatePartialReads(false) + .build(); tsExtractor.init(new FakeExtractorOutput()); PositionHolder seekPositionHolder = new PositionHolder(); int readResult = Extractor.RESULT_CONTINUE; @@ -165,7 +181,6 @@ public final class TsExtractorTest extends InstrumentationTestCase { return defaultFactory.createPayloadReader(streamType, esInfo); } } - } private static final class CustomEsReader implements ElementaryStreamReader { @@ -179,24 +194,22 @@ public final class TsExtractorTest extends InstrumentationTestCase { } @Override - public void seek() { - } + public void seek() {} @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN); - output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0, - language, null, 0)); + output.format( + Format.createTextSampleFormat( + idGenerator.getFormatId(), "mime", null, 0, 0, language, null, 0)); } @Override - public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - } + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {} @Override - public void consume(ParsableByteArray data) { - } + public void consume(ParsableByteArray data) {} @Override public void packetFinished() { @@ -206,7 +219,6 @@ public final class TsExtractorTest extends InstrumentationTestCase { public TrackOutput getTrackOutput() { return output; } - } private static final class SdtSectionReader implements SectionPayloadReader { @@ -214,7 +226,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { private int consumedSdts; @Override - public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, + public void init( + TimestampAdjuster timestampAdjuster, + ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { // Do nothing. } @@ -248,7 +262,5 @@ public final class TsExtractorTest extends InstrumentationTestCase { } consumedSdts++; } - } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index 36c05aa72e..e75525bb1e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -15,23 +15,26 @@ */ package com.google.android.exoplayer2.extractor.wav; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link WavExtractor}. - */ -public final class WavExtractorTest extends InstrumentationTestCase { +/** Unit test for {@link WavExtractor}. */ +@RunWith(RobolectricTestRunner.class) +public final class WavExtractorTest { + @Test public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(new ExtractorFactory() { - @Override - public Extractor create() { - return new WavExtractor(); - } - }, "wav/sample.wav", getInstrumentation()); + ExtractorAsserts.assertBehavior( + new ExtractorFactory() { + @Override + public Extractor create() { + return new WavExtractor(); + } + }, + "wav/sample.wav"); } - } 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 3a6e96b3e8..c6558e3fc9 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 @@ -23,13 +23,11 @@ import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link EventMessageDecoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class EventMessageDecoderTest { @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 f0a6d3e19b..7195548fbf 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 @@ -24,13 +24,11 @@ import java.nio.ByteBuffer; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link EventMessageEncoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class EventMessageEncoderTest { @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index 58f2b9f55d..30e1cd6c1f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -21,13 +21,11 @@ import android.os.Parcel; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link EventMessage}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class EventMessageTest { @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java index a42b71731a..714f77a752 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java @@ -21,13 +21,11 @@ import android.os.Parcel; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link ChapterFrame}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ChapterFrameTest { @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java index 9636b04e51..98a99a4219 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java @@ -21,13 +21,11 @@ import android.os.Parcel; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link ChapterTocFrame}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ChapterTocFrameTest { @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 06ce330146..4e7ae0eec0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -25,13 +25,11 @@ import java.nio.charset.Charset; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link Id3Decoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class Id3DecoderTest { private static final byte[] TAG_HEADER = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 0}; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java index 8cd90c7a64..2afe80bb0a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -28,13 +28,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link SpliceInfoDecoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class SpliceInfoDecoderTest { private SpliceInfoDecoder decoder; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java index 6b22126725..466aea3795 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -31,13 +31,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Unit tests for {@link ProgressiveDownloadAction}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class ActionFileTest { private File tempFile; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index 62f9cf3d7f..1445d33845 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -29,13 +29,11 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link ProgressiveDownloadAction}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class ProgressiveDownloadActionTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java similarity index 77% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 4d5b94e88a..1e0d8681c5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -16,10 +16,11 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -30,11 +31,16 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link ClippingMediaSource}. - */ -public final class ClippingMediaSourceTest extends InstrumentationTestCase { +/** Unit tests for {@link ClippingMediaSource}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class ClippingMediaSourceTest { private static final long TEST_PERIOD_DURATION_US = 1000000; private static final long TEST_CLIP_AMOUNT_US = 300000; @@ -42,13 +48,13 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { private Window window; private Period period; - @Override - protected void setUp() throws Exception { - super.setUp(); + @Before + public void setUp() throws Exception { window = new Timeline.Window(); period = new Timeline.Period(); } + @Test public void testNoClipping() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); @@ -62,6 +68,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { .isEqualTo(TEST_PERIOD_DURATION_US); } + @Test public void testClippingUnseekableWindowThrows() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); @@ -76,67 +83,76 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } } + @Test public void testClippingStart() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US); + Timeline clippedTimeline = + getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); } + @Test public void testClippingEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); - Timeline clippedTimeline = getClippedTimeline(timeline, 0, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + Timeline clippedTimeline = + getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); } + @Test public void testClippingStartAndEndInitial() throws IOException { // Timeline that's dynamic and not seekable. A child source might report such a timeline prior // 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); + Timeline timeline = + new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, /* isDynamic= */ true); - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + Timeline clippedTimeline = + getClippedTimeline( + timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); } + @Test public void testClippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + Timeline clippedTimeline = + getClippedTimeline( + timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); assertThat(clippedTimeline.getWindow(0, window).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); assertThat(clippedTimeline.getPeriod(0, period).getDurationUs()) .isEqualTo(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3); } + @Test public void testWindowAndPeriodIndices() throws IOException { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); - Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, - TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); + Timeline clippedTimeline = + getClippedTimeline( + timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); TimelineAsserts.assertWindowIds(clippedTimeline, 111); TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + clippedTimeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); } @@ -158,5 +174,4 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { testRunner.release(); } } - } 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 f7e29d2b06..82d7f21852 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 @@ -21,13 +21,11 @@ import com.google.android.exoplayer2.C; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link CompositeSequenceableLoader}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class CompositeSequenceableLoaderTest { /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java similarity index 70% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 5b3b684ebb..d7cf8db4bc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -28,13 +29,17 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link ConcatenatingMediaSource}. - */ -public final class ConcatenatingMediaSourceTest extends TestCase { +/** Unit tests for {@link ConcatenatingMediaSource}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class ConcatenatingMediaSourceTest { + @Test public void testEmptyConcatenation() throws IOException { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -48,17 +53,18 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testSingleMediaSource() throws IOException { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); } @@ -67,17 +73,18 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); } } + @Test public void testMultipleMediaSources() throws IOException { Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) @@ -85,20 +92,20 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); @@ -110,14 +117,14 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); @@ -125,34 +132,36 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testNestedMediaSources() throws IOException { - Timeline timeline = getConcatenatedTimeline(false, - getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), - getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); + Timeline timeline = + getConcatenatedTimeline( + false, + getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), + getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, - 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, - 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, - 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, - 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 3, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } + @Test public void testEmptyTimelineMediaSources() throws IOException { // Empty timelines in the front, back, and the middle (single and multiple in a row). Timeline[] timelines = { @@ -168,20 +177,20 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); @@ -193,14 +202,14 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); @@ -208,10 +217,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create media source with ad child source. - Timeline timelineContentOnly = new FakeTimeline( - new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineContentOnly = + new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); Timeline timelineWithAds = new FakeTimeline( new TimelineWindowDefinition( @@ -224,8 +235,8 @@ public final class ConcatenatingMediaSourceTest extends TestCase { /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, - mediaSourceWithAds); + ConcatenatingMediaSource mediaSource = + new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { @@ -246,17 +257,18 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } /** - * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns - * the concatenated timeline. + * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the + * concatenated timeline. */ - private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, - Timeline... timelines) throws IOException { + private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, 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); } - ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, - new FakeShuffleOrder(mediaSources.length), mediaSources); + ConcatenatingMediaSource mediaSource = + new ConcatenatingMediaSource( + isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline timeline = testRunner.prepareSource(); @@ -273,5 +285,4 @@ public final class ConcatenatingMediaSourceTest extends TestCase { private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId)); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java similarity index 86% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 38ac324e69..a0847bf9ff 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.verify; import android.os.ConditionVariable; @@ -23,6 +24,7 @@ import android.os.Handler; import android.os.HandlerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -33,32 +35,37 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import java.util.Arrays; -import junit.framework.TestCase; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link DynamicConcatenatingMediaSource} - */ -public final class DynamicConcatenatingMediaSourceTest extends TestCase { +/** Unit tests for {@link DynamicConcatenatingMediaSource} */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class DynamicConcatenatingMediaSourceTest { private DynamicConcatenatingMediaSource mediaSource; private MediaSourceTestRunner testRunner; - @Override + @Before public void setUp() throws Exception { - super.setUp(); mediaSource = new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); testRunner = new MediaSourceTestRunner(mediaSource, null); } - @Override + @After public void tearDown() throws Exception { - super.tearDown(); testRunner.release(); } - public void testPlaylistChangesAfterPreparation() throws IOException { + @Test + public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -89,8 +96,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); // Add bulk. - mediaSource.addMediaSources(3, Arrays.asList(childSources[4], childSources[5], - childSources[6])); + mediaSource.addMediaSources( + 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); @@ -129,22 +136,22 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); @@ -174,7 +181,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() throws IOException { + @Test + public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { FakeMediaSource[] childSources = createMediaSources(4); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -188,14 +196,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); testRunner.assertPrepareAndReleaseAllPeriods(); mediaSource.releaseSource(); @@ -204,7 +212,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testPlaylistWithLazyMediaSource() throws IOException { + @Test + public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); @@ -230,12 +239,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Trigger source info refresh for lazy source and check that the timeline now contains all // information for all windows. - testRunner.runOnPlaybackThread(new Runnable() { - @Override - public void run() { - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - } - }); + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); @@ -259,8 +269,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not // called yet. MediaPeriod lazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); - ConditionVariable preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); - assertThat(preparedCondition.block(1)).isFalse(); + 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(0)); @@ -268,17 +278,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // 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(new Runnable() { - @Override - public void run() { - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - } - }); + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); - assertThat(preparedCondition.block(1)).isTrue(); + assertThat(preparedCondition.getCount()).isEqualTo(0); // Release the period and source. testRunner.releasePeriod(lazyPeriod); @@ -293,7 +304,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() throws IOException { + @Test + public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -322,20 +334,20 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, - C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); @@ -345,6 +357,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { testRunner.assertPrepareAndReleaseAllPeriods(); } + @Test public void testDynamicChangeOfEmptyTimelines() throws IOException { FakeMediaSource[] childSources = new FakeMediaSource[] { @@ -371,6 +384,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); } + @Test public void testIllegalArguments() { MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); @@ -382,7 +396,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Expected. } - MediaSource[] mediaSources = { validSource, null }; + MediaSource[] mediaSources = {validSource, null}; try { mediaSource.addMediaSources(Arrays.asList(mediaSources)); fail("Null mediaSource not allowed."); @@ -399,8 +413,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { // Expected. } - mediaSources = new MediaSource[] { - new FakeMediaSource(createFakeTimeline(2), null), validSource }; + mediaSources = + new MediaSource[] {new FakeMediaSource(createFakeTimeline(2), null), validSource}; try { mediaSource.addMediaSources(Arrays.asList(mediaSources)); fail("Duplicate mediaSource not allowed."); @@ -409,6 +423,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackBeforePreparationAddSingle() { Runnable runnable = Mockito.mock(Runnable.class); @@ -416,14 +431,17 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } + @Test public void testCustomCallbackBeforePreparationAddMultiple() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); verify(runnable).run(); } + @Test public void testCustomCallbackBeforePreparationAddSingleWithIndex() { Runnable runnable = Mockito.mock(Runnable.class); @@ -431,15 +449,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } + @Test public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(/* index */ 0, + mediaSource.addMediaSources( + /* index */ 0, Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); verify(runnable).run(); } + @Test public void testCustomCallbackBeforePreparationRemove() { Runnable runnable = Mockito.mock(Runnable.class); @@ -448,6 +469,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } + @Test public void testCustomCallbackBeforePreparationMove() { Runnable runnable = Mockito.mock(Runnable.class); @@ -457,17 +479,19 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } + @Test public void testCustomCallbackAfterPreparationAddSingle() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread(new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -475,6 +499,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { @@ -497,17 +522,19 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread(new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(1); } finally { @@ -515,6 +542,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { @@ -538,25 +566,28 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackAfterPreparationRemove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); - dummyMainThread.runOnMainThread(new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource()); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); testRunner.assertTimelineChangeBlocking(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread(new Runnable() { - @Override - public void run() { - mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(0); } finally { @@ -564,6 +595,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testCustomCallbackAfterPreparationMove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { @@ -580,13 +612,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { testRunner.assertTimelineChangeBlocking(); final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread(new Runnable() { - @Override - public void run() { - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, - timelineGrabber); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); + } + }); Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); assertThat(timeline.getWindowCount()).isEqualTo(2); } finally { @@ -594,10 +626,12 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } + @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create dynamic media source with ad child source. - Timeline timelineContentOnly = new FakeTimeline( - new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); + Timeline timelineContentOnly = + new FakeTimeline( + new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); Timeline timelineWithAds = new FakeTimeline( new TimelineWindowDefinition( @@ -628,6 +662,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } + @Test public void testAtomicTimelineWindowOrder() throws IOException { // Release default test runner with non-atomic media source and replace with new test runner. testRunner.release(); @@ -668,6 +703,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); } + @Test public void testNestedTimeline() throws IOException { DynamicConcatenatingMediaSource nestedSource1 = new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); @@ -714,6 +750,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); } + @Test public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); @@ -760,20 +797,20 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { */ public void runOnMainThread(final Runnable runnable) { final ConditionVariable finishedCondition = new ConditionVariable(); - handler.post(new Runnable() { - @Override - public void run() { - runnable.run(); - finishedCondition.open(); - } - }); + handler.post( + new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); } public void release() { thread.quit(); } - } private static final class TimelineGrabber implements Runnable { @@ -806,7 +843,5 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } return timeline; } - } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 68a9e7676c..f0b4772422 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -24,77 +25,87 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link LoopingMediaSource}. - */ -public class LoopingMediaSourceTest extends TestCase { +/** Unit tests for {@link LoopingMediaSource}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class LoopingMediaSourceTest { private FakeTimeline multiWindowTimeline; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111), - new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); + multiWindowTimeline = + new FakeTimeline( + new TimelineWindowDefinition(1, 111), + new TimelineWindowDefinition(1, 222), + new TimelineWindowDefinition(1, 333)); } + @Test public void testSingleLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); } } + @Test public void testMultiLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 1, 2, 3, 4, 5, 6, 7, 8, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 8, 0, 1, 2, 3, 4, 5, 6, 7); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 3, 4, 5, 6, 7, 8, 0); } } + @Test public void testInfiniteLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, - 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, - 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, - 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, shuffled, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 0); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); } } + @Test public void testEmptyTimelineLoop() throws IOException { Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); TimelineAsserts.assertEmpty(timeline); @@ -107,8 +118,7 @@ public class LoopingMediaSourceTest extends TestCase { } /** - * Wraps the specified timeline in a {@link LoopingMediaSource} and returns - * the looping timeline. + * 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); @@ -123,5 +133,4 @@ public class LoopingMediaSourceTest extends TestCase { testRunner.release(); } } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java similarity index 76% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java index 6c048db09d..b03a76c23e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -16,8 +16,10 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -25,29 +27,33 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link MergingMediaSource}. - */ -public class MergingMediaSourceTest extends TestCase { +/** Unit tests for {@link MergingMediaSource}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class MergingMediaSourceTest { + @Test public void testMergingDynamicTimelines() throws IOException { - FakeTimeline firstTimeline = new FakeTimeline( - new TimelineWindowDefinition(true, true, C.TIME_UNSET)); - FakeTimeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + FakeTimeline firstTimeline = + new FakeTimeline(new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + FakeTimeline secondTimeline = + new FakeTimeline(new TimelineWindowDefinition(true, true, C.TIME_UNSET)); testMergingMediaSourcePrepare(firstTimeline, secondTimeline); } + @Test public void testMergingStaticTimelines() throws IOException { - FakeTimeline firstTimeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 20)); - FakeTimeline secondTimeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 10)); + FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 20)); + FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 10)); testMergingMediaSourcePrepare(firstTimeline, secondTimeline); } + @Test public void testMergingTimelinesWithDifferentPeriodCounts() throws IOException { FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(1, null)); FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(2, null)); @@ -82,5 +88,4 @@ public class MergingMediaSourceTest extends TestCase { testRunner.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 6030238131..4d6b6dd72d 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 @@ -36,13 +36,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Test for {@link SampleQueue}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class SampleQueueTest { private static final int ALLOCATION_SIZE = 16; 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 1229e47883..e15c8f0aaa 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 @@ -24,13 +24,11 @@ import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link ShuffleOrder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ShuffleOrderTest { public static final long RANDOM_SEED = 1234567890L; 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 94ca8b03f0..2627052cc5 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 @@ -25,13 +25,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link SinglePeriodTimeline}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class SinglePeriodTimelineTest { private Window window; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java index ca8bf5d393..a8cc04473d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ads/AdPlaybackStateTest.java @@ -24,11 +24,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** Unit test for {@link AdPlaybackState}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class AdPlaybackStateTest { private static final long[] TEST_AD_GROUP_TMES_US = new long[] {0, C.msToUs(10_000)}; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java similarity index 83% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java index 49d05e993f..746066cb35 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ssa/SsaDecoderTest.java @@ -17,15 +17,17 @@ package com.google.android.exoplayer2.text.ssa; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.util.ArrayList; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link SsaDecoder}. - */ -public final class SsaDecoderTest extends InstrumentationTestCase { +/** Unit test for {@link SsaDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public final class SsaDecoderTest { private static final String EMPTY = "ssa/empty"; private static final String TYPICAL = "ssa/typical"; @@ -35,18 +37,20 @@ public final class SsaDecoderTest extends InstrumentationTestCase { 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 { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, EMPTY); SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); } + @Test public void testDecodeTypical() throws IOException { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL); SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -55,14 +59,15 @@ public final class SsaDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 4); } + @Test public void testDecodeTypicalWithInitializationData() throws IOException { - byte[] headerBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_HEADER_ONLY); - byte[] formatBytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FORMAT_ONLY); + byte[] headerBytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_HEADER_ONLY); + byte[] formatBytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_FORMAT_ONLY); ArrayList initializationData = new ArrayList<>(); initializationData.add(formatBytes); initializationData.add(headerBytes); SsaDecoder decoder = new SsaDecoder(initializationData); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_DIALOGUE_ONLY); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_DIALOGUE_ONLY); SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -71,19 +76,21 @@ public final class SsaDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 4); } + @Test public void testDecodeInvalidTimecodes() throws IOException { // Parsing should succeed, parsing the third cue only. SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), INVALID_TIMECODES); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, INVALID_TIMECODES); SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); } + @Test public void testDecodeNoEndTimecodes() throws IOException { SsaDecoder decoder = new SsaDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, NO_END_TIMECODES); SsaSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -121,5 +128,4 @@ public final class SsaDecoderTest extends InstrumentationTestCase { .isEqualTo("This is the third subtitle, with a comma."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8900000); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java similarity index 84% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index c810e3590d..e9abaca075 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -17,14 +17,16 @@ package com.google.android.exoplayer2.text.subrip; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link SubripDecoder}. - */ -public final class SubripDecoderTest extends InstrumentationTestCase { +/** Unit test for {@link SubripDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public final class SubripDecoderTest { private static final String EMPTY_FILE = "subrip/empty"; private static final String TYPICAL_FILE = "subrip/typical"; @@ -36,18 +38,20 @@ public final class SubripDecoderTest extends InstrumentationTestCase { private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; + @Test public void testDecodeEmpty() throws IOException { SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, EMPTY_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(0); assertThat(subtitle.getCues(0).isEmpty()).isTrue(); } + @Test public void testDecodeTypical() throws IOException { SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_FILE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -56,9 +60,11 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 4); } + @Test public void testDecodeTypicalWithByteOrderMark() throws IOException { SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_WITH_BYTE_ORDER_MARK); + byte[] bytes = + TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_WITH_BYTE_ORDER_MARK); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -67,9 +73,10 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 4); } + @Test public void testDecodeTypicalExtraBlankLine() throws IOException { SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_EXTRA_BLANK_LINE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_EXTRA_BLANK_LINE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -78,10 +85,11 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 4); } + @Test public void testDecodeTypicalMissingTimecode() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_TIMECODE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_MISSING_TIMECODE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -89,10 +97,11 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 2); } + @Test public void testDecodeTypicalMissingSequence() throws IOException { // Parsing should succeed, parsing the first and third cues only. SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_MISSING_SEQUENCE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_MISSING_SEQUENCE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -100,20 +109,23 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 2); } + @Test public void testDecodeTypicalNegativeTimestamps() throws IOException { // Parsing should succeed, parsing the third cue only. SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS); + byte[] bytes = + TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_NEGATIVE_TIMESTAMPS); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); assertTypicalCue3(subtitle, 0); } + @Test public void testDecodeTypicalUnexpectedEnd() throws IOException { // Parsing should succeed, parsing the first and second cues only. SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_UNEXPECTED_END); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, TYPICAL_UNEXPECTED_END); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -121,9 +133,10 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue2(subtitle, 2); } + @Test public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, NO_END_TIMECODES_FILE); SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getEventTimeCount()).isEqualTo(3); @@ -161,5 +174,4 @@ public final class SubripDecoderTest extends InstrumentationTestCase { .isEqualTo("This is the third subtitle."); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8901000); } - } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java similarity index 83% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java index 95f0dfe3c8..fdf454e5df 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlDecoderTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.text.ttml; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.test.InstrumentationTestCase; import android.text.Layout; import android.text.Spannable; import android.text.SpannableStringBuilder; @@ -38,11 +37,14 @@ import com.google.android.exoplayer2.util.ColorParser; import java.io.IOException; import java.util.List; import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link TtmlDecoder}. - */ -public final class TtmlDecoderTest extends InstrumentationTestCase { +/** Unit test for {@link TtmlDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public final class TtmlDecoderTest { private static final String INLINE_ATTRIBUTES_TTML_FILE = "ttml/inline_style_attributes.xml"; private static final String INHERIT_STYLE_TTML_FILE = "ttml/inherit_style.xml"; @@ -62,6 +64,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { private static final String FONT_SIZE_EMPTY_TTML_FILE = "ttml/font_size_empty.xml"; private static final String FRAME_RATE_TTML_FILE = "ttml/frame_rate.xml"; + @Test public void testInlineAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -78,82 +81,182 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(firstPStyle.isUnderline()).isTrue(); } + @Test public void testInheritInlineAttributes() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, - 0xFF00FFFF, ColorParser.parseTtmlColor("lime"), false, true, null); + assertSpans( + subtitle, + 20, + "text 2", + "sansSerif", + TtmlStyle.STYLE_ITALIC, + 0xFF00FFFF, + ColorParser.parseTtmlColor("lime"), + false, + true, + null); } /** - * Regression test for devices on JellyBean where some named colors are not correctly defined - * on framework level. Tests that lime resolves to #FF00FF00 not - * #00FF00. + * Regression test for devices on JellyBean where some named colors are not correctly defined on + * framework level. Tests that lime resolves to #FF00FF00 not #00FF00 + * . * - * @see - * JellyBean Color - * + * @see + * JellyBean Color * Kitkat Color * @throws IOException thrown if reading subtitle file fails. */ + @Test public void testLime() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INLINE_ATTRIBUTES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, 0xFF00FFFF, 0xFF00FF00, - false, true, null); + assertSpans( + subtitle, + 20, + "text 2", + "sansSerif", + TtmlStyle.STYLE_ITALIC, + 0xFF00FFFF, + 0xFF00FF00, + false, + true, + null); } + @Test public void testInheritGlobalStyle() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); - assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF, - 0xFFFFFF00, true, false, null); + assertSpans( + subtitle, + 10, + "text 1", + "serif", + TtmlStyle.STYLE_BOLD_ITALIC, + 0xFF0000FF, + 0xFFFFFF00, + true, + false, + null); } - public void testInheritGlobalStyleOverriddenByInlineAttributes() throws IOException, - SubtitleDecoderException { + @Test + public void testInheritGlobalStyleOverriddenByInlineAttributes() + throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_STYLE_OVERRIDE_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans(subtitle, 10, "text 1", "serif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF, - 0xFFFFFF00, true, false, null); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_ITALIC, 0xFFFF0000, 0xFFFFFF00, - true, false, null); + assertSpans( + subtitle, + 10, + "text 1", + "serif", + TtmlStyle.STYLE_BOLD_ITALIC, + 0xFF0000FF, + 0xFFFFFF00, + true, + false, + null); + assertSpans( + subtitle, + 20, + "text 2", + "sansSerif", + TtmlStyle.STYLE_ITALIC, + 0xFFFF0000, + 0xFFFFFF00, + true, + false, + null); } + @Test public void testInheritGlobalAndParent() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_GLOBAL_AND_PARENT_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); - assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_NORMAL, 0xFFFF0000, - ColorParser.parseTtmlColor("lime"), false, true, Layout.Alignment.ALIGN_CENTER); - assertSpans(subtitle, 20, "text 2", "serif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF, - 0xFFFFFF00, true, true, Layout.Alignment.ALIGN_CENTER); + assertSpans( + subtitle, + 10, + "text 1", + "sansSerif", + TtmlStyle.STYLE_NORMAL, + 0xFFFF0000, + ColorParser.parseTtmlColor("lime"), + false, + true, + Layout.Alignment.ALIGN_CENTER); + assertSpans( + subtitle, + 20, + "text 2", + "serif", + TtmlStyle.STYLE_BOLD_ITALIC, + 0xFF0000FF, + 0xFFFFFF00, + true, + true, + Layout.Alignment.ALIGN_CENTER); } + @Test public void testInheritMultipleStyles() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans(subtitle, 10, "text 1", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF, - 0xFFFFFF00, false, true, null); + assertSpans( + subtitle, + 10, + "text 1", + "sansSerif", + TtmlStyle.STYLE_BOLD_ITALIC, + 0xFF0000FF, + 0xFFFFFF00, + false, + true, + null); } - public void testInheritMultipleStylesWithoutLocalAttributes() throws IOException, - SubtitleDecoderException { + @Test + public void testInheritMultipleStylesWithoutLocalAttributes() + throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans(subtitle, 20, "text 2", "sansSerif", TtmlStyle.STYLE_BOLD_ITALIC, 0xFF0000FF, - 0xFF000000, false, true, null); + assertSpans( + subtitle, + 20, + "text 2", + "sansSerif", + TtmlStyle.STYLE_BOLD_ITALIC, + 0xFF0000FF, + 0xFF000000, + false, + true, + null); } - public void testMergeMultipleStylesWithParentStyle() throws IOException, - SubtitleDecoderException { + @Test + public void testMergeMultipleStylesWithParentStyle() + throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); - assertSpans(subtitle, 30, "text 2.5", "sansSerifInline", TtmlStyle.STYLE_ITALIC, 0xFFFF0000, - 0xFFFFFF00, true, true, null); + assertSpans( + subtitle, + 30, + "text 2.5", + "sansSerifInline", + TtmlStyle.STYLE_ITALIC, + 0xFFFF0000, + 0xFFFFFF00, + true, + true, + null); } + @Test public void testMultipleRegions() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(MULTIPLE_REGIONS_TTML_FILE); List output = subtitle.getCues(1000000); @@ -208,6 +311,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(ttmlCue.line).isEqualTo(45f / 100f); } + @Test public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); @@ -219,6 +323,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(queryChildrenForTag(fourthDiv, TtmlNode.TAG_P, 0).getStyleIds()).isNull(); } + @Test public void testNonexistingStyleId() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); @@ -230,8 +335,9 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(queryChildrenForTag(fifthDiv, TtmlNode.TAG_P, 0).getStyleIds()).hasLength(1); } - public void testNonExistingAndExistingStyleIdWithRedundantSpaces() throws IOException, - SubtitleDecoderException { + @Test + public void testNonExistingAndExistingStyleIdWithRedundantSpaces() + throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(INHERIT_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(12); @@ -243,6 +349,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(styleIds).hasLength(2); } + @Test public void testMultipleChaining() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(CHAIN_MULTIPLE_STYLES_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); @@ -265,6 +372,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(style.isLinethrough()).isTrue(); } + @Test public void testNoUnderline() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -279,6 +387,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { .isFalse(); } + @Test public void testNoLinethrough() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(NO_UNDERLINE_LINETHROUGH_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -293,6 +402,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { .isFalse(); } + @Test public void testFontSizeSpans() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(10); @@ -328,6 +438,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertRelativeFontSize(spannable, 0.5f); } + @Test public void testFontSizeWithMissingUnitIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_MISSING_UNIT_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); @@ -339,6 +450,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); } + @Test public void testFontSizeWithInvalidValueIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_INVALID_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(6); @@ -365,6 +477,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); } + @Test public void testFontSizeWithEmptyValueIsIgnored() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FONT_SIZE_EMPTY_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(2); @@ -376,6 +489,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat(spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class)).hasLength(0); } + @Test public void testFrameRate() throws IOException, SubtitleDecoderException { TtmlSubtitle subtitle = getSubtitle(FRAME_RATE_TTML_FILE); assertThat(subtitle.getEventTimeCount()).isEqualTo(4); @@ -385,12 +499,19 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertThat((double) subtitle.getEventTime(3)).isWithin(2000).of(2_002_000_000); } - private void assertSpans(TtmlSubtitle subtitle, int second, - String text, String font, int fontStyle, - int backgroundColor, int color, boolean isUnderline, - boolean isLinethrough, Layout.Alignment alignment) { + private void assertSpans( + TtmlSubtitle subtitle, + int second, + String text, + String font, + int fontStyle, + int backgroundColor, + int color, + boolean isUnderline, + boolean isLinethrough, + Layout.Alignment alignment) { - long timeUs = second * 1000000; + long timeUs = second * 1000000L; List cues = subtitle.getCues(timeUs); assertThat(cues).hasSize(1); @@ -409,15 +530,15 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { } private void assertAbsoluteFontSize(Spannable spannable, int absoluteFontSize) { - AbsoluteSizeSpan[] absoluteSizeSpans = spannable.getSpans(0, spannable.length(), - AbsoluteSizeSpan.class); + AbsoluteSizeSpan[] absoluteSizeSpans = + spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class); assertThat(absoluteSizeSpans).hasLength(1); assertThat(absoluteSizeSpans[0].getSize()).isEqualTo(absoluteFontSize); } private void assertRelativeFontSize(Spannable spannable, float relativeFontSize) { - RelativeSizeSpan[] relativeSizeSpans = spannable.getSpans(0, spannable.length(), - RelativeSizeSpan.class); + RelativeSizeSpan[] relativeSizeSpans = + spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class); assertThat(relativeSizeSpans).hasLength(1); assertThat(relativeSizeSpans[0].getSizeChange()).isEqualTo(relativeFontSize); } @@ -440,8 +561,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { } private void assertStrikethrough(Spannable spannable, boolean isStrikethrough) { - StrikethroughSpan[] striketroughSpans = spannable.getSpans(0, spannable.length(), - StrikethroughSpan.class); + StrikethroughSpan[] striketroughSpans = + spannable.getSpans(0, spannable.length(), StrikethroughSpan.class); assertWithMessage(isStrikethrough ? "must be strikethrough" : "must not be strikethrough") .that(striketroughSpans) .hasLength(isStrikethrough ? 1 : 0); @@ -491,8 +612,7 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { private TtmlSubtitle getSubtitle(String file) throws IOException, SubtitleDecoderException { TtmlDecoder ttmlDecoder = new TtmlDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), file); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, file); return ttmlDecoder.decode(bytes, bytes.length, false); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 557611c4ea..536ddbabbc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -29,13 +29,11 @@ import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link TtmlRenderUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class TtmlRenderUtilTest { @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java index 4c35e259ff..aa46584f54 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -30,11 +30,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** Unit test for {@link TtmlStyle}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class TtmlStyleTest { private static final String FONT_FAMILY = "serif"; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java similarity index 84% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java index c5c0df61da..c0fa52f74b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/tx3g/Tx3gDecoderTest.java @@ -16,10 +16,10 @@ package com.google.android.exoplayer2.text.tx3g; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.graphics.Color; import android.graphics.Typeface; -import android.test.InstrumentationTestCase; import android.text.SpannedString; import android.text.style.ForegroundColorSpan; import android.text.style.StyleSpan; @@ -32,11 +32,14 @@ import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import java.io.IOException; import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link Tx3gDecoder}. - */ -public final class Tx3gDecoderTest extends InstrumentationTestCase { +/** Unit test for {@link Tx3gDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public final class Tx3gDecoderTest { private static final String NO_SUBTITLE = "tx3g/no_subtitle"; private static final String SAMPLE_JUST_TEXT = "tx3g/sample_just_text"; @@ -50,16 +53,18 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { private static final String INITIALIZATION = "tx3g/initialization"; private static final String INITIALIZATION_ALL_DEFAULTS = "tx3g/initialization_all_defaults"; + @Test public void testDecodeNoSubtitle() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_SUBTITLE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, NO_SUBTITLE); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); assertThat(subtitle.getCues(0)).isEmpty(); } + @Test public void testDecodeJustText() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_JUST_TEXT); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_JUST_TEXT); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -67,9 +72,10 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeWithStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -82,9 +88,11 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeWithStylAllDefaults() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL_ALL_DEFAULTS); + byte[] bytes = + TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL_ALL_DEFAULTS); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -92,9 +100,10 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeUtf16BeNoStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_BE_NO_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_UTF16_BE_NO_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("你好"); @@ -102,9 +111,10 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeUtf16LeNoStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_UTF16_LE_NO_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_UTF16_LE_NO_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("你好"); @@ -112,9 +122,10 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeWithMultipleStyl() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_MULTIPLE_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_MULTIPLE_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("Line 2\nLine 3"); @@ -129,9 +140,11 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testDecodeWithOtherExtension() throws IOException, SubtitleDecoderException { Tx3gDecoder decoder = new Tx3gDecoder(Collections.emptyList()); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_OTHER_EXTENSION); + byte[] bytes = + TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_OTHER_EXTENSION); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -143,10 +156,11 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } + @Test public void testInitializationDecodeWithStyl() throws IOException, SubtitleDecoderException { - byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION); + byte[] initBytes = TestUtil.getByteArray(RuntimeEnvironment.application, INITIALIZATION); Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -163,10 +177,11 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1f); } + @Test public void testInitializationDecodeWithTbox() throws IOException, SubtitleDecoderException { - byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION); + byte[] initBytes = TestUtil.getByteArray(RuntimeEnvironment.application, INITIALIZATION); Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_TBOX); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_TBOX); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -181,11 +196,13 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.1875f); } - public void testInitializationAllDefaultsDecodeWithStyl() throws IOException, - SubtitleDecoderException { - byte[] initBytes = TestUtil.getByteArray(getInstrumentation(), INITIALIZATION_ALL_DEFAULTS); + @Test + public void testInitializationAllDefaultsDecodeWithStyl() + throws IOException, SubtitleDecoderException { + byte[] initBytes = + TestUtil.getByteArray(RuntimeEnvironment.application, INITIALIZATION_ALL_DEFAULTS); Tx3gDecoder decoder = new Tx3gDecoder(Collections.singletonList(initBytes)); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), SAMPLE_WITH_STYL); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, SAMPLE_WITH_STYL); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); SpannedString text = new SpannedString(subtitle.getCues(0).get(0).text); assertThat(text.toString()).isEqualTo("CC Test"); @@ -198,8 +215,8 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertFractionalLinePosition(subtitle.getCues(0).get(0), 0.85f); } - private static T findSpan(SpannedString testObject, int expectedStart, int expectedEnd, - Class expectedType) { + private static T findSpan( + SpannedString testObject, int expectedStart, int expectedEnd, Class expectedType) { T[] spans = testObject.getSpans(0, testObject.length(), expectedType); for (T span : spans) { if (testObject.getSpanStart(span) == expectedStart @@ -216,5 +233,4 @@ public final class Tx3gDecoderTest extends InstrumentationTestCase { assertThat(cue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_START); assertThat(Math.abs(expectedFraction - cue.line) < 1e-6).isTrue(); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index 6ade85be28..b81c0c68c3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -24,13 +24,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link CssParser}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class CssParserTest { private CssParser parser; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 8937007990..f0c426ea65 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -25,13 +25,11 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link Mp4WebvttDecoder}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class Mp4WebvttDecoderTest { private static final byte[] SINGLE_CUE_SAMPLE = { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index 2a6e461627..b89eb47618 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -27,13 +27,11 @@ import java.util.Collections; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttCueParser}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class WebvttCueParserTest { @Test diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java similarity index 75% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java index 3d5e62de76..eec985ef5b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java @@ -16,9 +16,9 @@ package com.google.android.exoplayer2.text.webvtt; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.graphics.Typeface; -import android.test.InstrumentationTestCase; import android.text.Layout.Alignment; import android.text.Spanned; import android.text.style.BackgroundColorSpan; @@ -31,11 +31,14 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SubtitleDecoderException; import java.io.IOException; import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit test for {@link WebvttDecoder}. - */ -public class WebvttDecoderTest extends InstrumentationTestCase { +/** Unit test for {@link WebvttDecoder}. */ +@RunWith(RobolectricTestRunner.class) +public class WebvttDecoderTest { private static final String TYPICAL_FILE = "webvtt/typical"; private static final String TYPICAL_WITH_BAD_TIMESTAMPS = "webvtt/typical_with_bad_timestamps"; @@ -48,9 +51,10 @@ public class WebvttDecoderTest extends InstrumentationTestCase { private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors"; private static final String EMPTY_FILE = "webvtt/empty"; + @Test public void testDecodeEmpty() throws IOException { WebvttDecoder decoder = new WebvttDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, EMPTY_FILE); try { decoder.decode(bytes, bytes.length, false); fail(); @@ -59,6 +63,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { } } + @Test public void testDecodeTypical() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE); @@ -70,6 +75,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } + @Test public void testDecodeTypicalWithBadTimestamps() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS); @@ -81,6 +87,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } + @Test public void testDecodeTypicalWithIds() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE); @@ -92,6 +99,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } + @Test public void testDecodeTypicalWithComments() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE); @@ -103,6 +111,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle."); } + @Test public void testDecodeWithTags() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE); @@ -116,30 +125,93 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 6, 6000000, 7000000, "This is the &subtitle."); } + @Test public void testDecodeWithPositioning() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE); // Test event count. assertThat(subtitle.getEventTimeCount()).isEqualTo(12); // Test cues. - assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL, - Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f); - assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.", - Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, - Cue.TYPE_UNSET, 0.35f); - assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.", - Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET, - Cue.TYPE_UNSET, 0.35f); - assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.", - Alignment.ALIGN_CENTER, -11f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, - Cue.TYPE_UNSET, Cue.DIMEN_UNSET); - assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.", - Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, - Cue.ANCHOR_TYPE_END, 0.1f); - assertCue(subtitle, 10, 10000000, 11000000, "This is the sixth subtitle.", - Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET, - Cue.TYPE_UNSET, 0.35f); + assertCue( + subtitle, + 0, + 0, + 1234000, + "This is the first subtitle.", + Alignment.ALIGN_NORMAL, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.TYPE_UNSET, + 0.1f, + Cue.ANCHOR_TYPE_START, + 0.35f); + assertCue( + subtitle, + 2, + 2345000, + 3456000, + "This is the second subtitle.", + Alignment.ALIGN_OPPOSITE, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + 0.35f); + assertCue( + subtitle, + 4, + 4000000, + 5000000, + "This is the third subtitle.", + Alignment.ALIGN_CENTER, + 0.45f, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_END, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + 0.35f); + assertCue( + subtitle, + 6, + 6000000, + 7000000, + "This is the fourth subtitle.", + Alignment.ALIGN_CENTER, + -11f, + Cue.LINE_TYPE_NUMBER, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET); + assertCue( + subtitle, + 8, + 7000000, + 8000000, + "This is the fifth subtitle.", + Alignment.ALIGN_OPPOSITE, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.TYPE_UNSET, + 0.1f, + Cue.ANCHOR_TYPE_END, + 0.1f); + assertCue( + subtitle, + 10, + 10000000, + 11000000, + "This is the sixth subtitle.", + Alignment.ALIGN_CENTER, + 0.45f, + Cue.LINE_TYPE_FRACTION, + Cue.ANCHOR_TYPE_END, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + 0.35f); } + @Test public void testDecodeWithBadCueHeader() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE); @@ -151,6 +223,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle."); } + @Test public void testWebvttWithCssStyle() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES); @@ -175,6 +248,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { .isEqualTo(Typeface.BOLD); } + @Test public void testWithComplexCssSelectors() throws IOException, SubtitleDecoderException { WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS); Spanned text = getUniqueSpanTextAt(subtitle, 0); @@ -211,10 +285,10 @@ public class WebvttDecoderTest extends InstrumentationTestCase { .isEqualTo(Typeface.ITALIC); } - private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException, - SubtitleDecoderException { + private WebvttSubtitle getSubtitleForTestAsset(String asset) + throws IOException, SubtitleDecoderException { WebvttDecoder decoder = new WebvttDecoder(); - byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset); + byte[] bytes = TestUtil.getByteArray(RuntimeEnvironment.application, asset); return decoder.decode(bytes, bytes.length, false); } @@ -222,15 +296,36 @@ public class WebvttDecoderTest extends InstrumentationTestCase { return (Spanned) sub.getCues(timeUs).get(0).text; } - private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, - int endTimeUs, String text) { - assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET, - Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + private static void assertCue( + WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, int endTimeUs, String text) { + assertCue( + subtitle, + eventTimeIndex, + startTimeUs, + endTimeUs, + text, + null, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET, + Cue.TYPE_UNSET, + Cue.DIMEN_UNSET); } - private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs, - int endTimeUs, String text, Alignment textAlignment, float line, int lineType, int lineAnchor, - float position, int positionAnchor, float size) { + private static void assertCue( + WebvttSubtitle subtitle, + int eventTimeIndex, + long startTimeUs, + int endTimeUs, + String text, + Alignment textAlignment, + float line, + int lineType, + int lineAnchor, + float position, + int positionAnchor, + float size) { assertThat(subtitle.getEventTime(eventTimeIndex)).isEqualTo(startTimeUs); assertThat(subtitle.getEventTime(eventTimeIndex + 1)).isEqualTo(endTimeUs); List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex)); @@ -246,5 +341,4 @@ public class WebvttDecoderTest extends InstrumentationTestCase { assertThat(cue.positionAnchor).isEqualTo(positionAnchor); assertThat(cue.size).isEqualTo(size); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index c3c30e44a8..3074f28b64 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -26,13 +26,11 @@ import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttSubtitle}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class WebvttSubtitleTest { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; 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 ea19c72826..956174f43b 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 @@ -38,11 +38,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** Unit test for {@link AdaptiveTrackSelection}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class AdaptiveTrackSelectionTest { @Mock private BandwidthMeter mockBandwidthMeter; 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 24362d1570..4b2a3a5ad6 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 @@ -25,13 +25,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link DefaultTrackSelector}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DefaultTrackSelectorTest { private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = 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 b9ea0087c7..b80110365c 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 @@ -30,13 +30,11 @@ import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link MappingTrackSelector}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class MappingTrackSelectorTest { private static final RendererCapabilities VIDEO_CAPABILITIES = diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java similarity index 58% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index d582d25ab1..7ffc14d51f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -16,28 +16,37 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.testutil.TestUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit tests for {@link AssetDataSource}. - */ -public final class AssetDataSourceTest extends InstrumentationTestCase { +/** Unit tests for {@link AssetDataSource}. */ +@RunWith(RobolectricTestRunner.class) +public final class AssetDataSourceTest { private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + @Test public void testReadFileUri() throws Exception { - AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + AssetDataSource dataSource = new AssetDataSource(RuntimeEnvironment.application); DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); - TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); + TestUtil.assertDataSourceContent( + dataSource, + dataSpec, + TestUtil.getByteArray(RuntimeEnvironment.application, DATA_PATH), + true); } + @Test public void testReadAssetUri() throws Exception { - AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + AssetDataSource dataSource = new AssetDataSource(RuntimeEnvironment.application); DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); - TestUtil.assertDataSourceContent(dataSource, dataSpec, - TestUtil.getByteArray(getInstrumentation(), DATA_PATH), true); + TestUtil.assertDataSourceContent( + dataSource, + dataSpec, + TestUtil.getByteArray(RuntimeEnvironment.application, DATA_PATH), + true); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index a72d060287..f04f01bd5f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -23,13 +23,11 @@ import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link ByteArrayDataSource}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ByteArrayDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 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 85c4341232..49f865e2b5 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 @@ -27,13 +27,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSchemeDataSource}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DataSchemeDataSourceTest { private DataSource schemeDataDataSource; 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 8cd6c23fb1..f47cfc4469 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 @@ -25,13 +25,11 @@ import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSourceInputStream}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DataSourceInputStreamTest { private static final byte[] TEST_DATA = TestUtil.buildTestData(16); 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 67fae69b44..09be138abe 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 @@ -37,13 +37,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Unit tests for {@link CacheDataSource}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java index 3b8276c731..1e6febd8a9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -39,13 +39,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Additional tests for {@link CacheDataSource}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class CacheDataSourceTest2 { private static final String EXO_CACHE_DIR = "exo"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index 250e09bab4..7237ecd50d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -44,13 +44,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Tests {@link CacheUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class CacheUtilTest { /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java index 0b0556e513..50f9cd2ae8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java @@ -20,29 +20,37 @@ import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.when; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; -import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.TreeSet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Tests for {@link CachedRegionTracker}. - */ -public final class CachedRegionTrackerTest extends InstrumentationTestCase { +/** Tests for {@link CachedRegionTracker}. */ +@RunWith(RobolectricTestRunner.class) +public final class CachedRegionTrackerTest { private static final String CACHE_KEY = "abc"; private static final long MS_IN_US = 1000; // 5 chunks, each 20 bytes long and 100 ms long. - private static final ChunkIndex CHUNK_INDEX = new ChunkIndex( - new int[] {20, 20, 20, 20, 20}, - new long[] {100, 120, 140, 160, 180}, - new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US}, - new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US}); + private static final ChunkIndex CHUNK_INDEX = + new ChunkIndex( + new int[] {20, 20, 20, 20, 20}, + new long[] {100, 120, 140, 160, 180}, + new long[] { + 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US + }, + new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US}); @Mock private Cache cache; private CachedRegionTracker tracker; @@ -50,68 +58,59 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { private CachedContentIndex index; private File cacheDir; - @Override - protected void setUp() throws Exception { - super.setUp(); - MockitoUtil.setUpMockito(this); + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); when(cache.addListener(anyString(), any(Cache.Listener.class))) .thenReturn(new TreeSet()); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { Util.recursiveDelete(cacheDir); - super.tearDown(); } + @Test public void testGetRegion_noSpansInCache() { assertThat(tracker.getRegionEndTimeMs(100)).isEqualTo(CachedRegionTracker.NOT_CACHED); assertThat(tracker.getRegionEndTimeMs(150)).isEqualTo(CachedRegionTracker.NOT_CACHED); } + @Test public void testGetRegion_fullyCached() throws Exception { - tracker.onSpanAdded( - cache, - newCacheSpan(100, 100)); + tracker.onSpanAdded(cache, newCacheSpan(100, 100)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(CachedRegionTracker.CACHED_TO_END); assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(CachedRegionTracker.CACHED_TO_END); } + @Test public void testGetRegion_partiallyCached() throws Exception { - tracker.onSpanAdded( - cache, - newCacheSpan(100, 40)); + tracker.onSpanAdded(cache, newCacheSpan(100, 40)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200); assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200); } + @Test public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception { - tracker.onSpanAdded( - cache, - newCacheSpan(100, 20)); - tracker.onSpanAdded( - cache, - newCacheSpan(120, 20)); + tracker.onSpanAdded(cache, newCacheSpan(100, 20)); + tracker.onSpanAdded(cache, newCacheSpan(120, 20)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200); assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200); } + @Test public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception { // Start with the full stream in cache. - tracker.onSpanAdded( - cache, - newCacheSpan(100, 100)); + tracker.onSpanAdded(cache, newCacheSpan(100, 100)); // Remove the middle bit. - tracker.onSpanRemoved( - cache, - newCacheSpan(140, 40)); + tracker.onSpanRemoved(cache, newCacheSpan(140, 40)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(200); assertThat(tracker.getRegionEndTimeMs(121)).isEqualTo(200); @@ -119,17 +118,32 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { assertThat(tracker.getRegionEndTimeMs(181)).isEqualTo(CachedRegionTracker.CACHED_TO_END); } + @Test public void testGetRegion_subchunkEstimation() throws Exception { - tracker.onSpanAdded( - cache, - newCacheSpan(100, 10)); + tracker.onSpanAdded(cache, newCacheSpan(100, 10)); assertThat(tracker.getRegionEndTimeMs(101)).isEqualTo(50); assertThat(tracker.getRegionEndTimeMs(111)).isEqualTo(CachedRegionTracker.NOT_CACHED); } private CacheSpan newCacheSpan(int position, int length) throws IOException { - return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); + int id = index.assignIdForKey(CACHE_KEY); + File cacheFile = createCacheSpanFile(cacheDir, id, position, length, 0); + return SimpleCacheSpan.createCacheEntry(cacheFile, index); } + public static File createCacheSpanFile( + File cacheDir, int id, long offset, int length, long lastAccessTimestamp) throws IOException { + File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp); + createTestFile(cacheFile, length); + return cacheFile; + } + + private static void createTestFile(File file, int length) throws IOException { + FileOutputStream output = new FileOutputStream(file); + for (int i = 0; i < length; i++) { + output.write(i); + } + output.close(); + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java index 6f7f567ae7..84327e1091 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java @@ -21,13 +21,11 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link LeastRecentlyUsedCacheEvictor}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class LeastRecentlyUsedCacheEvictorTest { @Before 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 bbcf46a30c..89ace34edc 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 @@ -41,13 +41,11 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Unit tests for {@link SimpleCache}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class SimpleCacheTest { private static final String KEY_1 = "key1"; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java index 833a7e10c1..40b626a7db 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java @@ -26,13 +26,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link AesFlushingCipher}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class AesFlushingCipherTest { private static final int DATA_LENGTH = 65536; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java index dcf3d31eb3..4d80a9647e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -27,13 +27,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; /** * Tests {@link AtomicFile}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class AtomicFileTest { private File tempFolder; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java index 13b126090c..af596c35f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -27,13 +27,11 @@ import android.graphics.Color; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit test for ColorParser. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ColorParserTest { // Negative tests. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java index ee77664cce..473e5a8b05 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -23,13 +23,11 @@ import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link NalUnitUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class NalUnitUtilTest { private static final int TEST_PARTIAL_NAL_POSITION = 4; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index 0d864f407f..611584a38c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -21,13 +21,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link ParsableBitArray}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ParsableBitArrayTest { private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 96c29f571d..701f532d6a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -24,13 +24,11 @@ import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link ParsableByteArray}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ParsableByteArrayTest { private static final byte[] TEST_DATA = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java index a3f38abcdb..210c42cfa5 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java @@ -22,13 +22,11 @@ import static org.junit.Assert.fail; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests for {@link ParsableNalUnitBitArray}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ParsableNalUnitBitArrayTest { private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java index 8e384bbb10..6c921f0288 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java @@ -21,13 +21,11 @@ import java.io.ByteArrayOutputStream; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Tests {@link ReusableBufferedOutputStream}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class ReusableBufferedOutputStreamTest { private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java index 52e7a722fb..a52867e1b2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -21,13 +21,11 @@ import static com.google.common.truth.Truth.assertThat; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link UriUtil}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class UriUtilTest { /** 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 ca7a3b199d..cdd5d1a696 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 @@ -32,13 +32,11 @@ import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** * Unit tests for {@link Util}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class UtilTest { @Test 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 bee3334f65..1e4811aadf 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 @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; -import android.app.Instrumentation; +import android.content.Context; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.reflect.Field; import java.util.Arrays; /** @@ -34,6 +35,22 @@ import java.util.Arrays; */ public final class ExtractorAsserts { + private static Context robolectricContext; + + static { + try { + Class runtimeEnvironmentClass = Class.forName("org.robolectric.RuntimeEnvironment"); + Field applicationField = runtimeEnvironmentClass.getDeclaredField("application"); + robolectricContext = (Context) applicationField.get(null); + } catch (ClassNotFoundException e) { + // Keep Robolectric context at null if not found. + } catch (NoSuchFieldException e) { + // Keep Robolectric context at null if not found. + } catch (IllegalAccessException e) { + // Keep Robolectric context at null if not found. + } + } + /** * A factory for {@link Extractor} instances. */ @@ -45,57 +62,87 @@ public final class ExtractorAsserts { private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; /** - * Asserts that an extractor behaves correctly given valid input data: + * Asserts that an extractor behaves correctly given valid input data. Can only be used from + * Robolectric tests. + * *

    *
  • Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling - * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.
  • - *
  • Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean, boolean)} with all possible combinations of "simulate" parameters.
  • + * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail. + *
  • Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean, + * boolean)} with all possible combinations of "simulate" parameters. *
* * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. * @param file The path to the input sample. - * @param instrumentation To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static void assertBehavior(ExtractorFactory factory, String file, - Instrumentation instrumentation) throws IOException, InterruptedException { + public static void assertBehavior(ExtractorFactory factory, String file) + throws IOException, InterruptedException { // Check behavior prior to initialization. Extractor extractor = factory.create(); extractor.seek(0, 0); extractor.release(); // Assert output. - byte[] fileData = TestUtil.getByteArray(instrumentation, file); - assertOutput(factory, file, fileData, instrumentation); + byte[] fileData = TestUtil.getByteArray(robolectricContext, file); + assertOutput(factory, file, fileData, robolectricContext); } /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean, boolean)} with all possible combinations of "simulate" parameters with - * {@code sniffFirst} set to true, and makes one additional call with the "simulate" and - * {@code sniffFirst} parameters all set to false. + * Asserts that an extractor behaves correctly given valid input data: + * + *
    + *
  • Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling + * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail. + *
  • Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean, + * boolean)} with all possible combinations of "simulate" parameters. + *
+ * + * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} + * class which is to be tested. + * @param file The path to the input sample. + * @param context To be used to load the sample file. + * @throws IOException If reading from the input fails. + * @throws InterruptedException If interrupted while reading from the input. + */ + public static void assertBehavior(ExtractorFactory factory, String file, Context context) + throws IOException, InterruptedException { + // Check behavior prior to initialization. + Extractor extractor = factory.create(); + extractor.seek(0, 0); + extractor.release(); + // Assert output. + byte[] fileData = TestUtil.getByteArray(context, file); + assertOutput(factory, file, fileData, context); + } + + /** + * Calls {@link #assertOutput(Extractor, String, byte[], Context, boolean, boolean, boolean, + * boolean)} with all possible combinations of "simulate" parameters with {@code sniffFirst} set + * to true, and makes one additional call with the "simulate" and {@code sniffFirst} parameters + * all set to false. * * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. * @param file The path to the input sample. * @param data Content of the input file. - * @param instrumentation To be used to load the sample file. + * @param context To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static void assertOutput(ExtractorFactory factory, String file, byte[] data, - Instrumentation instrumentation) throws IOException, InterruptedException { - assertOutput(factory.create(), file, data, instrumentation, true, false, false, false); - assertOutput(factory.create(), file, data, instrumentation, true, false, false, true); - assertOutput(factory.create(), file, data, instrumentation, true, false, true, false); - assertOutput(factory.create(), file, data, instrumentation, true, false, true, true); - assertOutput(factory.create(), file, data, instrumentation, true, true, false, false); - assertOutput(factory.create(), file, data, instrumentation, true, true, false, true); - assertOutput(factory.create(), file, data, instrumentation, true, true, true, false); - assertOutput(factory.create(), file, data, instrumentation, true, true, true, true); - assertOutput(factory.create(), file, data, instrumentation, false, false, false, false); + public static void assertOutput( + ExtractorFactory factory, String file, byte[] data, Context context) + throws IOException, InterruptedException { + assertOutput(factory.create(), file, data, context, true, false, false, false); + assertOutput(factory.create(), file, data, context, true, false, false, true); + assertOutput(factory.create(), file, data, context, true, false, true, false); + assertOutput(factory.create(), file, data, context, true, false, true, true); + assertOutput(factory.create(), file, data, context, true, true, false, false); + assertOutput(factory.create(), file, data, context, true, true, false, true); + assertOutput(factory.create(), file, data, context, true, true, true, false); + assertOutput(factory.create(), file, data, context, true, true, true, true); + assertOutput(factory.create(), file, data, context, false, false, false, false); } /** @@ -107,7 +154,7 @@ public final class ExtractorAsserts { * @param extractor The {@link Extractor} to be tested. * @param file The path to the input sample. * @param data Content of the input file. - * @param instrumentation To be used to load the sample file. + * @param context To be used to load the sample file. * @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)} * prior to consuming it. * @param simulateIOErrors Whether to simulate IO errors. @@ -117,10 +164,16 @@ public final class ExtractorAsserts { * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static FakeExtractorOutput assertOutput(Extractor extractor, String file, byte[] data, - Instrumentation instrumentation, boolean sniffFirst, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { + private static FakeExtractorOutput assertOutput( + Extractor extractor, + String file, + byte[] data, + Context context, + boolean sniffFirst, + boolean simulateIOErrors, + boolean simulateUnknownLength, + boolean simulatePartialReads) + throws IOException, InterruptedException { FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength) @@ -132,11 +185,10 @@ public final class ExtractorAsserts { } FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); - if (simulateUnknownLength - && assetExists(instrumentation, file + UNKNOWN_LENGTH_EXTENSION)) { - extractorOutput.assertOutput(instrumentation, file + UNKNOWN_LENGTH_EXTENSION); + if (simulateUnknownLength && assetExists(context, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(context, file + UNKNOWN_LENGTH_EXTENSION); } else { - extractorOutput.assertOutput(instrumentation, file + ".0" + DUMP_EXTENSION); + extractorOutput.assertOutput(context, file + ".0" + DUMP_EXTENSION); } SeekMap seekMap = extractorOutput.seekMap; @@ -151,7 +203,7 @@ public final class ExtractorAsserts { } consumeTestData(extractor, input, timeUs, extractorOutput, false); - extractorOutput.assertOutput(instrumentation, file + '.' + j + DUMP_EXTENSION); + extractorOutput.assertOutput(context, file + '.' + j + DUMP_EXTENSION); } } @@ -165,16 +217,19 @@ public final class ExtractorAsserts { * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. * @param sampleFile The path to the input sample. - * @param instrumentation To be used to load the sample file. + * @param context To be used to load the sample file. * @param expectedThrowable Expected {@link Throwable} class. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) */ - public static void assertThrows(ExtractorFactory factory, String sampleFile, - Instrumentation instrumentation, Class expectedThrowable) + public static void assertThrows( + ExtractorFactory factory, + String sampleFile, + Context context, + Class expectedThrowable) throws IOException, InterruptedException { - byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); + byte[] fileData = TestUtil.getByteArray(context, sampleFile); assertThrows(factory, fileData, expectedThrowable); } @@ -190,8 +245,9 @@ public final class ExtractorAsserts { * @throws InterruptedException If interrupted while reading from the input. * @see #assertThrows(Extractor, byte[], Class, boolean, boolean, boolean) */ - public static void assertThrows(ExtractorFactory factory, byte[] fileData, - Class expectedThrowable) throws IOException, InterruptedException { + private static void assertThrows( + ExtractorFactory factory, byte[] fileData, Class expectedThrowable) + throws IOException, InterruptedException { assertThrows(factory.create(), fileData, expectedThrowable, false, false, false); assertThrows(factory.create(), fileData, expectedThrowable, true, false, false); assertThrows(factory.create(), fileData, expectedThrowable, false, true, false); @@ -214,10 +270,14 @@ public final class ExtractorAsserts { * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static void assertThrows(Extractor extractor, byte[] fileData, - Class expectedThrowable, boolean simulateIOErrors, - boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, - InterruptedException { + private static void assertThrows( + Extractor extractor, + byte[] fileData, + Class expectedThrowable, + boolean simulateIOErrors, + boolean simulateUnknownLength, + boolean simulatePartialReads) + throws IOException, InterruptedException { FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength) @@ -278,13 +338,11 @@ public final class ExtractorAsserts { } } - private static boolean assetExists(Instrumentation instrumentation, String fileName) - throws IOException { + private static boolean assetExists(Context context, String fileName) throws IOException { int i = fileName.lastIndexOf('/'); String path = i >= 0 ? fileName.substring(0, i) : ""; String file = i >= 0 ? fileName.substring(i + 1) : fileName; - return Arrays.asList(instrumentation.getContext().getResources().getAssets().list(path)) - .contains(file); + return Arrays.asList(context.getResources().getAssets().list(path)).contains(file); } } 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 9b59e48a0e..c6543bd7a5 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 @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import android.app.Instrumentation; +import android.content.Context; import android.util.SparseArray; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; @@ -32,9 +32,9 @@ import java.io.PrintWriter; public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpable { /** - * If true, makes {@link #assertOutput(Instrumentation, String)} method write dump result to - * {@code /sdcard/Android/data/apk_package/ + dumpfile} file instead of comparing it with an - * existing file. + * If true, makes {@link #assertOutput(Context, String)} method write dump result to {@code + * /sdcard/Android/data/apk_package/ + dumpfile} file instead of comparing it with an existing + * file. */ private static final boolean WRITE_DUMP = false; @@ -97,18 +97,18 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab * actual dump will be written to {@code dumpFile}. This new dump file needs to be copied to the * project, {@code library/src/androidTest/assets} folder manually. */ - public void assertOutput(Instrumentation instrumentation, String dumpFile) throws IOException { + public void assertOutput(Context context, String dumpFile) throws IOException { String actual = new Dumper().add(this).toString(); if (WRITE_DUMP) { - File directory = instrumentation.getContext().getExternalFilesDir(null); + File directory = context.getExternalFilesDir(null); File file = new File(directory, dumpFile); file.getParentFile().mkdirs(); PrintWriter out = new PrintWriter(file); out.print(actual); out.close(); } else { - String expected = TestUtil.getString(instrumentation, dumpFile); + String expected = TestUtil.getString(context, dumpFile); assertWithMessage(dumpFile).that(actual).isEqualTo(expected); } } 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 b1598a608c..cf0cc342f8 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 @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -142,30 +143,38 @@ public class MediaSourceTestRunner { } /** - * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread. + * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread and blocks + * until the method has been called. * * @param mediaPeriod The {@link MediaPeriod} to prepare. * @param positionUs The position at which to prepare. - * @return A {@link ConditionVariable} that will be opened when preparation completes. + * @return A {@link CountDownLatch} that will be counted down when preparation completes. */ - public ConditionVariable preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { - final ConditionVariable preparedCondition = new ConditionVariable(); - runOnPlaybackThread(new Runnable() { - @Override - public void run() { - mediaPeriod.prepare(new MediaPeriod.Callback() { + public CountDownLatch preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { + final ConditionVariable prepareCalled = new ConditionVariable(); + final CountDownLatch preparedCountDown = new CountDownLatch(1); + runOnPlaybackThread( + new Runnable() { @Override - public void onPrepared(MediaPeriod mediaPeriod) { - preparedCondition.open(); + public void run() { + mediaPeriod.prepare( + new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + preparedCountDown.countDown(); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Do nothing. + } + }, + positionUs); + prepareCalled.open(); } - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - // Do nothing. - } - }, positionUs); - } - }); - return preparedCondition; + }); + prepareCalled.block(); + return preparedCountDown; } /** @@ -234,10 +243,10 @@ public class MediaSourceTestRunner { /** * Creates and releases all periods (including ad periods) defined in the last timeline to be - * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or - * {@link #assertTimelineChangeBlocking()}. + * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or {@link + * #assertTimelineChangeBlocking()}. */ - public void assertPrepareAndReleaseAllPeriods() { + public void assertPrepareAndReleaseAllPeriods() throws InterruptedException { Timeline.Period period = new Timeline.Period(); for (int i = 0; i < timeline.getPeriodCount(); i++) { assertPrepareAndReleasePeriod(new MediaPeriodId(i)); @@ -250,15 +259,16 @@ public class MediaSourceTestRunner { } } - private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { + private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) + throws InterruptedException { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); - ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); - assertThat(preparedCondition.block(TIMEOUT_MS)).isTrue(); + CountDownLatch preparedCondition = preparePeriod(mediaPeriod, 0); + assertThat(preparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); - ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertThat(secondPreparedCondition.block(TIMEOUT_MS)).isTrue(); + CountDownLatch secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); + assertThat(secondPreparedCondition.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue(); // Release the periods. releasePeriod(mediaPeriod); releasePeriod(secondMediaPeriod); 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 34d951b142..fb50ef131b 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 @@ -150,9 +150,8 @@ public class TestUtil { return context.getResources().getAssets().open(fileName); } - public static String getString(Instrumentation instrumentation, String fileName) - throws IOException { - return new String(getByteArray(instrumentation, fileName)); + public static String getString(Context context, String fileName) throws IOException { + return new String(getByteArray(context, fileName)); } /** From 77ec24ca5850386d0060fd2a6c37d1abd7245157 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 12 Feb 2018 11:38:36 -0800 Subject: [PATCH 031/376] Translation console import ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185409304 --- .../src/main/res/values-af/strings.xml | 17 ++++---- .../src/main/res/values-am/strings.xml | 17 ++++---- .../src/main/res/values-ar/strings.xml | 17 ++++---- .../src/main/res/values-b+sr+Latn/strings.xml | 17 ++++---- .../src/main/res/values-bg/strings.xml | 17 ++++---- .../src/main/res/values-ca/strings.xml | 17 ++++---- .../src/main/res/values-cs/strings.xml | 17 ++++---- .../src/main/res/values-da/strings.xml | 17 ++++---- .../src/main/res/values-de/strings.xml | 17 ++++---- .../src/main/res/values-el/strings.xml | 17 ++++---- .../src/main/res/values-en-rAU/strings.xml | 17 ++++---- .../src/main/res/values-en-rGB/strings.xml | 17 ++++---- .../src/main/res/values-en-rIN/strings.xml | 17 ++++---- .../src/main/res/values-es-rUS/strings.xml | 17 ++++---- .../src/main/res/values-es/strings.xml | 17 ++++---- .../src/main/res/values-fa/strings.xml | 17 ++++---- .../src/main/res/values-fi/strings.xml | 17 ++++---- .../src/main/res/values-fr-rCA/strings.xml | 17 ++++---- .../src/main/res/values-fr/strings.xml | 17 ++++---- .../src/main/res/values-hi/strings.xml | 17 ++++---- .../src/main/res/values-hr/strings.xml | 17 ++++---- .../src/main/res/values-hu/strings.xml | 17 ++++---- .../src/main/res/values-in/strings.xml | 17 ++++---- .../src/main/res/values-it/strings.xml | 17 ++++---- .../src/main/res/values-iw/strings.xml | 17 ++++---- .../src/main/res/values-ja/strings.xml | 17 ++++---- .../src/main/res/values-ko/strings.xml | 17 ++++---- .../src/main/res/values-lt/strings.xml | 17 ++++---- .../src/main/res/values-lv/strings.xml | 17 ++++---- .../src/main/res/values-nb/strings.xml | 17 ++++---- .../src/main/res/values-nl/strings.xml | 17 ++++---- .../src/main/res/values-pl/strings.xml | 17 ++++---- .../src/main/res/values-pt-rPT/strings.xml | 17 ++++---- .../src/main/res/values-pt/strings.xml | 17 ++++---- .../src/main/res/values-ro/strings.xml | 17 ++++---- .../src/main/res/values-ru/strings.xml | 17 ++++---- .../src/main/res/values-sk/strings.xml | 17 ++++---- .../src/main/res/values-sl/strings.xml | 17 ++++---- .../src/main/res/values-sr/strings.xml | 14 ++++--- .../src/main/res/values-sv/strings.xml | 17 ++++---- .../src/main/res/values-sw/strings.xml | 17 ++++---- .../src/main/res/values-th/strings.xml | 17 ++++---- .../src/main/res/values-tl/strings.xml | 17 ++++---- .../src/main/res/values-tr/strings.xml | 17 ++++---- .../src/main/res/values-uk/strings.xml | 17 ++++---- .../src/main/res/values-vi/strings.xml | 17 ++++---- .../src/main/res/values-zh-rCN/strings.xml | 17 ++++---- .../src/main/res/values-zh-rHK/strings.xml | 17 ++++---- .../src/main/res/values-zh-rTW/strings.xml | 17 ++++---- .../src/main/res/values-zu/strings.xml | 17 ++++---- library/ui/src/main/res/values-af/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-am/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-ar/strings.xml | 39 +++++++++++-------- .../src/main/res/values-b+sr+Latn/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-bg/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-ca/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-cs/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-da/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-de/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-el/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-en-rAU/strings.xml | 38 ++++++++++-------- .../ui/src/main/res/values-en-rGB/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-en-rIN/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-es-rUS/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-es/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-fa/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-fi/strings.xml | 38 ++++++++++-------- .../ui/src/main/res/values-fr-rCA/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-fr/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-hi/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-hr/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-hu/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-in/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-it/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-iw/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-ja/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-ko/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-lt/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-lv/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-nb/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-nl/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-pl/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-pt-rPT/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-pt/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-ro/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-ru/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-sk/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-sl/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-sr/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-sv/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-sw/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-th/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-tl/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-tr/strings.xml | 38 ++++++++++-------- library/ui/src/main/res/values-uk/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-vi/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-zh-rCN/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-zh-rHK/strings.xml | 39 +++++++++++-------- .../ui/src/main/res/values-zh-rTW/strings.xml | 39 +++++++++++-------- library/ui/src/main/res/values-zu/strings.xml | 39 +++++++++++-------- 100 files changed, 1550 insertions(+), 1229 deletions(-) diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml index 4ef78cd84f..65bc1e89d8 100644 --- a/extensions/mediasession/src/main/res/values-af/strings.xml +++ b/extensions/mediasession/src/main/res/values-af/strings.xml @@ -1,6 +1,5 @@ - - - - "Herhaal alles" - "Herhaal niks" - "Herhaal een" + --> + + + "Herhaal niks" + "Herhaal een" + "Herhaal alles" diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml index 531f605584..0dc20aaa04 100644 --- a/extensions/mediasession/src/main/res/values-am/strings.xml +++ b/extensions/mediasession/src/main/res/values-am/strings.xml @@ -1,6 +1,5 @@ - - - - "ሁሉንም ድገም" - "ምንም አትድገም" - "አንዱን ድገም" + --> + + + "ምንም አትድገም" + "አንድ ድገም" + "ሁሉንም ድገም" diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml index 0101a746e0..2776e28356 100644 --- a/extensions/mediasession/src/main/res/values-ar/strings.xml +++ b/extensions/mediasession/src/main/res/values-ar/strings.xml @@ -1,6 +1,5 @@ - - - - "تكرار الكل" - "عدم التكرار" - "تكرار مقطع واحد" + --> + + + "عدم التكرار" + "تكرار مقطع صوتي واحد" + "تكرار الكل" diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml index 67a51cf85e..d20b16531a 100644 --- a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml +++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml @@ -1,6 +1,5 @@ - - - - "Ponovi sve" - "Ne ponavljaj nijednu" - "Ponovi jednu" + --> + + + "Ne ponavljaj nijednu" + "Ponovi jednu" + "Ponovi sve" diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml index 16910d640a..087eaee8c2 100644 --- a/extensions/mediasession/src/main/res/values-bg/strings.xml +++ b/extensions/mediasession/src/main/res/values-bg/strings.xml @@ -1,6 +1,5 @@ - - - - "Повтаряне на всички" - "Без повтаряне" - "Повтаряне на един елемент" + --> + + + "Без повтаряне" + "Повтаряне на един елемент" + "Повтаряне на всички" diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml index 89414d736e..4a4d8646a2 100644 --- a/extensions/mediasession/src/main/res/values-ca/strings.xml +++ b/extensions/mediasession/src/main/res/values-ca/strings.xml @@ -1,6 +1,5 @@ - - - - "Repeteix-ho tot" - "No en repeteixis cap" - "Repeteix-ne un" + --> + + + "No en repeteixis cap" + "Repeteix una" + "Repeteix tot" diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml index 784d872570..c59dcfc874 100644 --- a/extensions/mediasession/src/main/res/values-cs/strings.xml +++ b/extensions/mediasession/src/main/res/values-cs/strings.xml @@ -1,6 +1,5 @@ - - - - "Opakovat vše" - "Neopakovat" - "Opakovat jednu položku" + --> + + + "Neopakovat" + "Opakovat jednu" + "Opakovat vše" diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml index 2c9784d122..0d31261f3d 100644 --- a/extensions/mediasession/src/main/res/values-da/strings.xml +++ b/extensions/mediasession/src/main/res/values-da/strings.xml @@ -1,6 +1,5 @@ - - - - "Gentag alle" - "Gentag ingen" - "Gentag en" + --> + + + "Gentag ingen" + "Gentag én" + "Gentag alle" diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml index c11e449665..dfa86a54d4 100644 --- a/extensions/mediasession/src/main/res/values-de/strings.xml +++ b/extensions/mediasession/src/main/res/values-de/strings.xml @@ -1,6 +1,5 @@ - - - - "Alle wiederholen" - "Keinen Titel wiederholen" - "Einen Titel wiederholen" + --> + + + "Keinen wiederholen" + "Einen wiederholen" + "Alle wiederholen" diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml index 6279af5d64..e73b24592e 100644 --- a/extensions/mediasession/src/main/res/values-el/strings.xml +++ b/extensions/mediasession/src/main/res/values-el/strings.xml @@ -1,6 +1,5 @@ - - - - "Επανάληψη όλων" - "Καμία επανάληψη" - "Επανάληψη ενός στοιχείου" + --> + + + "Καμία επανάληψη" + "Επανάληψη ενός κομματιού" + "Επανάληψη όλων" diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml index a3fccf8b52..197222473d 100644 --- a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml @@ -1,6 +1,5 @@ - - - - "Repeat all" - "Repeat none" - "Repeat one" + --> + + + "Repeat none" + "Repeat one" + "Repeat all" diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml index a3fccf8b52..197222473d 100644 --- a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml @@ -1,6 +1,5 @@ - - - - "Repeat all" - "Repeat none" - "Repeat one" + --> + + + "Repeat none" + "Repeat one" + "Repeat all" diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml index a3fccf8b52..197222473d 100644 --- a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml +++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml @@ -1,6 +1,5 @@ - - - - "Repeat all" - "Repeat none" - "Repeat one" + --> + + + "Repeat none" + "Repeat one" + "Repeat all" diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml index 0fe29d3d5a..192ad2f2ef 100644 --- a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml +++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml @@ -1,6 +1,5 @@ - - - - "Repetir todo" - "No repetir" - "Repetir uno" + --> + + + "No repetir" + "Repetir uno" + "Repetir todo" diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml index 0fe29d3d5a..192ad2f2ef 100644 --- a/extensions/mediasession/src/main/res/values-es/strings.xml +++ b/extensions/mediasession/src/main/res/values-es/strings.xml @@ -1,6 +1,5 @@ - - - - "Repetir todo" - "No repetir" - "Repetir uno" + --> + + + "No repetir" + "Repetir uno" + "Repetir todo" diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml index e37a08de64..42b1b14c90 100644 --- a/extensions/mediasession/src/main/res/values-fa/strings.xml +++ b/extensions/mediasession/src/main/res/values-fa/strings.xml @@ -1,6 +1,5 @@ - - - - "تکرار همه" - "تکرار هیچ‌کدام" - "یک‌بار تکرار" + --> + + + "تکرار هیچ‌کدام" + "یکبار تکرار" + "تکرار همه" diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml index c920827976..68f1b6c93b 100644 --- a/extensions/mediasession/src/main/res/values-fi/strings.xml +++ b/extensions/mediasession/src/main/res/values-fi/strings.xml @@ -1,6 +1,5 @@ - - - - "Toista kaikki" - "Toista ei mitään" - "Toista yksi" + --> + + + "Ei uudelleentoistoa" + "Toista yksi uudelleen" + "Toista kaikki uudelleen" diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml index c5191e74a9..62edf759bb 100644 --- a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml +++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml @@ -1,6 +1,5 @@ - - - - "Tout lire en boucle" - "Aucune répétition" - "Répéter un élément" + --> + + + "Ne rien lire en boucle" + "Lire une chanson en boucle" + "Tout lire en boucle" diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml index 1d76358d1f..2ea8653e93 100644 --- a/extensions/mediasession/src/main/res/values-fr/strings.xml +++ b/extensions/mediasession/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - - - - "Tout lire en boucle" - "Ne rien lire en boucle" - "Lire en boucle un élément" + --> + + + "Ne rien lire en boucle" + "Lire un titre en boucle" + "Tout lire en boucle" diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml index 8ce336d5e5..79261e4e59 100644 --- a/extensions/mediasession/src/main/res/values-hi/strings.xml +++ b/extensions/mediasession/src/main/res/values-hi/strings.xml @@ -1,6 +1,5 @@ - - - - "सभी को दोहराएं" - "कुछ भी न दोहराएं" - "एक दोहराएं" + --> + + + "किसी को न दोहराएं" + "एक को दोहराएं" + "सभी को दोहराएं" diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml index 9f995ec15b..81bb428528 100644 --- a/extensions/mediasession/src/main/res/values-hr/strings.xml +++ b/extensions/mediasession/src/main/res/values-hr/strings.xml @@ -1,6 +1,5 @@ - - - - "Ponovi sve" - "Bez ponavljanja" - "Ponovi jedno" + --> + + + "Bez ponavljanja" + "Ponovi jedno" + "Ponovi sve" diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml index 2335ade72e..8e8369a61f 100644 --- a/extensions/mediasession/src/main/res/values-hu/strings.xml +++ b/extensions/mediasession/src/main/res/values-hu/strings.xml @@ -1,6 +1,5 @@ - - - - "Összes ismétlése" - "Nincs ismétlés" - "Egy ismétlése" + --> + + + "Nincs ismétlés" + "Egy szám ismétlése" + "Összes szám ismétlése" diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml index 093a7f8576..a20a6362c8 100644 --- a/extensions/mediasession/src/main/res/values-in/strings.xml +++ b/extensions/mediasession/src/main/res/values-in/strings.xml @@ -1,6 +1,5 @@ - - - - "Ulangi Semua" - "Jangan Ulangi" - "Ulangi Satu" + --> + + + "Jangan ulangi" + "Ulangi 1" + "Ulangi semua" diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml index c0682519f9..3a59bb5804 100644 --- a/extensions/mediasession/src/main/res/values-it/strings.xml +++ b/extensions/mediasession/src/main/res/values-it/strings.xml @@ -1,6 +1,5 @@ - - - - "Ripeti tutti" - "Non ripetere nessuno" - "Ripeti uno" + --> + + + "Non ripetere nulla" + "Ripeti uno" + "Ripeti tutto" diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml index 5cf23d5a4c..f9eac73e59 100644 --- a/extensions/mediasession/src/main/res/values-iw/strings.xml +++ b/extensions/mediasession/src/main/res/values-iw/strings.xml @@ -1,6 +1,5 @@ - - - - "חזור על הכל" - "אל תחזור על כלום" - "חזור על פריט אחד" + --> + + + "אל תחזור על אף פריט" + "חזור על פריט אחד" + "חזור על הכול" diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml index 6f543fbdee..bcfb6eb7c2 100644 --- a/extensions/mediasession/src/main/res/values-ja/strings.xml +++ b/extensions/mediasession/src/main/res/values-ja/strings.xml @@ -1,6 +1,5 @@ - - - - "全曲を繰り返し" - "繰り返しなし" - "1曲を繰り返し" + --> + + + "リピートなし" + "1 曲をリピート" + "全曲をリピート" diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml index d269937771..7be13b133a 100644 --- a/extensions/mediasession/src/main/res/values-ko/strings.xml +++ b/extensions/mediasession/src/main/res/values-ko/strings.xml @@ -1,6 +1,5 @@ - - - - "전체 반복" - "반복 안함" - "한 항목 반복" + --> + + + "반복 안함" + "현재 미디어 반복" + "모두 반복" diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml index ae8f1cf8c3..78d1753ed0 100644 --- a/extensions/mediasession/src/main/res/values-lt/strings.xml +++ b/extensions/mediasession/src/main/res/values-lt/strings.xml @@ -1,6 +1,5 @@ - - - - "Kartoti viską" - "Nekartoti nieko" - "Kartoti vieną" + --> + + + "Nekartoti nieko" + "Kartoti vieną" + "Kartoti viską" diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml index a69f6a0ad5..085723a271 100644 --- a/extensions/mediasession/src/main/res/values-lv/strings.xml +++ b/extensions/mediasession/src/main/res/values-lv/strings.xml @@ -1,6 +1,5 @@ - - - - "Atkārtot visu" - "Neatkārtot nevienu" - "Atkārtot vienu" + --> + + + "Neatkārtot nevienu" + "Atkārtot vienu" + "Atkārtot visu" diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml index 10f334b226..2e986733fc 100644 --- a/extensions/mediasession/src/main/res/values-nb/strings.xml +++ b/extensions/mediasession/src/main/res/values-nb/strings.xml @@ -1,6 +1,5 @@ - - - - "Gjenta alle" - "Ikke gjenta noen" - "Gjenta én" + --> + + + "Ikke gjenta noen" + "Gjenta én" + "Gjenta alle" diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml index 55997be098..4dfc31bb98 100644 --- a/extensions/mediasession/src/main/res/values-nl/strings.xml +++ b/extensions/mediasession/src/main/res/values-nl/strings.xml @@ -1,6 +1,5 @@ - - - - "Alles herhalen" - "Niet herhalen" - "Eén herhalen" + --> + + + "Niets herhalen" + "Eén herhalen" + "Alles herhalen" diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml index 6a52d58b63..37af4c1616 100644 --- a/extensions/mediasession/src/main/res/values-pl/strings.xml +++ b/extensions/mediasession/src/main/res/values-pl/strings.xml @@ -1,6 +1,5 @@ - - - - "Powtórz wszystkie" - "Nie powtarzaj" - "Powtórz jeden" + --> + + + "Nie powtarzaj" + "Powtórz jeden" + "Powtórz wszystkie" diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml index efb8fc433f..43a4cd9e6a 100644 --- a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml +++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml @@ -1,6 +1,5 @@ - - - - "Repetir tudo" - "Não repetir" - "Repetir um" + --> + + + "Não repetir nenhum" + "Repetir um" + "Repetir tudo" diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml index aadebbb3b0..4e7ce248cc 100644 --- a/extensions/mediasession/src/main/res/values-pt/strings.xml +++ b/extensions/mediasession/src/main/res/values-pt/strings.xml @@ -1,6 +1,5 @@ - - - - "Repetir tudo" - "Não repetir" - "Repetir uma" + --> + + + "Não repetir" + "Repetir uma" + "Repetir tudo" diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml index f6aee447e5..9345a5df35 100644 --- a/extensions/mediasession/src/main/res/values-ro/strings.xml +++ b/extensions/mediasession/src/main/res/values-ro/strings.xml @@ -1,6 +1,5 @@ - - - - "Repetați toate" - "Repetați niciuna" - "Repetați unul" + --> + + + "Nu repetați niciunul" + "Repetați unul" + "Repetați-le pe toate" diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml index 575ad9f930..8c52ea8395 100644 --- a/extensions/mediasession/src/main/res/values-ru/strings.xml +++ b/extensions/mediasession/src/main/res/values-ru/strings.xml @@ -1,6 +1,5 @@ - - - - "Повторять все" - "Не повторять" - "Повторять один элемент" + --> + + + "Не повторять" + "Повторять трек" + "Повторять все" diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml index 5d092003e5..9a7cccd096 100644 --- a/extensions/mediasession/src/main/res/values-sk/strings.xml +++ b/extensions/mediasession/src/main/res/values-sk/strings.xml @@ -1,6 +1,5 @@ - - - - "Opakovať všetko" - "Neopakovať" - "Opakovať jednu položku" + --> + + + "Neopakovať" + "Opakovať jednu" + "Opakovať všetko" diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml index ecac3800c8..7bf20baa19 100644 --- a/extensions/mediasession/src/main/res/values-sl/strings.xml +++ b/extensions/mediasession/src/main/res/values-sl/strings.xml @@ -1,6 +1,5 @@ - - - - "Ponovi vse" - "Ne ponovi" - "Ponovi eno" + --> + + + "Brez ponavljanja" + "Ponavljanje ene" + "Ponavljanje vseh" diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml index 881cb2703b..b82940da2e 100644 --- a/extensions/mediasession/src/main/res/values-sr/strings.xml +++ b/extensions/mediasession/src/main/res/values-sr/strings.xml @@ -1,6 +1,5 @@ - - - + --> + + + "Не понављај ниједну" + "Понови једну" + "Понови све" diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml index 3a7bb630aa..13edc46d1f 100644 --- a/extensions/mediasession/src/main/res/values-sv/strings.xml +++ b/extensions/mediasession/src/main/res/values-sv/strings.xml @@ -1,6 +1,5 @@ - - - - "Upprepa alla" - "Upprepa inga" - "Upprepa en" + --> + + + "Upprepa inga" + "Upprepa en" + "Upprepa alla" diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml index 726012ab88..b40ce1a727 100644 --- a/extensions/mediasession/src/main/res/values-sw/strings.xml +++ b/extensions/mediasession/src/main/res/values-sw/strings.xml @@ -1,6 +1,5 @@ - - - - "Rudia zote" - "Usirudie Yoyote" - "Rudia Moja" + --> + + + "Usirudie yoyote" + "Rudia moja" + "Rudia zote" diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml index af502b3a4c..4e40f559d0 100644 --- a/extensions/mediasession/src/main/res/values-th/strings.xml +++ b/extensions/mediasession/src/main/res/values-th/strings.xml @@ -1,6 +1,5 @@ - - - - "เล่นซ้ำทั้งหมด" - "ไม่เล่นซ้ำ" - "เล่นซ้ำรายการเดียว" + --> + + + "ไม่เล่นซ้ำ" + "เล่นซ้ำเพลงเดียว" + "เล่นซ้ำทั้งหมด" diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml index 239972a4c7..4fff164f9f 100644 --- a/extensions/mediasession/src/main/res/values-tl/strings.xml +++ b/extensions/mediasession/src/main/res/values-tl/strings.xml @@ -1,6 +1,5 @@ - - - - "Ulitin Lahat" - "Walang Uulitin" - "Ulitin ang Isa" + --> + + + "Walang uulitin" + "Mag-ulit ng isa" + "Ulitin lahat" diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml index 89a98b1ed9..f93fd7fc80 100644 --- a/extensions/mediasession/src/main/res/values-tr/strings.xml +++ b/extensions/mediasession/src/main/res/values-tr/strings.xml @@ -1,6 +1,5 @@ - - - - "Tümünü Tekrarla" - "Hiçbirini Tekrarlama" - "Birini Tekrarla" + --> + + + "Hiçbirini tekrarlama" + "Bir şarkıyı tekrarla" + "Tümünü tekrarla" diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml index 4e1d25eb8a..fb9d000474 100644 --- a/extensions/mediasession/src/main/res/values-uk/strings.xml +++ b/extensions/mediasession/src/main/res/values-uk/strings.xml @@ -1,6 +1,5 @@ - - - - "Повторити все" - "Не повторювати" - "Повторити один елемент" + --> + + + "Не повторювати" + "Повторити 1" + "Повторити всі" diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml index dabc9e05d5..379dc36ee6 100644 --- a/extensions/mediasession/src/main/res/values-vi/strings.xml +++ b/extensions/mediasession/src/main/res/values-vi/strings.xml @@ -1,6 +1,5 @@ - - - - "Lặp lại tất cả" - "Không lặp lại" - "Lặp lại một mục" + --> + + + "Không lặp lại" + "Lặp lại một" + "Lặp lại tất cả" diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml index beb3403cb9..6917f75bf9 100644 --- a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,5 @@ - - - - "重复播放全部" - "不重复播放" - "重复播放单个视频" + --> + + + "不重复播放" + "重复播放一项" + "全部重复播放" diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml index 775cd6441c..b63f103e2a 100644 --- a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml @@ -1,6 +1,5 @@ - - - - "重複播放所有媒體項目" - "不重複播放任何媒體項目" - "重複播放一個媒體項目" + --> + + + "不重複播放" + "重複播放一個" + "全部重複播放" diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml index d3789f4145..0a460b9e08 100644 --- a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml +++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml @@ -1,6 +1,5 @@ - - - - "重複播放所有媒體項目" - "不重複播放" - "重複播放單一媒體項目" + --> + + + "不重複播放" + "重複播放單一項目" + "重複播放所有項目" diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml index 789b6fecb4..ccf8452d69 100644 --- a/extensions/mediasession/src/main/res/values-zu/strings.xml +++ b/extensions/mediasession/src/main/res/values-zu/strings.xml @@ -1,6 +1,5 @@ - - - - "Phinda konke" - "Ungaphindi lutho" - "Phida okukodwa" + --> + + + "Phinda okungekho" + "Phinda okukodwa" + "Phinda konke" diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index f1c45cd7f7..46484d96f7 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -1,6 +1,5 @@ - - - - "Vorige snit" - "Volgende snit" - "Wag" - "Speel" - "Stop" - "Spoel terug" - "Vinnig vorentoe" - "Herhaal alles" - "Herhaal niks" - "Herhaal een" - "Skommel" - Volskermmodus + --> + + + "Vorige snit" + "Volgende snit" + "Onderbreek" + "Speel" + "Stop" + "Spoel terug" + "Spoel vorentoe" + "Herhaal niks" + "Herhaal een" + "Herhaal alles" + "Skommel" + "Volskermmodus" + "Aflaai op waglys" + "Laai tans af" + "Aflaai is voltooi" + "Kon nie aflaai nie" diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index 14d3ff0242..a69d730fa7 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -1,6 +1,5 @@ - - - - "ቀዳሚ ትራክ" - "ቀጣይ ትራክ" - "ለአፍታ አቁም" - "አጫውት" - "አቁም" - "ወደኋላ አጠንጥን" - "በፍጥነት አሳልፍ" - "ሁሉንም ድገም" - "ምንም አትድገም" - "አንዱን ድገም" - "በው" - ባለ ሙሉ ማያ ገጽ ሁኔታ + --> + + + "ቀዳሚ ትራክ" + "ቀጣይ ትራክ" + "ላፍታ አቁም" + "አጫውት" + "አቁም" + "ወደኋላ መልስ" + "በፍጥነት አሳልፍ" + "ምንም አትድገም" + "አንድ ድገም" + "ሁሉንም ድገም" + "በውዝ" + "የሙሉ ማያ ሁነታ" + "ማውረድ ወረፋ ይዟል" + "በማውረድ ላይ" + "ማውረድ ተጠናቋል" + "ማውረድ አልተሳካም" diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 2cc56abbfa..efc65b3dc0 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -1,6 +1,5 @@ - - - - "المقطع الصوتي السابق" - "المقطع الصوتي التالي" - "إيقاف مؤقت" - "تشغيل" - "إيقاف" - "إرجاع" - "تقديم سريع" - "تكرار الكل" - "عدم التكرار" - "تكرار مقطع واحد" - "ترتيب عشوائي" - وضع ملء الشاشة + --> + + + "المقطع الصوتي السابق" + "المقطع الصوتي التالي" + "إيقاف مؤقت" + "تشغيل" + "إيقاف" + "إرجاع" + "تقديم سريع" + "عدم التكرار" + "تكرار مقطع صوتي واحد" + "تكرار الكل" + "ترتيب عشوائي" + "وضع ملء الشاشة" + "التنزيل قيد الانتظار" + "تحميل" + "اكتمل التنزيل" + "تعذّر التنزيل" 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 a9d35e5cb6..3ae9cea4b4 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,6 +1,5 @@ - - - - "Prethodna pesma" - "Sledeća pesma" - "Pauza" - "Pusti" - "Zaustavi" - "Premotaj unazad" - "Premotaj unapred" - "Ponovi sve" - "Ne ponavljaj nijednu" - "Ponovi jednu" - "Pusti nasumično" + --> + + + "Prethodna pesma" + "Sledeća pesma" + "Pauziraj" + "Pusti" + "Zaustavi" + "Premotaj unazad" + "Premotaj unapred" + "Ne ponavljaj nijednu" + "Ponovi jednu" + "Ponovi sve" + "Pusti nasumično" + "Režim celog ekrana" + "Preuzimanje je na čekanju" + "Preuzimanje" + "Preuzimanje je završeno" + "Preuzimanje nije uspelo" diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index e350479788..2524ded55f 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -1,6 +1,5 @@ - - - - "Предишен запис" - "Следващ запис" - "Пауза" - "Пускане" - "Спиране" - "Превъртане назад" - "Превъртане напред" - "Повтаряне на всички" - "Без повтаряне" - "Повтаряне на един елемент" - "Разбъркване" + --> + + + "Предишен запис" + "Следващ запис" + "Поставяне на пауза" + "Възпроизвеждане" + "Спиране" + "Превъртане назад" + "Превъртане напред" + "Без повтаряне" + "Повтаряне на един елемент" + "Повтаряне на всички" + "Разбъркване" + "Режим на цял екран" + "Изтеглянето е в опашката" + "Изтегля се" + "Изтеглянето завърши" + "Изтеглянето не бе успешно" diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index fd76a8e08e..d62f14274a 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -1,6 +1,5 @@ - - - - "Ruta anterior" - "Ruta següent" - "Posa en pausa" - "Reprodueix" - "Atura" - "Rebobina" - "Avança ràpidament" - "Repeteix-ho tot" - "No en repeteixis cap" - "Repeteix-ne un" - "Reprodueix aleatòriament" + --> + + + "Pista anterior" + "Pista següent" + "Posa en pausa" + "Reprodueix" + "Atura" + "Rebobina" + "Avança ràpidament" + "No en repeteixis cap" + "Repeteix una" + "Repeteix tot" + "Reprodueix aleatòriament" + "Mode de pantalla completa" + "La baixada s\'ha posat a la cua" + "S\'està baixant" + "S\'ha completat la baixada" + "No s\'ha pogut baixar" diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 087ab79c25..fd8c7115f0 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -1,6 +1,5 @@ - - - - "Předchozí skladba" - "Další skladba" - "Pozastavit" - "Přehrát" - "Zastavit" - "Přetočit zpět" - "Přetočit vpřed" - "Opakovat vše" - "Neopakovat" - "Opakovat jednu položku" - "Náhodně" + --> + + + "Předchozí skladba" + "Další skladba" + "Pozastavit" + "Přehrát" + "Zastavit" + "Přetočit zpět" + "Rychle vpřed" + "Neopakovat" + "Opakovat jednu" + "Opakovat vše" + "Náhodně" + "Režim celé obrazovky" + "Zařazeno do fronty stahování" + "Stahování" + "Stahování bylo dokončeno" + "Stažení se nezdařilo" diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 0ae23ee288..a53e9d132c 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -1,6 +1,5 @@ - - - - "Forrige nummer" - "Næste nummer" - "Pause" - "Afspil" - "Stop" - "Spol tilbage" - "Spol frem" - "Gentag alle" - "Gentag ingen" - "Gentag en" - "Bland" + --> + + + "Afspil forrige" + "Afspil næste" + "Sæt på pause" + "Afspil" + "Stop" + "Spol tilbage" + "Spol frem" + "Gentag ingen" + "Gentag én" + "Gentag alle" + "Bland" + "Fuld skærm" + "Downloaden er i kø" + "Download" + "Downloaden er udført" + "Download mislykkedes" diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index b31ecc93e8..9850520d1f 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -1,6 +1,5 @@ - - - - "Vorheriger Titel" - "Nächster Titel" - "Pausieren" - "Wiedergabe" - "Beenden" - "Zurückspulen" - "Vorspulen" - "Alle wiederholen" - "Keinen Titel wiederholen" - "Einen Titel wiederholen" - "Zufallsmix" - Vollbildmodus + --> + + + "Vorheriger Titel" + "Nächster Titel" + "Pausieren" + "Wiedergeben" + "Beenden" + "Zurückspulen" + "Vorspulen" + "Keinen wiederholen" + "Einen wiederholen" + "Alle wiederholen" + "Zufallsmix" + "Vollbildmodus" + "Download in der Warteschlange" + "Wird heruntergeladen" + "Download abgeschlossen" + "Download fehlgeschlagen" diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 9bc6a87889..ebfe7c0cb1 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -1,6 +1,5 @@ - - - - "Προηγούμενο κομμάτι" - "Επόμενο κομμάτι" - "Παύση" - "Αναπαραγωγή" - "Διακοπή" - "Επαναφορά" - "Γρήγορη προώθηση" - "Επανάληψη όλων" - "Καμία επανάληψη" - "Επανάληψη ενός στοιχείου" - "Τυχαία αναπαραγωγή" - Λειτουργία πλήρους οθόνης + --> + + + "Προηγούμενο κομμάτι" + "Επόμενο κομμάτι" + "Παύση" + "Αναπαραγωγή" + "Διακοπή" + "Επαναφορά" + "Γρήγορη προώθηση" + "Καμία επανάληψη" + "Επανάληψη ενός κομματιού" + "Επανάληψη όλων" + "Τυχαία αναπαραγωγή" + "Λειτουργία πλήρους οθόνης" + "Η λήψη προστέθηκε στην ουρά" + "Λήψη" + "Η λήψη ολοκληρώθηκε" + "Η λήψη απέτυχε" 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 0b4c465853..6c2767cacb 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -1,6 +1,5 @@ - - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" + --> + + + "Previous track" + "Next track" + "Pause" + "Play" + "Stop" + "Rewind" + "Fast-forward" + "Repeat none" + "Repeat one" + "Repeat all" + "Shuffle" + "Full-screen mode" + "Download queued" + "Downloading" + "Download completed" + "Download failed" 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 e80b2c70c6..6c2767cacb 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -1,6 +1,5 @@ - - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" - Fullscreen mode + --> + + + "Previous track" + "Next track" + "Pause" + "Play" + "Stop" + "Rewind" + "Fast-forward" + "Repeat none" + "Repeat one" + "Repeat all" + "Shuffle" + "Full-screen mode" + "Download queued" + "Downloading" + "Download completed" + "Download failed" 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 e80b2c70c6..6c2767cacb 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -1,6 +1,5 @@ - - - - "Previous track" - "Next track" - "Pause" - "Play" - "Stop" - "Rewind" - "Fast-forward" - "Repeat all" - "Repeat none" - "Repeat one" - "Shuffle" - Fullscreen mode + --> + + + "Previous track" + "Next track" + "Pause" + "Play" + "Stop" + "Rewind" + "Fast-forward" + "Repeat none" + "Repeat one" + "Repeat all" + "Shuffle" + "Full-screen mode" + "Download queued" + "Downloading" + "Download completed" + "Download failed" 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 e6cf3fc6f2..6836cfb577 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -1,6 +1,5 @@ - - - - "Pista anterior" - "Siguiente pista" - "Pausar" - "Reproducir" - "Detener" - "Retroceder" - "Avanzar" - "Repetir todo" - "No repetir" - "Repetir uno" - "Reproducir aleatoriamente" + --> + + + "Pista anterior" + "Pista siguiente" + "Pausar" + "Reproducir" + "Detener" + "Retroceder" + "Avanzar" + "No repetir" + "Repetir uno" + "Repetir todo" + "Reproducir aleatoriamente" + "Modo de pantalla completa" + "Descarga en fila" + "Descargando" + "Se completó la descarga" + "No se pudo descargar" diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 2029ab833e..ab71863ca2 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -1,6 +1,5 @@ - - - - "Canción anterior" - "Siguiente canción" - "Pausar" - "Reproducir" - "Detener" - "Rebobinar" - "Avance rápido" - "Repetir todo" - "No repetir" - "Repetir uno" - "Reproducción aleatoria" - Modo de pantalla completa + --> + + + "Pista anterior" + "Siguiente pista" + "Pausar" + "Reproducir" + "Detener" + "Rebobinar" + "Avanzar rápidamente" + "No repetir" + "Repetir uno" + "Repetir todo" + "Reproducir aleatoriamente" + "Modo de pantalla completa" + "Descarga en cola" + "Descarga de archivos" + "Descarga de archivos completado" + "No se ha podido descargar" diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index c2303a6e62..80c7340496 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -1,6 +1,5 @@ - - - - "آهنگ قبلی" - "آهنگ بعدی" - "مکث" - "پخش" - "توقف" - "عقب بردن" - "جلو بردن سریع" - "تکرار همه" - "تکرار هیچ‌کدام" - "یک‌بار تکرار" - "پخش درهم" - حالت تمام صفحه + --> + + + "آهنگ قبلی" + "آهنگ بعدی" + "مکث" + "پخش" + "توقف" + "عقب بردن" + "جلو بردن سریع" + "تکرار هیچ‌کدام" + "یکبار تکرار" + "تکرار همه" + "درهم" + "حالت تمام‌صفحه" + "درانتظار بارگیری" + "درحال بارگیری" + "بارگیری کامل شد" + "بارگیری نشد" diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 92feb86683..4eed46ee78 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -1,6 +1,5 @@ - - - - "Edellinen raita" - "Seuraava raita" - "Tauko" - "Toista" - "Seis" - "Kelaa taakse" - "Kelaa eteen" - "Toista kaikki" - "Toista ei mitään" - "Toista yksi" - "Toista satunnaisesti" + --> + + + "Edellinen kappale" + "Seuraava kappale" + "Keskeytä" + "Toista" + "Lopeta" + "Kelaa taaksepäin" + "Kelaa eteenpäin" + "Ei uudelleentoistoa" + "Toista yksi uudelleen" + "Toista kaikki uudelleen" + "Satunnaistoisto" + "Koko näytön tila" + "Lataus jonossa" + "Ladataan" + "Lataus valmis" + "Lataus epäonnistui" 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 45fc0a86f9..40258f6480 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -1,6 +1,5 @@ - - - - "Chanson précédente" - "Chanson suivante" - "Pause" - "Lecture" - "Arrêter" - "Reculer" - "Avance rapide" - "Tout lire en boucle" - "Aucune répétition" - "Répéter un élément" - "Lecture aléatoire" + --> + + + "Chanson précédente" + "Chanson suivante" + "Pause" + "Lire" + "Arrêter" + "Retour arrière" + "Avance rapide" + "Ne rien lire en boucle" + "Lire une chanson en boucle" + "Tout lire en boucle" + "Lecture aléatoire" + "Mode Plein écran" + "File d\'attente de télécharg." + "Téléchargement en cours…" + "Téléchargement terminé" + "Échec du téléchargement" diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index 6617fd6e6a..3641c218d9 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - - - - "Piste précédente" - "Piste suivante" - "Interrompre" - "Lire" - "Arrêter" - "Retour arrière" - "Avance rapide" - "Tout lire en boucle" - "Ne rien lire en boucle" - "Lire en boucle un élément" - "Lire en mode aléatoire" - Mode plein écran + --> + + + "Titre précédent" + "Titre suivant" + "Pause" + "Lecture" + "Arrêter" + "Retour arrière" + "Avance rapide" + "Ne rien lire en boucle" + "Lire un titre en boucle" + "Tout lire en boucle" + "Aléatoire" + "Mode plein écran" + "Téléchargement en attente" + "Téléchargement…" + "Téléchargement terminé" + "Échec du téléchargement" diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index 6545307e8c..9f7ed0d171 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -1,6 +1,5 @@ - - - - "पिछला ट्रैक" - "अगला ट्रैक" - "रोकें" - "चलाएं" - "बंद करें" - "रिवाइंड करें" - "फ़ास्ट फ़ॉरवर्ड" - "सभी को दोहराएं" - "कुछ भी न दोहराएं" - "एक दोहराएं" - "शफ़ल करें" - पूर्ण-स्क्रीन मोड + --> + + + "पिछला ट्रैक" + "अगला ट्रैक" + "रोकें" + "चलाएं" + "बंद करें" + "पीछे ले जाएं" + "तेज़ी से आगे बढ़ाएं" + "किसी को न दोहराएं" + "एक को दोहराएं" + "सभी को दोहराएं" + "शफ़ल करें" + "फ़ुलस्क्रीन मोड" + "डाउनलोड को कतार में लगाया गया" + "डाउनलोड हो रहा है" + "डाउनलोड पूरा हुआ" + "डाउनलोड नहीं हो सका" diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index dd7423032b..aeae6d6507 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -1,6 +1,5 @@ - - - - "Prethodna pjesma" - "Sljedeća pjesma" - "Pauziraj" - "Reproduciraj" - "Zaustavi" - "Unatrag" - "Brzo unaprijed" - "Ponovi sve" - "Bez ponavljanja" - "Ponovi jedno" - "Reproduciraj nasumično" - Prikaz na cijelom zaslonu + --> + + + "Prethodni zapis" + "Sljedeći zapis" + "Pauza" + "Reproduciraj" + "Zaustavi" + "Unatrag" + "Brzo unaprijed" + "Bez ponavljanja" + "Ponovi jedno" + "Ponovi sve" + "Reproduciraj nasumično" + "Prikaz na cijelom zaslonu" + "Preuzimanje na čekanju" + "Preuzimanje datoteka" + "Preuzimanje je dovršeno" + "Preuzimanje nije uspjelo" diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index abd9f9cfac..b001182a35 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -1,6 +1,5 @@ - - - - "Előző szám" - "Következő szám" - "Szünet" - "Lejátszás" - "Leállítás" - "Visszatekerés" - "Előretekerés" - "Összes ismétlése" - "Nincs ismétlés" - "Egy ismétlése" - "Véletlenszerű lejátszás" - Teljes képernyős mód + --> + + + "Előző szám" + "Következő szám" + "Szüneteltetés" + "Lejátszás" + "Leállítás" + "Visszatekerés" + "Előretekerés" + "Nincs ismétlés" + "Egy szám ismétlése" + "Összes szám ismétlése" + "Keverés" + "Teljes képernyős mód" + "Letöltés várólistára helyezve" + "Letöltés folyamatban" + "A letöltés befejeződött" + "Nem sikerült a letöltés" diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 09b05815e6..5bac891438 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -1,6 +1,5 @@ - - - - "Lagu sebelumnya" - "Lagu berikutnya" - "Jeda" - "Putar" - "Berhenti" - "Putar Ulang" - "Maju cepat" - "Ulangi Semua" - "Jangan Ulangi" - "Ulangi Satu" - "Acak" + --> + + + "Lagu sebelumnya" + "Lagu berikutnya" + "Jeda" + "Putar" + "Berhenti" + "Putar Ulang" + "Maju cepat" + "Jangan ulangi" + "Ulangi 1" + "Ulangi semua" + "Acak" + "Mode layar penuh" + "Download masih dalam antrean" + "Mendownload" + "Download selesai" + "Download gagal" diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 3300224fbb..7ce5dc5f45 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -1,6 +1,5 @@ - - - - "Traccia precedente" - "Traccia successiva" - "Metti in pausa" - "Riproduci" - "Interrompi" - "Riavvolgi" - "Avanti veloce" - "Ripeti tutti" - "Non ripetere nessuno" - "Ripeti uno" - "Riproduci casualmente" - Modalità a schermo intero + --> + + + "Traccia precedente" + "Traccia successiva" + "Pausa" + "Riproduci" + "Interrompi" + "Riavvolgi" + "Avanti veloce" + "Non ripetere nulla" + "Ripeti uno" + "Ripeti tutto" + "Riproduzione casuale" + "Modalità a schermo intero" + "Download aggiunto alla coda" + "Download" + "Download completato" + "Download non riuscito" diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index dd973af50b..036a124adc 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -1,6 +1,5 @@ - - - - "הרצועה הקודמת" - "הרצועה הבאה" - "השהה" - "הפעל" - "הפסק" - "הרץ אחורה" - "הרץ קדימה" - "חזור על הכל" - "אל תחזור על כלום" - "חזור על פריט אחד" - "ערבב" + --> + + + "הרצועה הקודמת" + "הרצועה הבאה" + "השהיה" + "הפעלה" + "הפסקה" + "הרצה אחורה" + "הרצה קדימה" + "אל תחזור על אף פריט" + "חזור על פריט אחד" + "חזור על הכול" + "ערבוב" + "מצב מסך מלא" + "ההורדה עדיין לא התחילה" + "מתבצעת הורדה" + "ההורדה הושלמה" + "ההורדה לא הושלמה" diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index 2e0f23a78f..d388bf6f05 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -1,6 +1,5 @@ - - - - "前のトラック" - "次のトラック" - "一時停止" - "再生" - "停止" - "巻き戻し" - "早送り" - "全曲を繰り返し" - "繰り返しなし" - "1曲を繰り返し" - "シャッフル" - フルスクリーンモード + --> + + + "前のトラック" + "次のトラック" + "一時停止" + "再生" + "停止" + "巻き戻し" + "早送り" + "リピートなし" + "1 曲をリピート" + "全曲をリピート" + "シャッフル" + "全画面モード" + "ダウンロードを待機しています" + "ダウンロードしています" + "ダウンロードが完了しました" + "ダウンロードに失敗しました" diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 32d3deeb9e..3acf944f22 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -1,6 +1,5 @@ - - - - "이전 트랙" - "다음 트랙" - "일시중지" - "재생" - "중지" - "되감기" - "빨리 감기" - "전체 반복" - "반복 안함" - "한 항목 반복" - "셔플" - 전체 화면 모드 + --> + + + "이전 트랙" + "다음 트랙" + "일시중지" + "재생" + "중지" + "되감기" + "빨리 감기" + "반복 안함" + "현재 미디어 반복" + "모두 반복" + "셔플" + "전체화면 모드" + "다운로드 대기 중" + "다운로드하는 중" + "다운로드 완료" + "다운로드 실패" diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index 091f2384b2..f8b5366eb5 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -1,6 +1,5 @@ - - - - "Ankstesnis takelis" - "Kitas takelis" - "Pristabdyti" - "Leisti" - "Stabdyti" - "Sukti atgal" - "Sukti pirmyn" - "Kartoti viską" - "Nekartoti nieko" - "Kartoti vieną" - "Maišyti" - Viso ekrano režimas + --> + + + "Ankstesnis takelis" + "Kitas takelis" + "Pristabdyti" + "Leisti" + "Sustabdyti" + "Sukti atgal" + "Sukti pirmyn" + "Nekartoti nieko" + "Kartoti vieną" + "Kartoti viską" + "Maišyti" + "Viso ekrano režimas" + "Atsisiunč. elem. laukia eilėje" + "Atsisiunčiama" + "Atsisiuntimo procesas baigtas" + "Nepavyko 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 af982587bf..3f5f696f86 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -1,6 +1,5 @@ - - - - "Iepriekšējais ieraksts" - "Nākamais ieraksts" - "Pārtraukt" - "Atskaņot" - "Apturēt" - "Attīt atpakaļ" - "Ātri patīt" - "Atkārtot visu" - "Neatkārtot nevienu" - "Atkārtot vienu" - "Atskaņot jauktā secībā" - Pilnekrāna režīms + --> + + + "Iepriekšējais ieraksts" + "Nākamais ieraksts" + "Pauzēt" + "Atskaņot" + "Apturēt" + "Attīt atpakaļ" + "Pārtīt uz priekšu" + "Neatkārtot nevienu" + "Atkārtot vienu" + "Atkārtot visu" + "Atskaņot jauktā secībā" + "Pilnekrāna režīms" + "Lejupielāde gaida rindā" + "Notiek lejupielāde" + "Lejupielāde ir pabeigta" + "Lejupielāde neizdevās" diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 370c759b84..7e7d580e63 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -1,6 +1,5 @@ - - - - "Forrige spor" - "Neste spor" - "Sett på pause" - "Spill av" - "Stopp" - "Tilbakespoling" - "Fremoverspoling" - "Gjenta alle" - "Ikke gjenta noen" - "Gjenta én" - "Spill av i tilfeldig rekkefølge" + --> + + + "Forrige spor" + "Neste spor" + "Sett på pause" + "Spill av" + "Stopp" + "Spol tilbake" + "Spol forover" + "Ikke gjenta noen" + "Gjenta én" + "Gjenta alle" + "Tilfeldig rekkefølge" + "Fullskjermmodus" + "Nedlasting står i kø" + "Laster ned" + "Nedlastingen er fullført" + "Nedlastingen mislyktes" diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index a67ab2968c..ad24b79908 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -1,6 +1,5 @@ - - - - "Vorig nummer" - "Volgend nummer" - "Onderbreken" - "Afspelen" - "Stoppen" - "Terugspoelen" - "Vooruitspoelen" - "Alles herhalen" - "Niet herhalen" - "Eén herhalen" - "Shuffle" + --> + + + "Vorige track" + "Volgende track" + "Pauzeren" + "Afspelen" + "Stoppen" + "Terugspoelen" + "Vooruitspoelen" + "Niets herhalen" + "Eén herhalen" + "Alles herhalen" + "Shuffle" + "Modus \'Volledig scherm\'" + "Download in de wachtrij" + "Downloaden" + "Downloaden voltooid" + "Downloaden mislukt" diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 981aa17543..45f930a18c 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -1,6 +1,5 @@ - - - - "Poprzedni utwór" - "Następny utwór" - "Wstrzymaj" - "Odtwórz" - "Zatrzymaj" - "Przewiń do tyłu" - "Przewiń do przodu" - "Powtórz wszystkie" - "Nie powtarzaj" - "Powtórz jeden" - "Odtwarzaj losowo" - Tryb pełnoekranowy + --> + + + "Poprzedni utwór" + "Następny utwór" + "Wstrzymaj" + "Odtwórz" + "Zatrzymaj" + "Przewiń do tyłu" + "Przewiń do przodu" + "Nie powtarzaj" + "Powtórz jeden" + "Powtórz wszystkie" + "Odtwarzanie losowe" + "Tryb pełnoekranowy" + "W kolejce pobierania" + "Pobieranie" + "Zakończono pobieranie" + "Nie udało się pobrać" 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 f0c3770c51..d728f17347 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -1,6 +1,5 @@ - - - - "Faixa anterior" - "Faixa seguinte" - "Interromper" - "Reproduzir" - "Parar" - "Rebobinar" - "Avançar" - "Repetir tudo" - "Não repetir" - "Repetir um" - "Reproduzir aleatoriamente" - Modo de ecrã inteiro + --> + + + "Faixa anterior" + "Faixa seguinte" + "Colocar em pausa" + "Reproduzir" + "Parar" + "Recuar" + "Avançar" + "Não repetir nenhum" + "Repetir um" + "Repetir tudo" + "Reproduzir aleatoriamente" + "Modo de ecrã inteiro" + "Transfer. em fila de espera" + "A transferir…" + "Transferência concluída" + "Falha na transferência" diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 8441e4e1cc..90595f0f81 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -1,6 +1,5 @@ - - - - "Faixa anterior" - "Próxima faixa" - "Pausar" - "Reproduzir" - "Parar" - "Retroceder" - "Avançar" - "Repetir tudo" - "Não repetir" - "Repetir uma" - "Reproduzir aleatoriamente" + --> + + + "Faixa anterior" + "Próxima faixa" + "Pausar" + "Reproduzir" + "Parar" + "Retroceder" + "Avançar" + "Não repetir" + "Repetir uma" + "Repetir tudo" + "Aleatório" + "Modo de tela cheia" + "Item na fila de download" + "Fazendo download" + "Download concluído" + "Falha no download" diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 6b8644e30a..2f173f53ff 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -1,6 +1,5 @@ - - - - "Melodia anterioară" - "Melodia următoare" - "Pauză" - "Redați" - "Opriți" - "Derulați" - "Derulați rapid înainte" - "Repetați toate" - "Repetați niciuna" - "Repetați unul" - "Redați aleatoriu" + --> + + + "Melodia anterioară" + "Următoarea înregistrare" + "Întrerupeți" + "Redați" + "Opriți" + "Derulați înapoi" + "Derulați rapid înainte" + "Nu repetați niciunul" + "Repetați unul" + "Repetați-le pe toate" + "Redați aleatoriu" + "Modul Ecran complet" + "Descărcarea este în lista de așteptare" + "Se descarcă" + "Descărcarea a fost finalizată" + "Descărcarea nu a reușit" diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index 51d11d6371..1daee396cc 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -1,6 +1,5 @@ - - - - "Предыдущий трек" - "Следующий трек" - "Приостановить" - "Воспроизвести" - "Остановить" - "Перемотать назад" - "Перемотать вперед" - "Повторять все" - "Не повторять" - "Повторять один элемент" - "Перемешать" + --> + + + "Предыдущий трек" + "Следующий трек" + "Приостановить" + "Воспроизвести" + "Остановить" + "Перемотать назад" + "Перемотать вперед" + "Не повторять" + "Повторять трек" + "Повторять все" + "Перемешать" + "Полноэкранный режим" + "В очереди на скачивание" + "Загрузка файлов" + "Скачивание завершено" + "Ошибка скачивания" diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index a289e89d34..15cc6ab00e 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -1,6 +1,5 @@ - - - - "Predchádzajúca stopa" - "Ďalšia stopa" - "Pozastaviť" - "Prehrať" - "Zastaviť" - "Pretočiť späť" - "Pretočiť dopredu" - "Opakovať všetko" - "Neopakovať" - "Opakovať jednu položku" - "Náhodne prehrávať" - Režim celej obrazovky + --> + + + "Predchádzajúca skladba" + "Ďalšia skladba" + "Pozastaviť" + "Prehrať" + "Zastaviť" + "Pretočiť späť" + "Pretočiť dopredu" + "Neopakovať" + "Opakovať jednu" + "Opakovať všetko" + "Náhodne prehrávať" + "Režim celej obrazovky" + "Sťahovanie je v poradí" + "Sťahuje sa" + "Sťahovanie bolo dokončené" + "Nepodarilo sa stiahnuť" diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 8ed731b0d3..19ad13aa81 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -1,6 +1,5 @@ - - - - "Prejšnja skladba" - "Naslednja skladba" - "Zaustavi" - "Predvajaj" - "Ustavi" - "Previj nazaj" - "Previj naprej" - "Ponovi vse" - "Ne ponovi" - "Ponovi eno" - "Naključno predvajaj" + --> + + + "Prejšnja skladba" + "Naslednja skladba" + "Zaustavitev" + "Predvajanje" + "Ustavitev" + "Previjanje nazaj" + "Previjanje naprej" + "Brez ponavljanja" + "Ponavljanje ene" + "Ponavljanje vseh" + "Naključno predvajanje" + "Celozaslonski način" + "Prenos je v čakalni vrsti" + "Prenašanje" + "Prenos je končan" + "Prenos ni uspel" diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 9cff134a61..71fbc7e5c2 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -1,6 +1,5 @@ - - - - "Претходна песма" - "Следећа песма" - "Пауза" - "Пусти" - "Заустави" - "Премотај уназад" - "Премотај унапред" - "Понови све" - "Понављање је искључено" - "Понови једну" - "Пусти насумично" - Режим целог екрана + --> + + + "Претходна песма" + "Следећа песма" + "Паузирај" + "Пусти" + "Заустави" + "Премотај уназад" + "Премотај унапред" + "Не понављај ниједну" + "Понови једну" + "Понови све" + "Пусти насумично" + "Режим целог екрана" + "Преузимање је на чекању" + "Преузимање" + "Преузимање је завршено" + "Преузимање није успело" diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index b8fc7a1fff..1f0793ed23 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -1,6 +1,5 @@ - - - - "Föregående spår" - "Nästa spår" - "Pausa" - "Spela upp" - "Avbryt" - "Spola tillbaka" - "Snabbspola framåt" - "Upprepa alla" - "Upprepa inga" - "Upprepa en" - "Blanda" - Helskärmsläge + --> + + + "Föregående spår" + "Nästa spår" + "Pausa" + "Spela upp" + "Stoppa" + "Spola tillbaka" + "Snabbspola framåt" + "Upprepa inga" + "Upprepa en" + "Upprepa alla" + "Blanda spår" + "Helskärmsläge" + "Nedladdningen har köplacerats" + "Laddar ned" + "Nedladdningen är klar" + "Nedladdningen misslyckades" diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index 4451ad3c2b..eddaaf8264 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -1,6 +1,5 @@ - - - - "Wimbo uliotangulia" - "Wimbo unaofuata" - "Sitisha" - "Cheza" - "Simamisha" - "Rudisha nyuma" - "Peleka mbele kwa kasi" - "Rudia zote" - "Usirudie Yoyote" - "Rudia Moja" - "Changanya" - Hali ya skrini kamili + --> + + + "Wimbo uliotangulia" + "Wimbo unaofuata" + "Sitisha" + "Cheza" + "Simamisha" + "Rudisha nyuma" + "Sogeza mbele haraka" + "Usirudie yoyote" + "Rudia moja" + "Rudia zote" + "Changanya" + "Hali ya skrini nzima" + "Inasubiri kupakuliwa" + "Inapakua" + "Imepakuliwa" + "Imeshindwa kupakua" diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index 664900e7da..d235e0349a 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -1,6 +1,5 @@ - - - - "แทร็กก่อนหน้า" - "แทร็กถัดไป" - "หยุดชั่วคราว" - "เล่น" - "หยุด" - "กรอกลับ" - "กรอไปข้างหน้า" - "เล่นซ้ำทั้งหมด" - "ไม่เล่นซ้ำ" - "เล่นซ้ำรายการเดียว" - "สุ่มเพลง" - โหมดเต็มหน้าจอ + --> + + + "แทร็กก่อนหน้า" + "แทร็กถัดไป" + "หยุด" + "เล่น" + "หยุด" + "กรอกลับ" + "กรอไปข้างหน้า" + "ไม่เล่นซ้ำ" + "เล่นซ้ำเพลงเดียว" + "เล่นซ้ำทั้งหมด" + "สุ่ม" + "โหมดเต็มหน้าจอ" + "การดาวน์โหลดอยู่ในคิว" + "กำลังดาวน์โหลด" + "การดาวน์โหลดเสร็จสมบูรณ์" + "การดาวน์โหลดล้มเหลว" diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 471191a81a..b478e9d52f 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -1,6 +1,5 @@ - - - - "Nakaraang track" - "Susunod na track" - "I-pause" - "I-play" - "Ihinto" - "I-rewind" - "I-fast forward" - "Ulitin Lahat" - "Walang Uulitin" - "Ulitin ang Isa" - "I-shuffle" - Fullscreen mode + --> + + + "Nakaraang track" + "Susunod na track" + "I-pause" + "I-play" + "Ihinto" + "I-rewind" + "I-fast forward" + "Walang uulitin" + "Mag-ulit ng isa" + "Ulitin lahat" + "I-shuffle" + "Fullscreen mode" + "Naka-queue ang download" + "Nagda-download" + "Tapos na ang pag-download" + "Hindi na-download" diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index cd1bfc5444..e9251e4c79 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -1,6 +1,5 @@ - - - - "Önceki parça" - "Sonraki parça" - "Duraklat" - "Çal" - "Durdur" - "Geri sar" - "İleri sar" - "Tümünü Tekrarla" - "Hiçbirini Tekrarlama" - "Birini Tekrarla" - "Karıştır" + --> + + + "Önceki parça" + "Sonraki parça" + "Duraklat" + "Çal" + "Durdur" + "Geri sar" + "İleri sar" + "Hiçbirini tekrarlama" + "Birini tekrarla" + "Tümünü tekrarla" + "Karıştır" + "Tam ekran modu" + "İndirme işlemi sıraya alındı" + "İndiriliyor" + "İndirme işlemi tamamlandı" + "İndirilemedi" diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 36bfca2a34..e6c2289828 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -1,6 +1,5 @@ - - - - "Попередня композиція" - "Наступна композиція" - "Пауза" - "Відтворити" - "Зупинити" - "Перемотати назад" - "Перемотати вперед" - "Повторити все" - "Не повторювати" - "Повторити один елемент" - "Перемішати" - Повноекранний режим + --> + + + "Попередня композиція" + "Наступна композиція" + "Призупинити" + "Відтворити" + "Припинити" + "Перемотати назад" + "Перемотати вперед" + "Не повторювати" + "Повторити 1" + "Повторити всі" + "Перемішати" + "Повноекранний режим" + "Завантаження розміщено в черзі" + "Завантажується" + "Завантаження завершено" + "Не вдалося завантажити" diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 748de96949..939206b7b9 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -1,6 +1,5 @@ - - - - "Bản nhạc trước" - "Bản nhạc tiếp theo" - "Tạm dừng" - "Phát" - "Ngừng" - "Tua lại" - "Tua đi" - "Lặp lại tất cả" - "Không lặp lại" - "Lặp lại một mục" - "Trộn bài" - Chế độ toàn màn hình + --> + + + "Bản nhạc trước" + "Bản nhạc tiếp theo" + "Tạm dừng" + "Phát" + "Dừng" + "Tua lại" + "Tua đi" + "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" + "Chế độ toàn màn hình" + "Đã đưa tài nguyên đã tải xuống vào hàng đợi" + "Đang tải xuống" + "Đã hoàn tất tải xuống" + "Không tải xuống được" 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 d357152a64..f2749db0af 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -1,6 +1,5 @@ - - - - "上一曲" - "下一曲" - "暂停" - "播放" - "停止" - "快退" - "快进" - "重复播放全部" - "不重复播放" - "重复播放单个视频" - "随机播放" - 全屏模式 + --> + + + "上一曲" + "下一曲" + "暂停" + "播放" + "停止" + "快退" + "快进" + "不重复播放" + "重复播放一项" + "全部重复播放" + "随机播放" + "全屏模式" + "已加入待下载队列" + "正在下载" + "下载完毕" + "下载失败" 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 3a26b8b5f0..fadf8bf976 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -1,6 +1,5 @@ - - - - "上一首曲目" - "下一首曲目" - "暫停" - "播放" - "停止" - "倒帶" - "向前快轉" - "重複播放所有媒體項目" - "不重複播放任何媒體項目" - "重複播放一個媒體項目" - "隨機播放" - 全螢幕模式 + --> + + + "上一首曲目" + "下一首曲目" + "暫停" + "播放" + "停止" + "倒轉" + "向前快轉" + "不重複播放" + "重複播放單一項目" + "全部重複播放" + "隨機播放" + "全螢幕模式" + "已加入下載列" + "正在下載" + "下載完畢" + "下載失敗" 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 6f87d143ad..0679f360d1 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -1,6 +1,5 @@ - - - - "上一首曲目" - "下一首曲目" - "暫停" - "播放" - "停止" - "倒轉" - "快轉" - "重複播放所有媒體項目" - "不重複播放" - "重複播放單一媒體項目" - "隨機播放" - 全螢幕模式 + --> + + + "上一首曲目" + "下一首曲目" + "暫停" + "播放" + "停止" + "倒轉" + "快轉" + "不重複播放" + "重複播放單一項目" + "重複播放所有項目" + "隨機播放" + "全螢幕模式" + "已排入下載佇列" + "下載中" + "下載完成" + "無法下載" diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index aff66ba0cf..5eb5bf9807 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -1,6 +1,5 @@ - - - - "Ithrekhi yangaphambilini" - "Ithrekhi elandelayo" - "Misa isikhashana" - "Dlala" - "Misa" - "Buyisela emumva" - "Ukudlulisa ngokushesha" - "Phinda konke" - "Ungaphindi lutho" - "Phida okukodwa" - "Shova" - Imodi yesikrini esiphelele + --> + + + "Ithrekhi yangaphambilini" + "Ithrekhi elandelayo" + "Phumula" + "Dlala" + "Misa" + "Buyisela emuva" + "Dlulisela phambili" + "Phinda okungekho" + "Phinda okukodwa" + "Phinda konke" + "Shova" + "Imodi yesikrini esigcwele" + "Ukulanda kukulayini" + "Iyalanda" + "Ukulanda kuqedile" + "Ukulanda kuhlulekile" From 4cb2d255abcfdd8f83d4eb481072ba2df517bc62 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 13 Feb 2018 01:05:08 -0800 Subject: [PATCH 032/376] Fix handling of ad tags where ad groups are out of order IMA's cue points may not be in order, so sort them. It looks like IMA events use time ordered ad indices, so it is not necessary to map between the original cue point order and the time order. Issue: #3716 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185495798 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8e8b4c8d1f..230aa57904 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -88,6 +88,8 @@ * Add support for playing non-Extractor content MediaSources in the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). + * Fix handling of ad tags where ad groups are out of order + ([#3716](https://github.com/google/ExoPlayer/issues/3716)). * `EventLogger` moved from the demo app into the core library. * Fix ANR issue on Huawei P8 Lite ([#3724](https://github.com/google/ExoPlayer/issues/3724)). 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 d5e120afe7..b4bbded5b9 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 @@ -970,11 +970,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A int count = cuePoints.size(); long[] adGroupTimesUs = new long[count]; + int adGroupIndex = 0; for (int i = 0; i < count; i++) { double cuePoint = cuePoints.get(i); - adGroupTimesUs[i] = - cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint); + if (cuePoint == -1.0) { + adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; + } else { + adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint); + } } + // Cue points may be out of order, so sort them. + Arrays.sort(adGroupTimesUs, 0, adGroupIndex); return adGroupTimesUs; } From 51a88ac59d4fcf0cec4829b8353b9668a3349cf0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 13 Feb 2018 01:58:41 -0800 Subject: [PATCH 033/376] Add Moto C+ to surface switching workaround. Issue:#3835 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185501181 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 1 file changed, 2 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 f26db1054f..3fe863a96b 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 @@ -1091,7 +1091,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { && "OMX.qcom.video.decoder.avc".equals(name)) || (("tcl_eu".equals(Util.DEVICE) || "SVP-DTV15".equals(Util.DEVICE) - || "BRAVIA_ATV2".equals(Util.DEVICE)) + || "BRAVIA_ATV2".equals(Util.DEVICE) + || "panell_s".equals(Util.DEVICE)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) || ("OMX.k3.video.decoder.avc".equals(name) && "ALE-L21".equals(Util.MODEL)); } From 73e3e16949037cad02335582fafc760097d9e661 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 13 Feb 2018 07:18:07 -0800 Subject: [PATCH 034/376] Fix SmoothStreaming manifest url for downloading ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185526653 --- .../exoplayer2/source/dash/DashUtil.java | 6 ++-- .../source/hls/offline/HlsDownloader.java | 6 ++-- .../source/smoothstreaming/SsMediaSource.java | 6 ++-- .../smoothstreaming/manifest/SsUtil.java | 33 +++++++++++++++++++ .../smoothstreaming/offline/SsDownloader.java | 9 +++-- 5 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java 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 57632225a5..2227044da7 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 @@ -53,10 +53,8 @@ public final class DashUtil { */ public static DashManifest loadManifest(DataSource dataSource, Uri uri) throws IOException { - DataSpec dataSpec = new DataSpec(uri, - DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); - ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, - C.DATA_TYPE_MANIFEST, new DashManifestParser()); + ParsingLoadable loadable = + new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new DashManifestParser()); loadable.load(); return loadable.getResult(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index a7bf35f2d1..3d14283e86 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -104,10 +104,8 @@ public final class HlsDownloader extends SegmentDownloader loadable = new ParsingLoadable<>(dataSource, dataSpec, - C.DATA_TYPE_MANIFEST, new HlsPlaylistParser()); + ParsingLoadable loadable = + new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new HlsPlaylistParser()); loadable.load(); return loadable.getResult(); } 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 c20ab6b6ae..da9024a5b5 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 @@ -36,13 +36,13 @@ 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; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -403,9 +403,7 @@ public final class SsMediaSource implements MediaSource, MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; - this.manifestUri = manifestUri == null ? null - : Util.toLowerInvariant(manifestUri.getLastPathSegment()).matches("manifest(\\(.+\\))?") - ? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest"); + this.manifestUri = manifestUri == null ? null : SsUtil.fixManifestUri(manifestUri); this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java new file mode 100644 index 0000000000..4adf6acff7 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsUtil.java @@ -0,0 +1,33 @@ +/* + * 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.source.smoothstreaming.manifest; + +import android.net.Uri; +import com.google.android.exoplayer2.util.Util; + +/** SmoothStreaming related utility methods. */ +public final class SsUtil { + + /** Returns a fixed SmoothStreaming client manifest {@link Uri}. */ + public static Uri fixManifestUri(Uri manifestUri) { + if (Util.toLowerInvariant(manifestUri.getLastPathSegment()).matches("manifest(\\(.+\\))?")) { + return manifestUri; + } + return Uri.withAppendedPath(manifestUri, "Manifest"); + } + + private SsUtil() {} +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 5b97101fc6..12cfe2ee36 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.offline.SegmentDownloader; 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; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -64,7 +65,7 @@ public final class SsDownloader extends SegmentDownloader * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) */ public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { - super(manifestUri, constructorHelper); + super(SsUtil.fixManifestUri(manifestUri), constructorHelper); } @Override @@ -82,10 +83,8 @@ public final class SsDownloader extends SegmentDownloader @Override protected SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { - DataSpec dataSpec = new DataSpec(uri, - DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); - ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, - C.DATA_TYPE_MANIFEST, new SsManifestParser()); + ParsingLoadable loadable = + new ParsingLoadable<>(dataSource, uri, C.DATA_TYPE_MANIFEST, new SsManifestParser()); loadable.load(); return loadable.getResult(); } From 2dab21ab4eaad67d332f16de212f29e4e8b1721d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 13 Feb 2018 10:14:17 -0800 Subject: [PATCH 035/376] Add Y611 to setOutputSurfaceWorkaround Issue: #3724 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185548632 --- RELEASENOTES.md | 5 +++-- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 230aa57904..34de0baca8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -91,8 +91,9 @@ * Fix handling of ad tags where ad groups are out of order ([#3716](https://github.com/google/ExoPlayer/issues/3716)). * `EventLogger` moved from the demo app into the core library. -* Fix ANR issue on Huawei P8 Lite - ([#3724](https://github.com/google/ExoPlayer/issues/3724)). +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II and Moto C+ + ([#3724](https://github.com/google/ExoPlayer/issues/3724), + [#3835](https://github.com/google/ExoPlayer/issues/3835)). * Fix potential NPE when removing media sources from a DynamicConcatenatingMediaSource ([#3796](https://github.com/google/ExoPlayer/issues/3796)). 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 3fe863a96b..a897f3194c 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 @@ -1094,7 +1094,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || "BRAVIA_ATV2".equals(Util.DEVICE) || "panell_s".equals(Util.DEVICE)) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) - || ("OMX.k3.video.decoder.avc".equals(name) && "ALE-L21".equals(Util.MODEL)); + || (("ALE-L21".equals(Util.MODEL) || "CAM-L21".equals(Util.MODEL)) + && "OMX.k3.video.decoder.avc".equals(name)); } /** From b9f9232b9dcfed32b403328f13307c9a47dd9589 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Feb 2018 00:41:53 -0800 Subject: [PATCH 036/376] Release Extractors on the loading thread Releasing the player released the internal playback thread once renderers were released. Releasing a MediaPeriod queued a Loader.ReleaseTask on the loading thread which would post back to the playback thread. If the playback thread had been quit by the time this happened, the release task wouldn't be run. Release on the loading thread instead of the playback thread. This avoids needing to block releasing the player until the loading threads have ended, and ensures that release tasks will run eventually. As part of this change, ExtractorMediaPeriod's call to Extractor.release will now run on the loading thread (which means that all Extractor methods are called on that thread) and other cleanup in ReleaseCallback will run on the loading thread instead of the playback thread. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185651320 --- RELEASENOTES.md | 3 ++ .../source/ExtractorMediaPeriod.java | 6 ++-- .../source/chunk/ChunkSampleStream.java | 19 +++++------- .../android/exoplayer2/upstream/Loader.java | 30 +++++-------------- .../source/dash/DashMediaPeriod.java | 7 +++-- .../source/hls/HlsSampleStreamWrapper.java | 4 +-- 6 files changed, 29 insertions(+), 40 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 34de0baca8..c43ad5c19e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,9 @@ ([#2643](https://github.com/google/ExoPlayer/issues/2643)). * Check `sys.display-size` on Philips ATVs ([#3807](https://github.com/google/ExoPlayer/issues/3807)). +* Release `Extractor`s on the loading thread to avoid potentially leaking + resources when the playback thread has quit by the time the loading task has + completed. ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f4f59115eb..c771188e3b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -179,24 +179,24 @@ import java.util.Arrays; } public void release() { - boolean releasedSynchronously = loader.release(this); - if (prepared && !releasedSynchronously) { + if (prepared) { // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } } + loader.release(this); handler.removeCallbacksAndMessages(null); released = true; } @Override public void onLoaderReleased() { - extractorHolder.release(); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); } + extractorHolder.release(); } @Override 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 e0c5d35996..7096c84c5e 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 @@ -73,7 +73,7 @@ public class ChunkSampleStream implements SampleStream, S private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; - private ReleaseCallback releaseCallback; + private @Nullable ReleaseCallback releaseCallback; private long pendingResetPositionUs; private long lastSeekPositionUs; /* package */ long decodeOnlyUntilPositionUs; @@ -306,20 +306,17 @@ public class ChunkSampleStream implements SampleStream, S *

This method should be called when the stream is no longer required. Either this method or * {@link #release()} can be used to release this stream. * - * @param callback A callback to be called when the release ends. Will be called synchronously - * from this method if no load is in progress, or asynchronously once the load has been - * canceled otherwise. + * @param callback An optional callback to be called on the loading thread once the loader has + * been released. */ public void release(@Nullable ReleaseCallback callback) { this.releaseCallback = callback; - boolean releasedSynchronously = loader.release(this); - if (!releasedSynchronously) { - // Discard as much as we can synchronously. - primarySampleQueue.discardToEnd(); - for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { - embeddedSampleQueue.discardToEnd(); - } + // Discard as much as we can synchronously. + primarySampleQueue.discardToEnd(); + for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { + embeddedSampleQueue.discardToEnd(); } + loader.release(this); } @Override 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 9e495f42bf..a118f10784 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 @@ -20,6 +20,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.TraceUtil; @@ -197,25 +198,17 @@ public final class Loader implements LoaderErrorThrower { * Releases the {@link Loader}. This method should be called when the {@link Loader} is no longer * required. * - * @param callback A callback to be called when the release ends. Will be called synchronously - * from this method if no load is in progress, or asynchronously once the load has been - * canceled otherwise. May be null. - * @return True if {@code callback} was called synchronously. False if it will be called - * asynchronously or if {@code callback} is null. + * @param callback An optional callback to be called on the loading thread once the loader has + * been released. */ - public boolean release(ReleaseCallback callback) { - boolean callbackInvoked = false; + public void release(@Nullable ReleaseCallback callback) { if (currentTask != null) { currentTask.cancel(true); - if (callback != null) { - downloadExecutorService.execute(new ReleaseTask(callback)); - } - } else if (callback != null) { - callback.onLoaderReleased(); - callbackInvoked = true; + } + if (callback != null) { + downloadExecutorService.execute(new ReleaseTask(callback)); } downloadExecutorService.shutdown(); - return callbackInvoked; } // LoaderErrorThrower implementation. @@ -419,7 +412,7 @@ public final class Loader implements LoaderErrorThrower { } - private static final class ReleaseTask extends Handler implements Runnable { + private static final class ReleaseTask implements Runnable { private final ReleaseCallback callback; @@ -429,13 +422,6 @@ public final class Loader implements LoaderErrorThrower { @Override public void run() { - if (getLooper().getThread().isAlive()) { - sendEmptyMessage(0); - } - } - - @Override - public void handleMessage(Message msg) { callback.onLoaderReleased(); } 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 d6d6ca821c..00baf15228 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 @@ -153,7 +153,7 @@ import java.util.List; // ChunkSampleStream.ReleaseCallback implementation. @Override - public void onSampleStreamReleased(ChunkSampleStream stream) { + public synchronized void onSampleStreamReleased(ChunkSampleStream stream) { PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream); if (trackEmsgHandler != null) { trackEmsgHandler.release(); @@ -565,7 +565,10 @@ import java.util.List; positionUs, minLoadableRetryCount, eventDispatcher); - trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); + synchronized (this) { + // The map is also accessed on the loading thread so synchronize access. + trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); + } return stream; } 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 508f2f0f2f..f027ba5b05 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 @@ -386,14 +386,14 @@ import java.util.Arrays; } public void release() { - boolean releasedSynchronously = loader.release(this); - if (prepared && !releasedSynchronously) { + if (prepared) { // Discard as much as we can synchronously. We only do this if we're prepared, since otherwise // sampleQueues may still be being modified by the loading thread. for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.discardToEnd(); } } + loader.release(this); handler.removeCallbacksAndMessages(null); released = true; } From 5b6344a006cb4745869b6f8f9f834d988a49756e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Feb 2018 01:35:45 -0800 Subject: [PATCH 037/376] Skip ads for which the media failed to prepare Also make ad group skipping more robust. After calling onError for an ad, IMA will sometimes trigger an ad group load error, so this needs to be handled in a way that allows some ads to be loaded already for the ad group. This change also fixes calculation of the expected ad index to take into account whether the position is being faked to trigger loading an ad or is the actual player position. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185655844 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 89 +++++++++++++++---- .../source/DeferredMediaPeriod.java | 48 +++++++++- .../exoplayer2/source/ads/AdsLoader.java | 9 ++ .../exoplayer2/source/ads/AdsMediaSource.java | 23 +++++ 5 files changed, 151 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c43ad5c19e..66630054d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -90,6 +90,8 @@ ([#3676](https://github.com/google/ExoPlayer/issues/3676)). * Fix handling of ad tags where ad groups are out of order ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Propagate ad media preparation errors to IMA so that the ads can be + skipped. * `EventLogger` moved from the demo app into the core library. * Fix ANR issue on the Huawei P8 Lite, Huawei Y6II and Moto C+ ([#3724](https://github.com/google/ExoPlayer/issues/3724), 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 b4bbded5b9..d714eed98c 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 @@ -242,10 +242,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private int playingAdIndexInAdGroup; /** - * If a content period has finished but IMA has not yet sent an ad event with - * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of - * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to - * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + * Whether there's a pending ad preparation error which IMA needs to be notified of when it + * transitions from playing content to playing the ad. + */ + private boolean shouldNotifyAdPrepareError; + /** + * If a content period has finished but IMA has not yet sent an ad event with {@link + * AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of {@link + * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine + * a fake, increasing content position. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressElapsedRealtimeMs; /** @@ -432,6 +437,42 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } + @Override + public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) { + if (player == null) { + return; + } + if (DEBUG) { + Log.d( + TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); + } + if (imaAdState == IMA_AD_STATE_NONE) { + // Send IMA a content position at the ad group so that it will try to play it, at which point + // we can notify that it failed to load. + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { + fakeContentProgressOffsetMs = contentDurationMs; + } + shouldNotifyAdPrepareError = true; + } else { + // We're already playing an ad. + if (adIndexInAdGroup > playingAdIndexInAdGroup) { + // Mark the playing ad as ended so we can notify the error on the next ad and remove it, + // which means that the ad after will load (if any). + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); + } + } + playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(); + } + } + adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); + updateAdPlaybackState(); + } + // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. @Override @@ -566,17 +607,21 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (pendingContentPositionMs != C.TIME_UNSET) { sentPendingContentPositionMs = true; contentPositionMs = pendingContentPositionMs; + expectedAdGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); } else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) { long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; + expectedAdGroupIndex = + adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); } else if (imaAdState == IMA_AD_STATE_NONE && hasContentDuration) { contentPositionMs = player.getCurrentPosition(); + // Keep track of the ad group index that IMA will load for the current content position. + expectedAdGroupIndex = + adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); } else { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } - // Keep track of the ad group index that IMA will load for the current content position. - expectedAdGroupIndex = - adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; return new VideoProgressUpdate(contentPositionMs, contentDurationMs); } @@ -641,6 +686,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onPlay(); } + if (shouldNotifyAdPrepareError) { + shouldNotifyAdPrepareError = false; + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(); + } + } break; case IMA_AD_STATE_PAUSED: imaAdState = IMA_AD_STATE_PLAYING; @@ -748,7 +799,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onPlayerError(ExoPlaybackException error) { - if (playingAd) { + if (imaAdState != IMA_AD_STATE_NONE) { for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onError(); } @@ -776,6 +827,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (newAdGroupIndex != C.INDEX_UNSET) { sentPendingContentPositionMs = false; pendingContentPositionMs = positionMs; + if (newAdGroupIndex != adGroupIndex) { + shouldNotifyAdPrepareError = false; + } } } } else { @@ -907,16 +961,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A int adGroupIndex = this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - // Ad group load error can be notified more than once, so check if it was already handled. - if (adGroup.count == C.LENGTH_UNSET - || adGroup.states[0] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad group " + adGroupIndex + " as it failed to load"); - } + if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = - adPlaybackState.withAdCount(adGroupIndex, 1).withAdLoadError(adGroupIndex, 0); - updateAdPlaybackState(); + adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); } + for (int i = 0; i < adGroup.count; i++) { + if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { + if (DEBUG) { + Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); + } + adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); + } + } + updateAdPlaybackState(); } private void checkForContentComplete() { 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 1895f10d53..e13a563d50 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,7 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -29,6 +30,15 @@ import java.io.IOException; */ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + /** Listener for preparation errors. */ + public interface PrepareErrorListener { + + /** + * Called the first time an error occurs while refreshing source info or preparing the period. + */ + void onPrepareError(IOException exception); + } + public final MediaSource mediaSource; private final MediaPeriodId id; @@ -37,13 +47,33 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb private MediaPeriod mediaPeriod; private Callback callback; private long preparePositionUs; + private @Nullable PrepareErrorListener listener; + private boolean notifiedPrepareError; + /** + * Creates a new deferred media period. + * + * @param mediaSource The media source to wrap. + * @param id The identifier for the media period to create when {@link #createPeriod()} is called. + * @param allocator The allocator used to create the media period. + */ public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { this.id = id; this.allocator = allocator; this.mediaSource = mediaSource; } + /** + * Sets a listener for preparation errors. + * + * @param listener An listener to be notified of media period preparation errors. If a listener is + * set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first + * preparation error (if any) to the listener. + */ + public void setPrepareErrorListener(PrepareErrorListener listener) { + this.listener = listener; + } + /** * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} @@ -76,10 +106,20 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); + try { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } catch (final IOException e) { + if (listener == null) { + throw e; + } + if (!notifiedPrepareError) { + notifiedPrepareError = true; + listener.onPrepareError(e); + } } } 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 c2dfd91301..91111ec0ea 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 @@ -103,4 +103,13 @@ public interface AdsLoader { */ void release(); + /** + * Notifies the ads loader that the player was not able to prepare media for a given ad. + * Implementations should update the ad playback state as the specified ad has failed to load. + * + * @param adGroupIndex The index of the ad group. + * @param adIndexInAdGroup The index of the ad in the ad group. + * @param exception The preparation error. + */ + void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception); } 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 9ddbac1007..8c4d85ff4c 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 @@ -236,6 +236,8 @@ public final class AdsMediaSource extends CompositeMediaSource { MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + deferredMediaPeriod.setPrepareErrorListener( + new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); if (mediaPeriods == null) { deferredMediaPeriod.createPeriod(); @@ -433,4 +435,25 @@ public final class AdsMediaSource extends CompositeMediaSource { } + private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { + + private final int adGroupIndex; + private final int adIndexInAdGroup; + + public AdPrepareErrorListener(int adGroupIndex, int adIndexInAdGroup) { + this.adGroupIndex = adGroupIndex; + this.adIndexInAdGroup = adIndexInAdGroup; + } + + @Override + public void onPrepareError(final IOException exception) { + mainHandler.post( + new Runnable() { + @Override + public void run() { + adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception); + } + }); + } + } } From 09de29d5341357b5b54b4235e318262aaf8a5c04 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Feb 2018 02:55:37 -0800 Subject: [PATCH 038/376] Add Meizu M5C, Lenovo K4 Note, and Sony Xperia E5 to surface workaround. Also added comments for all existing devices for easier reference. Issue:#3835 Issue:#3236 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185661900 --- RELEASENOTES.md | 3 ++- .../video/MediaCodecVideoRenderer.java | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 66630054d9..df570372a5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -93,7 +93,8 @@ * Propagate ad media preparation errors to IMA so that the ads can be skipped. * `EventLogger` moved from the demo app into the core library. -* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II and Moto C+ +* Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, + Lenovo K4 Note and Sony Xperia E5. ([#3724](https://github.com/google/ExoPlayer/issues/3724), [#3835](https://github.com/google/ExoPlayer/issues/3835)). * Fix potential NPE when removing media sources from a 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 a897f3194c..9e6b0c0f3e 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 @@ -1085,16 +1085,21 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { // Work around https://github.com/google/ExoPlayer/issues/3236, // https://github.com/google/ExoPlayer/issues/3355, - // https://github.com/google/ExoPlayer/issues/3439 and - // https://github.com/google/ExoPlayer/issues/3724. - return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + // https://github.com/google/ExoPlayer/issues/3439, + // https://github.com/google/ExoPlayer/issues/3724 and + // https://github.com/google/ExoPlayer/issues/3835. + return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) // Nexus 7 (2013) && "OMX.qcom.video.decoder.avc".equals(name)) - || (("tcl_eu".equals(Util.DEVICE) - || "SVP-DTV15".equals(Util.DEVICE) - || "BRAVIA_ATV2".equals(Util.DEVICE) - || "panell_s".equals(Util.DEVICE)) + || (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV + || "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 + || "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB + || "panell_s".equals(Util.DEVICE) // Motorola Moto C Plus + || "F3311".equals(Util.DEVICE) // Sony Xperia E5 + || "M5c".equals(Util.DEVICE) // Meizu M5C + || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)) - || (("ALE-L21".equals(Util.MODEL) || "CAM-L21".equals(Util.MODEL)) + || (("ALE-L21".equals(Util.MODEL) // Huawei P8 Lite + || "CAM-L21".equals(Util.MODEL)) // Huawei Y6II && "OMX.k3.video.decoder.avc".equals(name)); } From 43cfb08207edec6c639ecfbf72c0f527b0a75fe1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Feb 2018 03:13:09 -0800 Subject: [PATCH 039/376] Handle VAST_LINEAR_ASSET_MISMATCH This error marks the current ad group as unplayable. Issue: #3801 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185663472 --- RELEASENOTES.md | 2 ++ .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index df570372a5..b089e99ca7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -85,6 +85,8 @@ ([#3584](https://github.com/google/ExoPlayer/issues/3584)). * Work around loadAd not being called beore the LOADED AdEvent arrives ([#3552](https://github.com/google/ExoPlayer/issues/3552)). + * Handle asset mismatch errors + ([#3801](https://github.com/google/ExoPlayer/issues/3801)). * Add support for playing non-Extractor content MediaSources in the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). 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 d714eed98c..d11bc920e1 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 @@ -25,6 +25,8 @@ import android.view.ViewGroup; import android.webkit.WebView; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdError; +import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; @@ -580,14 +582,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onAdError(AdErrorEvent adErrorEvent) { + AdError error = adErrorEvent.getError(); if (DEBUG) { - Log.d(TAG, "onAdError " + adErrorEvent); + Log.d(TAG, "onAdError", error); } if (adsManager == null) { // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(); updateAdPlaybackState(); + } else if (isAdGroupLoadError(error)) { + handleAdGroupLoadError(); } if (pendingAdErrorEvent == null) { pendingAdErrorEvent = adErrorEvent; @@ -1041,4 +1046,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } + private static boolean isAdGroupLoadError(AdError adError) { + // TODO: Find out what other errors need to be handled (if any), and whether each one relates to + // a single ad, ad group or the whole timeline. + return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH; + } } From 4d26b316ccc2335f09b606b84b0d669f93bc25d8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Feb 2018 04:25:44 -0800 Subject: [PATCH 040/376] Make ID3 GEOB frames parsing more robust Issue:#3792 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185668919 --- RELEASENOTES.md | 2 + .../exoplayer2/metadata/id3/Id3Decoder.java | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b089e99ca7..5117922a2d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -109,6 +109,8 @@ * Release `Extractor`s on the loading thread to avoid potentially leaking resources when the playback thread has quit by the time the loading task has completed. +* ID3: Better handle malformed ID3 data + ([#3792](https://github.com/google/ExoPlayer/issues/3792). ### 2.6.1 ### 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 6b2e5c3675..7646af718d 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 @@ -405,14 +405,9 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String value; int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (valueStartIndex < data.length) { - int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); - value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); - } else { - value = ""; - } + int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); + String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset); return new TextInformationFrame("TXXX", description, value); } @@ -452,14 +447,9 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String url; int urlStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (urlStartIndex < data.length) { - int urlEndIndex = indexOfZeroByte(data, urlStartIndex); - url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1"); - } else { - url = ""; - } + int urlEndIndex = indexOfZeroByte(data, urlStartIndex); + String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1"); return new UrlLinkFrame("WXXX", description, url); } @@ -502,13 +492,12 @@ public final class Id3Decoder implements MetadataDecoder { int filenameStartIndex = mimeTypeEndIndex + 1; int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding); - String filename = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex, - charset); + String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset); int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding); - String description = new String(data, descriptionStartIndex, - descriptionEndIndex - descriptionStartIndex, charset); + String description = + decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset); int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); @@ -573,14 +562,9 @@ public final class Id3Decoder implements MetadataDecoder { int descriptionEndIndex = indexOfEos(data, 0, encoding); String description = new String(data, 0, descriptionEndIndex, charset); - String text; int textStartIndex = descriptionEndIndex + delimiterLength(encoding); - if (textStartIndex < data.length) { - int textEndIndex = indexOfEos(data, textStartIndex, encoding); - text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); - } else { - text = ""; - } + int textEndIndex = indexOfEos(data, textStartIndex, encoding); + String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset); return new CommentFrame(language, description, text); } @@ -760,6 +744,25 @@ public final class Id3Decoder implements MetadataDecoder { return Arrays.copyOfRange(data, from, to); } + /** + * Returns a string obtained by decoding the specified range of {@code data} using the specified + * {@code charsetName}. An empty string is returned if the range is invalid. + * + * @param data The array from which to decode the string. + * @param from The start of the range. + * @param to The end of the range (exclusive). + * @param charsetName The name of the Charset to use. + * @return The decoded string, or an empty string if the range is invalid. + * @throws UnsupportedEncodingException If the Charset is not supported. + */ + private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName) + throws UnsupportedEncodingException { + if (to <= from || to > data.length) { + return ""; + } + return new String(data, from, to - from, charsetName); + } + private static final class Id3Header { private final int majorVersion; From 115d19953219c726745e36813f60c60d22e95faa Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Feb 2018 05:26:28 -0800 Subject: [PATCH 041/376] Fix CeaUtil's invalid SeiMessage skipping ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185673454 --- .../java/com/google/android/exoplayer2/text/cea/CeaUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0022d37d6c..67271ee218 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 @@ -52,7 +52,7 @@ public final class CeaUtil { if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) { // This might occur if we're trying to read an encrypted SEI NAL unit. Log.w(TAG, "Skipping remainder of malformed SEI NAL unit."); - seiBuffer.setPosition(seiBuffer.limit()); + nextPayloadPosition = seiBuffer.limit(); } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) { int countryCode = seiBuffer.readUnsignedByte(); int providerCode = seiBuffer.readUnsignedShort(); From 55f2b093406e33e773be1071bfa95f105ee77766 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 14 Feb 2018 05:31:02 -0800 Subject: [PATCH 042/376] Use stable order for subtitle buffers with identical timestamps Issue: #3782 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185673731 --- RELEASENOTES.md | 8 ++- .../exoplayer2/text/SubtitleInputBuffer.java | 20 +------- .../exoplayer2/text/cea/CeaDecoder.java | 50 +++++++++++++++---- .../exoplayer2/text/cea/CeaOutputBuffer.java | 40 --------------- 4 files changed, 49 insertions(+), 69 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5117922a2d..c51846ed7b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -76,8 +76,12 @@ * Support resampling 24-bit and 32-bit integer to 32-bit float for high resolution output in `DefaultAudioSink` ([#3635](https://github.com/google/ExoPlayer/pull/3635)). -* Captions: Initial support for PGS subtitles - ([#3008](https://github.com/google/ExoPlayer/issues/3008)). +* Captions: + * Initial support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). + * Fix issue handling CEA-608 captions where multiple buffers have the same + presentation timestamp + ([#3782](https://github.com/google/ExoPlayer/issues/3782)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. * IMA extension: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java index 4b3b61bddf..9866517a58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java @@ -15,15 +15,11 @@ */ package com.google.android.exoplayer2.text; -import android.support.annotation.NonNull; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -/** - * A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. - */ -public final class SubtitleInputBuffer extends DecoderInputBuffer - implements Comparable { +/** A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. */ +public class SubtitleInputBuffer extends DecoderInputBuffer { /** * An offset that must be added to the subtitle's event times after it's been decoded, or @@ -35,16 +31,4 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } - @Override - public int compareTo(@NonNull SubtitleInputBuffer other) { - if (isEndOfStream() != other.isEndOfStream()) { - return isEndOfStream() ? 1 : -1; - } - long delta = timeUs - other.timeUs; - if (delta == 0) { - return 0; - } - return delta > 0 ? 1 : -1; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java index bb13a7d143..07a55f1a40 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.text.cea; +import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.text.Subtitle; @@ -34,21 +35,22 @@ import java.util.PriorityQueue; private static final int NUM_INPUT_BUFFERS = 10; private static final int NUM_OUTPUT_BUFFERS = 2; - private final LinkedList availableInputBuffers; + private final LinkedList availableInputBuffers; private final LinkedList availableOutputBuffers; - private final PriorityQueue queuedInputBuffers; + private final PriorityQueue queuedInputBuffers; - private SubtitleInputBuffer dequeuedInputBuffer; + private CeaInputBuffer dequeuedInputBuffer; private long playbackPositionUs; + private long queuedInputBufferCount; public CeaDecoder() { availableInputBuffers = new LinkedList<>(); for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { - availableInputBuffers.add(new SubtitleInputBuffer()); + availableInputBuffers.add(new CeaInputBuffer()); } availableOutputBuffers = new LinkedList<>(); for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { - availableOutputBuffers.add(new CeaOutputBuffer(this)); + availableOutputBuffers.add(new CeaOutputBuffer()); } queuedInputBuffers = new PriorityQueue<>(); } @@ -77,9 +79,10 @@ import java.util.PriorityQueue; if (inputBuffer.isDecodeOnly()) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow // for decoding to begin mid-stream. - releaseInputBuffer(inputBuffer); + releaseInputBuffer(dequeuedInputBuffer); } else { - queuedInputBuffers.add(inputBuffer); + dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++; + queuedInputBuffers.add(dequeuedInputBuffer); } dequeuedInputBuffer = null; } @@ -94,7 +97,7 @@ import java.util.PriorityQueue; // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { - SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll(); + CeaInputBuffer inputBuffer = queuedInputBuffers.poll(); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that @@ -126,7 +129,7 @@ import java.util.PriorityQueue; return null; } - private void releaseInputBuffer(SubtitleInputBuffer inputBuffer) { + private void releaseInputBuffer(CeaInputBuffer inputBuffer) { inputBuffer.clear(); availableInputBuffers.add(inputBuffer); } @@ -138,6 +141,7 @@ import java.util.PriorityQueue; @Override public void flush() { + queuedInputBufferCount = 0; playbackPositionUs = 0; while (!queuedInputBuffers.isEmpty()) { releaseInputBuffer(queuedInputBuffers.poll()); @@ -169,4 +173,32 @@ import java.util.PriorityQueue; */ protected abstract void decode(SubtitleInputBuffer inputBuffer); + private static final class CeaInputBuffer extends SubtitleInputBuffer + implements Comparable { + + private long queuedInputBufferCount; + + @Override + public int compareTo(@NonNull CeaInputBuffer other) { + if (isEndOfStream() != other.isEndOfStream()) { + return isEndOfStream() ? 1 : -1; + } + long delta = timeUs - other.timeUs; + if (delta == 0) { + delta = queuedInputBufferCount - other.queuedInputBufferCount; + if (delta == 0) { + return 0; + } + } + return delta > 0 ? 1 : -1; + } + } + + private final class CeaOutputBuffer extends SubtitleOutputBuffer { + + @Override + public final void release() { + releaseOutputBuffer(this); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java deleted file mode 100644 index 4cc32bb9e4..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java +++ /dev/null @@ -1,40 +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.text.cea; - -import com.google.android.exoplayer2.text.SubtitleOutputBuffer; - -/** - * A {@link SubtitleOutputBuffer} for {@link CeaDecoder}s. - */ -public final class CeaOutputBuffer extends SubtitleOutputBuffer { - - private final CeaDecoder owner; - - /** - * @param owner The decoder that owns this buffer. - */ - public CeaOutputBuffer(CeaDecoder owner) { - super(); - this.owner = owner; - } - - @Override - public final void release() { - owner.releaseOutputBuffer(this); - } - -} From c8e950537d17c2240e237568e1bf2a6a96de0764 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 14 Feb 2018 05:43:47 -0800 Subject: [PATCH 043/376] Run DownloadManager on a custom thread while testing ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185674707 --- .../offline/DownloadManagerTest.java | 23 +++--- .../exoplayer2/offline/DownloadManager.java | 7 +- .../DynamicConcatenatingMediaSourceTest.java | 37 +--------- .../dash/offline/DownloadManagerDashTest.java | 35 +++++---- .../dash/offline/DownloadServiceDashTest.java | 57 +++++++++------ .../dash/offline/TestDownloadListener.java | 10 +-- .../exoplayer2/testutil/DummyMainThread.java | 72 +++++++++++++++++++ 7 files changed, 156 insertions(+), 85 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 85af6c32cd..10940eec49 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; @@ -53,12 +54,13 @@ public class DownloadManagerTest extends InstrumentationTestCase { private DownloadManager downloadManager; private File actionFile; private TestDownloadListener testDownloadListener; + private DummyMainThread dummyMainThread; @Override public void setUp() throws Exception { super.setUp(); MockitoUtil.setUpMockito(this); - + dummyMainThread = new DummyMainThread(); actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); testDownloadListener = new TestDownloadListener(); setUpDownloadManager(100); @@ -68,6 +70,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { public void tearDown() throws Exception { releaseDownloadManager(); actionFile.delete(); + dummyMainThread.release(); super.tearDown(); } @@ -76,7 +79,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { releaseDownloadManager(); } try { - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -98,7 +101,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { private void releaseDownloadManager() throws Exception { try { - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -345,7 +348,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { remove2Action.post().assertStarted(); download2Action.post().assertDoesNotStart(); - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -368,7 +371,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { // New download actions can be added but they don't start. download3Action.post().assertDoesNotStart(); - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -393,7 +396,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { // download3Action doesn't start as DM was configured to run two downloads in parallel. download3Action.post().assertDoesNotStart(); - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -404,7 +407,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { // download1Action doesn't stop yet as it ignores interrupts. download2Action.assertStopped(); - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { @@ -462,6 +465,10 @@ public class DownloadManagerTest extends InstrumentationTestCase { return new FakeDownloadAction(mediaId, true); } + private void runOnMainThread(final Runnable r) throws Throwable { + dummyMainThread.runOnMainThread(r); + } + private static final class TestDownloadListener implements DownloadListener { private ConditionVariable downloadFinishedCondition; @@ -544,7 +551,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { } private FakeDownloadAction post() throws Throwable { - runTestOnUiThread( + runOnMainThread( new Runnable() { @Override public void run() { 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 046f0d7593..33a5d8b900 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 @@ -115,7 +115,12 @@ public final class DownloadManager { tasks = new ArrayList<>(); activeDownloadTasks = new ArrayList<>(); - handler = new Handler(Looper.getMainLooper()); + + Looper looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + handler = new Handler(looper); fileIOThread = new HandlerThread("DownloadManager file i/o"); fileIOThread.start(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index a0847bf9ff..a58a427b67 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -20,13 +20,12 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.verify; import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -779,40 +778,6 @@ public final class DynamicConcatenatingMediaSourceTest { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static final class DummyMainThread { - - private final HandlerThread thread; - private final Handler handler; - - private DummyMainThread() { - thread = new HandlerThread("DummyMainThread"); - thread.start(); - handler = new Handler(thread.getLooper()); - } - - /** - * Runs the provided {@link Runnable} on the main thread, blocking until execution completes. - * - * @param runnable The {@link Runnable} to run. - */ - public void runOnMainThread(final Runnable runnable) { - final ConditionVariable finishedCondition = new ConditionVariable(); - handler.post( - new Runnable() { - @Override - public void run() { - runnable.run(); - finishedCondition.open(); - } - }); - assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); - } - - public void release() { - thread.quit(); - } - } - private static final class TimelineGrabber implements Runnable { private final MediaSourceTestRunner testRunner; diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 996dad3d2b..0d3c6ed0f7 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -28,6 +28,7 @@ import android.test.UiThreadTest; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.MockitoUtil; @@ -53,11 +54,13 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { private RepresentationKey fakeRepresentationKey2; private TestDownloadListener downloadListener; private File actionFile; + private DummyMainThread dummyMainThread; @UiThreadTest @Override public void setUp() throws Exception { super.setUp(); + dummyMainThread = new DummyMainThread(); Context context = getInstrumentation().getContext(); tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); File cacheFolder = new File(tempFolder, "cache"); @@ -85,6 +88,7 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { public void tearDown() throws Exception { downloadManager.release(); Util.recursiveDelete(tempFolder); + dummyMainThread.release(); super.tearDown(); } @@ -111,7 +115,7 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { // Run DM accessing code on UI/main thread as it should be. Also not to block handling of loaded // actions. - runTestOnUiThread( + dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { @@ -226,18 +230,25 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { } private void createDownloadManager() { - Factory fakeDataSourceFactory = new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); - downloadManager = - new DownloadManager( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - 1, - 3, - actionFile.getAbsolutePath(), - DashDownloadAction.DESERIALIZER); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + Factory fakeDataSourceFactory = + new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + 1, + 3, + actionFile.getAbsolutePath(), + DashDownloadAction.DESERIALIZER); - downloadListener = new TestDownloadListener(downloadManager, this); - downloadManager.addListener(downloadListener); - downloadManager.startDownloads(); + downloadListener = new TestDownloadListener(downloadManager, dummyMainThread); + downloadManager.addListener(downloadListener); + downloadManager.startDownloads(); + } + }); } } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 8da54ff89e..13c5a5c42f 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; @@ -38,6 +39,7 @@ import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.scheduler.Requirements; import com.google.android.exoplayer2.util.scheduler.Scheduler; import java.io.File; +import java.io.IOException; /** * Unit tests for {@link DownloadService}. @@ -53,10 +55,12 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { private DownloadService dashDownloadService; private ConditionVariable pauseDownloadCondition; private TestDownloadListener testDownloadListener; + private DummyMainThread dummyMainThread; @Override public void setUp() throws Exception { super.setUp(); + dummyMainThread = new DummyMainThread(); tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); @@ -84,31 +88,36 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { .setRandomData("text_segment_1", 1) .setRandomData("text_segment_2", 2) .setRandomData("text_segment_3", 3); - DataSource.Factory fakeDataSourceFactory = new FakeDataSource.Factory(null) - .setFakeDataSet(fakeDataSet); + final DataSource.Factory fakeDataSourceFactory = + new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); context = getInstrumentation().getContext(); - File actionFile = Util.createTempFile(context, "ExoPlayerTest"); - actionFile.delete(); - final DownloadManager dashDownloadManager = - new DownloadManager( - new DownloaderConstructorHelper(cache, fakeDataSourceFactory), - 1, - 3, - actionFile.getAbsolutePath(), - DashDownloadAction.DESERIALIZER); - testDownloadListener = new TestDownloadListener(dashDownloadManager, this); - dashDownloadManager.addListener(testDownloadListener); - dashDownloadManager.startDownloads(); - try { - runTestOnUiThread( + dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { + File actionFile = null; + try { + actionFile = Util.createTempFile(context, "ExoPlayerTest"); + } catch (IOException e) { + throw new RuntimeException(e); + } + actionFile.delete(); + final DownloadManager dashDownloadManager = + new DownloadManager( + new DownloaderConstructorHelper(cache, fakeDataSourceFactory), + 1, + 3, + actionFile.getAbsolutePath(), + DashDownloadAction.DESERIALIZER); + testDownloadListener = new TestDownloadListener(dashDownloadManager, dummyMainThread); + dashDownloadManager.addListener(testDownloadListener); + dashDownloadManager.startDownloads(); + dashDownloadService = new DownloadService(101010) { @@ -143,16 +152,18 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { @Override public void tearDown() throws Exception { try { - runTestOnUiThread(new Runnable() { - @Override - public void run() { - dashDownloadService.onDestroy(); - } - }); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + dashDownloadService.onDestroy(); + } + }); } catch (Throwable throwable) { throw new Exception(throwable); } Util.recursiveDelete(tempFolder); + dummyMainThread.release(); super.tearDown(); } @@ -197,7 +208,7 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { } private void callDownloadServiceOnStart(final DashDownloadAction action) throws Throwable { - runTestOnUiThread( + dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java index 2e6688fe07..6fb89697c7 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java @@ -17,10 +17,10 @@ package com.google.android.exoplayer2.source.dash.offline; import static com.google.common.truth.Truth.assertThat; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.testutil.DummyMainThread; /** A {@link DownloadListener} for testing. */ /*package*/ final class TestDownloadListener implements DownloadListener { @@ -28,13 +28,13 @@ import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; private static final int TIMEOUT = 1000; private final DownloadManager downloadManager; - private final InstrumentationTestCase testCase; + private final DummyMainThread dummyMainThread; private final android.os.ConditionVariable downloadFinishedCondition; private Throwable downloadError; - public TestDownloadListener(DownloadManager downloadManager, InstrumentationTestCase testCase) { + public TestDownloadListener(DownloadManager downloadManager, DummyMainThread dummyMainThread) { this.downloadManager = downloadManager; - this.testCase = testCase; + this.dummyMainThread = dummyMainThread; this.downloadFinishedCondition = new android.os.ConditionVariable(); } @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; * error. */ public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { - testCase.runTestOnUiThread( + dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java new file mode 100644 index 0000000000..6ef3dd0b0d --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java @@ -0,0 +1,72 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; + +/** Helper class to simulate main/UI thread in tests. */ +public final class DummyMainThread { + + /** Default timeout value used for {@link #runOnMainThread(Runnable)}. */ + public static final int TIMEOUT_MS = 10000; + + private final HandlerThread thread; + private final Handler handler; + + public DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or + * until {@link #TIMEOUT_MS} milliseconds have passed. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(final Runnable runnable) { + runOnMainThread(TIMEOUT_MS, runnable); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes or + * until timeout milliseconds have passed. + * + * @param timeoutMs the maximum time to wait in milliseconds. + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(int timeoutMs, final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post( + new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertThat(finishedCondition.block(timeoutMs)).isTrue(); + } + + public void release() { + thread.quit(); + } +} From 1d4bc7dda70e874527eb441dc94df1a4dc8ad914 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 14 Feb 2018 06:14:36 -0800 Subject: [PATCH 044/376] Add initial support for EXT-X-GAP ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185676652 --- RELEASENOTES.md | 1 + .../playlist/HlsMediaPlaylistParserTest.java | 158 +++++++++++------- .../exoplayer2/source/hls/HlsChunkSource.java | 26 ++- .../exoplayer2/source/hls/HlsMediaChunk.java | 10 +- .../source/hls/playlist/HlsMediaPlaylist.java | 25 ++- .../hls/playlist/HlsPlaylistParser.java | 27 ++- 6 files changed, 165 insertions(+), 82 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c51846ed7b..2f6b60f65d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,7 @@ ([#3622](https://github.com/google/ExoPlayer/issues/3622)). * Use long for media sequence numbers ([#3747](https://github.com/google/ExoPlayer/issues/3747)) + * Add initial support for the EXT-X-GAP tag. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: * Support TrueHD passthrough for rechunked samples in Matroska files diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index add631c39b..97a5386b04 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -33,7 +33,7 @@ import junit.framework.TestCase; */ public class HlsMediaPlaylistParserTest extends TestCase { - public void testParseMediaPlaylist() { + public void testParseMediaPlaylist() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); String playlistString = "#EXTM3U\n" + "#EXT-X-VERSION:3\n" @@ -69,76 +69,106 @@ public class HlsMediaPlaylistParserTest extends TestCase { + "#EXT-X-ENDLIST"; InputStream inputStream = new ByteArrayInputStream( playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - try { - HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); - assertThat(playlist).isNotNull(); + HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); - HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; - assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD); - assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000); + HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; + assertThat(mediaPlaylist.playlistType).isEqualTo(HlsMediaPlaylist.PLAYLIST_TYPE_VOD); + assertThat(mediaPlaylist.startOffsetUs).isEqualTo(mediaPlaylist.durationUs - 25000000); - assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679); - assertThat(mediaPlaylist.version).isEqualTo(3); - assertThat(mediaPlaylist.hasEndTag).isTrue(); - List segments = mediaPlaylist.segments; - assertThat(segments).isNotNull(); - assertThat(segments).hasSize(5); + assertThat(mediaPlaylist.mediaSequence).isEqualTo(2679); + assertThat(mediaPlaylist.version).isEqualTo(3); + assertThat(mediaPlaylist.hasEndTag).isTrue(); + List segments = mediaPlaylist.segments; + assertThat(segments).isNotNull(); + assertThat(segments).hasSize(5); - Segment segment = segments.get(0); - assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence) - .isEqualTo(4); - assertThat(segment.durationUs).isEqualTo(7975000); - assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); - assertThat(segment.encryptionIV).isNull(); - assertThat(segment.byterangeLength).isEqualTo(51370); - assertThat(segment.byterangeOffset).isEqualTo(0); - assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts"); + Segment segment = segments.get(0); + assertThat(mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence) + .isEqualTo(4); + assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); + assertThat(segment.encryptionIV).isNull(); + assertThat(segment.byterangeLength).isEqualTo(51370); + assertThat(segment.byterangeOffset).isEqualTo(0); + assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2679.ts"); - segment = segments.get(1); - assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); - assertThat(segment.durationUs).isEqualTo(7975000); - assertThat(segment.fullSegmentEncryptionKeyUri) - .isEqualTo("https://priv.example.com/key.php?r=2680"); - assertThat(segment.encryptionIV).isEqualTo("0x1566B"); - assertThat(segment.byterangeLength).isEqualTo(51501); - assertThat(segment.byterangeOffset).isEqualTo(2147483648L); - assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts"); + segment = segments.get(1); + assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); + assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.fullSegmentEncryptionKeyUri) + .isEqualTo("https://priv.example.com/key.php?r=2680"); + assertThat(segment.encryptionIV).isEqualTo("0x1566B"); + assertThat(segment.byterangeLength).isEqualTo(51501); + assertThat(segment.byterangeOffset).isEqualTo(2147483648L); + assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2680.ts"); - segment = segments.get(2); - assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); - assertThat(segment.durationUs).isEqualTo(7941000); - assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); - assertThat(segment.encryptionIV).isEqualTo(null); - assertThat(segment.byterangeLength).isEqualTo(51501); - assertThat(segment.byterangeOffset).isEqualTo(2147535149L); - assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts"); + segment = segments.get(2); + assertThat(segment.relativeDiscontinuitySequence).isEqualTo(0); + assertThat(segment.durationUs).isEqualTo(7941000); + assertThat(segment.fullSegmentEncryptionKeyUri).isNull(); + assertThat(segment.encryptionIV).isEqualTo(null); + assertThat(segment.byterangeLength).isEqualTo(51501); + assertThat(segment.byterangeOffset).isEqualTo(2147535149L); + assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2681.ts"); - segment = segments.get(3); - assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); - assertThat(segment.durationUs).isEqualTo(7975000); - assertThat(segment.fullSegmentEncryptionKeyUri) - .isEqualTo("https://priv.example.com/key.php?r=2682"); - // 0xA7A == 2682. - assertThat(segment.encryptionIV).isNotNull(); - assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7A"); - assertThat(segment.byterangeLength).isEqualTo(51740); - assertThat(segment.byterangeOffset).isEqualTo(2147586650L); - assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts"); + segment = segments.get(3); + assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); + assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.fullSegmentEncryptionKeyUri) + .isEqualTo("https://priv.example.com/key.php?r=2682"); + // 0xA7A == 2682. + assertThat(segment.encryptionIV).isNotNull(); + assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7A"); + assertThat(segment.byterangeLength).isEqualTo(51740); + assertThat(segment.byterangeOffset).isEqualTo(2147586650L); + assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2682.ts"); - segment = segments.get(4); - assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); - assertThat(segment.durationUs).isEqualTo(7975000); - assertThat(segment.fullSegmentEncryptionKeyUri) - .isEqualTo("https://priv.example.com/key.php?r=2682"); - // 0xA7B == 2683. - assertThat(segment.encryptionIV).isNotNull(); - assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7B"); - assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET); - assertThat(segment.byterangeOffset).isEqualTo(0); - assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); - } catch (IOException exception) { - fail(exception.getMessage()); - } + segment = segments.get(4); + assertThat(segment.relativeDiscontinuitySequence).isEqualTo(1); + assertThat(segment.durationUs).isEqualTo(7975000); + assertThat(segment.fullSegmentEncryptionKeyUri) + .isEqualTo("https://priv.example.com/key.php?r=2682"); + // 0xA7B == 2683. + assertThat(segment.encryptionIV).isNotNull(); + assertThat(segment.encryptionIV.toUpperCase(Locale.getDefault())).isEqualTo("A7B"); + assertThat(segment.byterangeLength).isEqualTo(C.LENGTH_UNSET); + assertThat(segment.byterangeOffset).isEqualTo(0); + assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); + } + + public void testGapTag() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test2.m3u8"); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:5\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PROGRAM-DATE-TIME:2016-09-22T02:00:01+00:00\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://example.com/key?value=something\"\n" + + "#EXTINF:5.005,\n" + + "02/00/27.ts\n" + + "#EXTINF:5.005,\n" + + "02/00/32.ts\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:5.005,\n" + + "#EXT-X-GAP \n" + + "../dummy.ts\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://key-service.bamgrid.com/1.0/key?" + + "hex-value=9FB8989D15EEAAF8B21B860D7ED3072A\",IV=0x410C8AC18AA42EFA18B5155484F5FC34\n" + + "#EXTINF:5.005,\n" + + "02/00/42.ts\n" + + "#EXTINF:5.005,\n" + + "02/00/47.ts\n"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + HlsMediaPlaylist playlist = + (HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + + assertThat(playlist.hasEndTag).isFalse(); + assertThat(playlist.segments.get(1).hasGapTag).isFalse(); + assertThat(playlist.segments.get(2).hasGapTag).isTrue(); + assertThat(playlist.segments.get(3).hasGapTag).isFalse(); } } 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 04f61810bc..db0db47aee 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 @@ -330,11 +330,27 @@ import java.util.List; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(extractorFactory, mediaDataSource, dataSpec, initDataSpec, - selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, - chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, - mediaPlaylist.drmInitData, encryptionKey, encryptionIv); + out.chunk = + new HlsMediaChunk( + extractorFactory, + mediaDataSource, + dataSpec, + initDataSpec, + selectedUrl, + muxedCaptionFormats, + trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), + startTimeUs, + startTimeUs + segment.durationUs, + chunkMediaSequence, + discontinuitySequence, + segment.hasGapTag, + isTimestampMaster, + timestampAdjuster, + previous, + mediaPlaylist.drmInitData, + encryptionKey, + encryptionIv); } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java index 4be758993d..9e993aa27b 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaChunk.java @@ -66,6 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger; private final DataSpec initDataSpec; private final boolean isEncrypted; private final boolean isMasterTimestampSource; + private final boolean hasGapTag; private final TimestampAdjuster timestampAdjuster; private final boolean shouldSpliceIn; private final Extractor extractor; @@ -97,6 +98,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @param endTimeUs The end time of the chunk in microseconds. * @param chunkMediaSequence The media sequence number of the chunk. * @param discontinuitySequenceNumber The discontinuity sequence number of the chunk. + * @param hasGapTag Whether the chunk is tagged with EXT-X-GAP. * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. @@ -119,6 +121,7 @@ import java.util.concurrent.atomic.AtomicInteger; long endTimeUs, long chunkMediaSequence, int discontinuitySequenceNumber, + boolean hasGapTag, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, @@ -141,6 +144,7 @@ import java.util.concurrent.atomic.AtomicInteger; this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; + this.hasGapTag = hasGapTag; Extractor previousExtractor = null; if (previousChunk != null) { shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; @@ -211,7 +215,10 @@ import java.util.concurrent.atomic.AtomicInteger; public void load() throws IOException, InterruptedException { maybeLoadInitData(); if (!loadCanceled) { - loadMedia(); + if (!hasGapTag) { + loadMedia(); + } + loadCompleted = true; } } @@ -283,7 +290,6 @@ import java.util.concurrent.atomic.AtomicInteger; } finally { Util.closeQuietly(dataSource); } - loadCompleted = true; } /** 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 77a4c9ed1d..9a9517e2d4 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 @@ -69,8 +69,16 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final long byterangeLength; + /** Whether the segment is tagged with #EXT-X-GAP. */ + public final boolean hasGapTag; + + /** + * @param uri See {@link #url}. + * @param byterangeOffset See {@link #byterangeOffset}. + * @param byterangeLength See {@link #byterangeLength}. + */ public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength); + this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false); } /** @@ -82,10 +90,18 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeLength See {@link #byterangeLength}. + * @param hasGapTag See {@link #hasGapTag}. */ - public Segment(String url, long durationUs, int relativeDiscontinuitySequence, - long relativeStartTimeUs, String fullSegmentEncryptionKeyUri, - String encryptionIV, long byterangeOffset, long byterangeLength) { + public Segment( + String url, + long durationUs, + int relativeDiscontinuitySequence, + long relativeStartTimeUs, + String fullSegmentEncryptionKeyUri, + String encryptionIV, + long byterangeOffset, + long byterangeLength, + boolean hasGapTag) { this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; @@ -94,6 +110,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; this.byterangeLength = byterangeLength; + this.hasGapTag = hasGapTag; } @Override 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 4deddc1869..acd0746e72 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 @@ -67,6 +67,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 14 Feb 2018 07:03:40 -0800 Subject: [PATCH 045/376] Fix handling of ad tags with only preroll and postroll ads Content progress is only polled if there are midroll ad groups. If the ad tag had only preroll/postroll ads, the pending content position was not provided to IMA, which meant that the postroll ad could never play. Only set the pending content position if there are midroll ads. Issue: #3715 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185680803 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 22 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2f6b60f65d..b20946d000 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -97,6 +97,8 @@ ([#3676](https://github.com/google/ExoPlayer/issues/3676)). * Fix handling of ad tags where ad groups are out of order ([#3716](https://github.com/google/ExoPlayer/issues/3716)). + * Fix handling of ad tags with only preroll/postroll ad groups + ([#3715](https://github.com/google/ExoPlayer/issues/3715)). * Propagate ad media preparation errors to IMA so that the ads can be skipped. * `EventLogger` moved from the demo app into the core library. 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 d11bc920e1..0a79acb617 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 @@ -851,15 +851,14 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setMimeTypes(supportedMimeTypes); // Set up the ad playback state, skipping ads based on the start position as required. - pendingContentPositionMs = player.getCurrentPosition(); long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); + long contentPositionMs = player.getCurrentPosition(); int adGroupIndexForPosition = - adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(pendingContentPositionMs)); + adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); if (adGroupIndexForPosition == 0) { podIndexOffset = 0; } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - pendingContentPositionMs = C.TIME_UNSET; // There is no preroll and midroll pod indices start at 1. podIndexOffset = -1; } else /* adGroupIndexForPosition > 0 */ { @@ -879,6 +878,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A podIndexOffset = adGroupIndexForPosition - 1; } + if (hasMidrollAdGroups(adGroupTimesUs)) { + // IMA will poll the content position, so provide the player's initial position like a seek. + pendingContentPositionMs = contentPositionMs; + } + // Start ad playback. adsManager.init(adsRenderingSettings); updateAdPlaybackState(); @@ -1051,4 +1055,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // a single ad, ad group or the whole timeline. return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH; } + + private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) { + int count = adGroupTimesUs.length; + if (count == 1) { + return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE; + } else if (count == 2) { + return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE; + } else { + // There's at least one midroll ad group, as adGroupTimesUs is never empty. + return true; + } + } } From 6af2a3c9ef425223523400f336b1252410ee8c34 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 14 Feb 2018 07:11:38 -0800 Subject: [PATCH 046/376] Broaden the surface switching workaround to other Moto C+ variants. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185681751 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 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 9e6b0c0f3e..62a7657ea7 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 @@ -1093,7 +1093,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { || (("tcl_eu".equals(Util.DEVICE) // TCL Percee TV || "SVP-DTV15".equals(Util.DEVICE) // Sony Bravia 4K 2015 || "BRAVIA_ATV2".equals(Util.DEVICE) // Sony Bravia 4K GB - || "panell_s".equals(Util.DEVICE) // Motorola Moto C Plus + || Util.DEVICE.startsWith("panell_") // Motorola Moto C Plus || "F3311".equals(Util.DEVICE) // Sony Xperia E5 || "M5c".equals(Util.DEVICE) // Meizu M5C || "A7010a48".equals(Util.DEVICE)) // Lenovo K4 Note From 344932af75de43b16c2335ac8e4561422e17d50c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Feb 2018 00:44:09 -0800 Subject: [PATCH 047/376] Fix handling of ad group load errors IMA sometimes delivers an ad group load error just after the time of the ad group, which is problematic now that we set the expected ad group index based on the last returned content progress. Only update the expected ad group index once we are within a fixed preloading threshold of the next ad. Also fix updating the ad group to use the new ad count, and check for ad group load errors when we have no expected ad group defensively. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185803086 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 20 +++++++++++++++++-- 1 file changed, 18 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 0a79acb617..312cb046ba 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 @@ -163,6 +163,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + /** The maximum duration before an ad break that IMA may start preloading the next ad. */ + private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000; + /** * The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be * clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in @@ -621,9 +624,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); } else if (imaAdState == IMA_AD_STATE_NONE && hasContentDuration) { contentPositionMs = player.getCurrentPosition(); - // Keep track of the ad group index that IMA will load for the current content position. - expectedAdGroupIndex = + // Update the expected ad group index for the current content position. The update is delayed + // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered + // just after an ad group isn't incorrectly attributed to the next ad group. + int nextAdGroupIndex = adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); + if (nextAdGroupIndex != expectedAdGroupIndex + && nextAdGroupIndex != C.INDEX_UNSET + && C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]) - contentPositionMs + < MAXIMUM_PRELOAD_DURATION_MS) { + expectedAdGroupIndex = nextAdGroupIndex; + } } else { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } @@ -969,10 +980,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void handleAdGroupLoadError() { int adGroupIndex = this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex; + if (adGroupIndex == C.INDEX_UNSET) { + // Drop the error, as we don't know which ad group it relates to. + return; + } AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length)); + adGroup = adPlaybackState.adGroups[adGroupIndex]; } for (int i = 0; i < adGroup.count; i++) { if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { From 0291bc5e2da83e3084356e3bab77f0f8ccd35e41 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Feb 2018 04:10:53 -0800 Subject: [PATCH 048/376] Fix content progress reporting if there is no preroll If there was no preroll and the pending content position was set before the first midroll, the pending content position was never cleared so loading the ad was never triggered. Only set a pending content position if we know that we need to trigger playing an ad for the current position and IMA will poll for an ad (because there is a midroll ad group). Clearing the pending content position happens when IMA pauses content to play the ad. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185818315 --- .../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 312cb046ba..43e3c0d93d 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 @@ -889,8 +889,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A podIndexOffset = adGroupIndexForPosition - 1; } - if (hasMidrollAdGroups(adGroupTimesUs)) { - // IMA will poll the content position, so provide the player's initial position like a seek. + if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) { + // Provide the player's initial position to trigger loading and playing the ad. pendingContentPositionMs = contentPositionMs; } From 67780bfbc28b2da0069ce91ec4116ec6b1d331cb Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 15 Feb 2018 06:29:45 -0800 Subject: [PATCH 049/376] Add window sequence number to MediaPeriodId. All media periods are part of a queue of windows buffered and played by ExoPlayer. When repeating windows, the current MediaPeriodId is insufficient to distinguish between the repetitions of the same period. This makes it hard to see to which media period load events belong to, and it is also difficult to determine whether two media periods belong to the same logical window or whether they are part of different repetitions of the same window. Therefore this change adds a unique sequence number to each window in the sequence of windows and this sequence number is saved as part of the MediaPeriodId. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185829509 --- .../android/exoplayer2/MediaPeriodQueue.java | 124 +++++++++++++++--- .../android/exoplayer2/PlaybackInfo.java | 2 +- .../exoplayer2/source/MediaSource.java | 47 +++++-- .../exoplayer2/source/ads/AdsMediaSource.java | 5 +- .../android/exoplayer2/ExoPlayerTest.java | 5 +- .../source/ConcatenatingMediaSourceTest.java | 26 +++- .../DynamicConcatenatingMediaSourceTest.java | 38 ++++-- .../testutil/MediaSourceTestRunner.java | 8 +- 8 files changed, 200 insertions(+), 55 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 208a235777..3efff58f5d 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 @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.util.Assertions; private final Timeline.Period period; private final Timeline.Window window; + private long nextWindowSequenceNumber; private Timeline timeline; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; @@ -368,18 +369,70 @@ import com.google.android.exoplayer2.util.Assertions; * @return The identifier for the first media period to play, taking into account unplayed ads. */ public MediaPeriodId resolveMediaPeriodIdForAds(int periodIndex, long positionUs) { - timeline.getPeriod(periodIndex, period); - int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); - if (adGroupIndex == C.INDEX_UNSET) { - return new MediaPeriodId(periodIndex); - } else { - int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); - return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); - } + long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodIndex); + return resolveMediaPeriodIdForAds(periodIndex, positionUs, windowSequenceNumber); } // Internal methods. + /** + * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be + * played, returning an identifier for an ad group if one needs to be played before the specified + * position, or an identifier for a content media period if not. + * + * @param periodIndex The index of the timeline period to play. + * @param positionUs The next content position in the period to play. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this period is part of. + * @return The identifier for the first media period to play, taking into account unplayed ads. + */ + private MediaPeriodId resolveMediaPeriodIdForAds( + int periodIndex, long positionUs, long windowSequenceNumber) { + timeline.getPeriod(periodIndex, period); + int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); + if (adGroupIndex == C.INDEX_UNSET) { + return new MediaPeriodId(periodIndex, windowSequenceNumber); + } else { + int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); + return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); + } + } + + /** + * Resolves the specified period index to a corresponding window sequence number. Either by + * reusing the window sequence number of an existing matching media period or by creating a new + * window sequence number. + * + * @param periodIndex The index of the timeline period. + * @return A window sequence number for a media period created for this timeline period. + */ + private long resolvePeriodIndexToWindowSequenceNumber(int periodIndex) { + Object periodUid = timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid; + MediaPeriodHolder mediaPeriodHolder = getFrontPeriod(); + while (mediaPeriodHolder != null) { + if (mediaPeriodHolder.uid.equals(periodUid)) { + // Reuse window sequence number of first exact period match. + return mediaPeriodHolder.info.id.windowSequenceNumber; + } + mediaPeriodHolder = mediaPeriodHolder.next; + } + int windowIndex = period.windowIndex; + mediaPeriodHolder = getFrontPeriod(); + while (mediaPeriodHolder != null) { + int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid); + if (indexOfHolderInTimeline != C.INDEX_UNSET) { + int holderWindowIndex = timeline.getPeriod(indexOfHolderInTimeline, period).windowIndex; + if (holderWindowIndex == windowIndex) { + // As an alternative, try to match other periods of the same window. + return mediaPeriodHolder.info.id.windowSequenceNumber; + } + } + mediaPeriodHolder = mediaPeriodHolder.next; + } + // If no match is found, create new sequence number. + return nextWindowSequenceNumber++; + } + /** * Returns whether {@code periodHolder} can be kept for playing the media period described by * {@code info}. @@ -466,7 +519,10 @@ import com.google.android.exoplayer2.util.Assertions; } long startPositionUs; - int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex; + int nextWindowIndex = + timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex; + Object nextPeriodUid = period.uid; + long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber; if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) { // 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. The expected delay until playback @@ -487,10 +543,16 @@ import com.google.android.exoplayer2.util.Assertions; } nextPeriodIndex = defaultPosition.first; startPositionUs = defaultPosition.second; + if (mediaPeriodHolder.next != null && mediaPeriodHolder.next.uid.equals(nextPeriodUid)) { + windowSequenceNumber = mediaPeriodHolder.next.info.id.windowSequenceNumber; + } else { + windowSequenceNumber = nextWindowSequenceNumber++; + } } else { startPositionUs = 0; } - MediaPeriodId periodId = resolveMediaPeriodIdForAds(nextPeriodIndex, startPositionUs); + MediaPeriodId periodId = + resolveMediaPeriodIdForAds(nextPeriodIndex, startPositionUs, windowSequenceNumber); return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs); } @@ -512,11 +574,14 @@ import com.google.android.exoplayer2.util.Assertions; currentPeriodId.periodIndex, adGroupIndex, nextAdIndexInAdGroup, - mediaPeriodInfo.contentPositionUs); + mediaPeriodInfo.contentPositionUs, + currentPeriodId.windowSequenceNumber); } else { // Play content from the ad group position. return getMediaPeriodInfoForContent( - currentPeriodId.periodIndex, mediaPeriodInfo.contentPositionUs); + currentPeriodId.periodIndex, + mediaPeriodInfo.contentPositionUs, + currentPeriodId.windowSequenceNumber); } } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) { // Play the next ad group if it's available. @@ -524,7 +589,9 @@ import com.google.android.exoplayer2.util.Assertions; if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the ad group position instead. return getMediaPeriodInfoForContent( - currentPeriodId.periodIndex, mediaPeriodInfo.endPositionUs); + currentPeriodId.periodIndex, + mediaPeriodInfo.endPositionUs, + currentPeriodId.windowSequenceNumber); } int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex); return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup) @@ -533,7 +600,8 @@ import com.google.android.exoplayer2.util.Assertions; currentPeriodId.periodIndex, nextAdGroupIndex, adIndexInAdGroup, - mediaPeriodInfo.endPositionUs); + mediaPeriodInfo.endPositionUs, + currentPeriodId.windowSequenceNumber); } else { // Check if the postroll ad should be played. int adGroupCount = period.getAdGroupCount(); @@ -551,7 +619,11 @@ import com.google.android.exoplayer2.util.Assertions; } long contentDurationUs = period.getDurationUs(); return getMediaPeriodInfoForAd( - currentPeriodId.periodIndex, adGroupIndex, adIndexInAdGroup, contentDurationUs); + currentPeriodId.periodIndex, + adGroupIndex, + adIndexInAdGroup, + contentDurationUs, + currentPeriodId.windowSequenceNumber); } } @@ -583,15 +655,24 @@ import com.google.android.exoplayer2.util.Assertions; return null; } return getMediaPeriodInfoForAd( - id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup, contentPositionUs); + id.periodIndex, + id.adGroupIndex, + id.adIndexInAdGroup, + contentPositionUs, + id.windowSequenceNumber); } else { - return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs); + return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, id.windowSequenceNumber); } } private MediaPeriodInfo getMediaPeriodInfoForAd( - int periodIndex, int adGroupIndex, int adIndexInAdGroup, long contentPositionUs) { - MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup); + int periodIndex, + int adGroupIndex, + int adIndexInAdGroup, + long contentPositionUs, + long windowSequenceNumber) { + MediaPeriodId id = + new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE); boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = @@ -612,8 +693,9 @@ import com.google.android.exoplayer2.util.Assertions; isLastInTimeline); } - private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs) { - MediaPeriodId id = new MediaPeriodId(periodIndex); + private MediaPeriodInfo getMediaPeriodInfoForContent( + int periodIndex, long startPositionUs, long windowSequenceNumber) { + MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber); timeline.getPeriod(id.periodIndex, period); int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); long endUs = 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 bb39bf3d0b..3ff2ec9461 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 @@ -41,7 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult; this( timeline, /* manifest= */ null, - new MediaPeriodId(0), + new MediaPeriodId(/* periodIndex= */ 0), startPositionUs, /* contentPositionUs =*/ C.TIME_UNSET, Player.STATE_IDLE, 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 25da60cb74..02bd0cdbc7 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 @@ -63,12 +63,6 @@ public interface MediaSource { */ final class MediaPeriodId { - /** - * Value for unset media period identifiers. - */ - public static final MediaPeriodId UNSET = - new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET); - /** * The timeline period index. */ @@ -86,13 +80,32 @@ public interface MediaSource { */ public final int adIndexInAdGroup; + /** + * The sequence number of the window in the buffered sequence of windows this media period is + * part of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of + * windows. + */ + public final long windowSequenceNumber; + + /** + * Creates a media period identifier for a dummy period which is not part of a buffered sequence + * of windows. + * + * @param periodIndex The period index. + */ + public MediaPeriodId(int periodIndex) { + this(periodIndex, C.INDEX_UNSET); + } + /** * Creates a media period identifier for the specified period in the timeline. * * @param periodIndex The timeline period index. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. */ - public MediaPeriodId(int periodIndex) { - this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET); + public MediaPeriodId(int periodIndex, long windowSequenceNumber) { + this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber); } /** @@ -102,19 +115,24 @@ public interface MediaSource { * @param periodIndex The index of the timeline period that contains the ad group. * @param adGroupIndex The index of the ad group. * @param adIndexInAdGroup The index of the ad in the ad group. + * @param windowSequenceNumber The sequence number of the window in the buffered sequence of + * windows this media period is part of. */ - public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) { + public MediaPeriodId( + int periodIndex, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) { this.periodIndex = periodIndex; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; + this.windowSequenceNumber = windowSequenceNumber; } /** * Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index. */ public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) { - return periodIndex == newPeriodIndex ? this - : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup); + return periodIndex == newPeriodIndex + ? this + : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); } /** @@ -134,8 +152,10 @@ public interface MediaSource { } MediaPeriodId periodId = (MediaPeriodId) obj; - return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex - && adIndexInAdGroup == periodId.adIndexInAdGroup; + return periodIndex == periodId.periodIndex + && adGroupIndex == periodId.adGroupIndex + && adIndexInAdGroup == periodId.adIndexInAdGroup + && windowSequenceNumber == periodId.windowSequenceNumber; } @Override @@ -144,6 +164,7 @@ public interface MediaSource { result = 31 * result + periodIndex; result = 31 * result + adGroupIndex; result = 31 * result + adIndexInAdGroup; + result = 31 * result + (int) windowSequenceNumber; return result; } 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 8c4d85ff4c..854be90d1c 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 @@ -235,7 +235,10 @@ public final class AdsMediaSource extends CompositeMediaSource { } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; DeferredMediaPeriod deferredMediaPeriod = - new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + new DeferredMediaPeriod( + mediaSource, + new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber), + allocator); deferredMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); 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 7faa349705..3d0cde5df8 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 @@ -1802,7 +1802,10 @@ public final class ExoPlayerTest { testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. assertThat(mediaSource.getCreatedMediaPeriods()) - .containsExactly(new MediaPeriodId(0), new MediaPeriodId(1), new MediaPeriodId(1)) + .containsExactly( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 2)) .inOrder(); } 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 d7cf8db4bc..ccc3ddea46 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 @@ -245,12 +245,26 @@ public final class ConcatenatingMediaSourceTest { // Create all periods and assert period creation of child media sources has been called. testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 1, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); } finally { testRunner.release(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index a58a427b67..d71b02d39a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -267,12 +267,16 @@ public final class DynamicConcatenatingMediaSourceTest { // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not // called yet. - MediaPeriod lazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + MediaPeriod lazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); 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(0)); + MediaPeriod secondLazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); testRunner.releasePeriod(secondLazyPeriod); // Trigger source info refresh for lazy media source. Assert that now all information is @@ -653,12 +657,26 @@ public final class DynamicConcatenatingMediaSourceTest { // Create all periods and assert period creation of child media sources has been called. testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 1, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); } @Test @@ -754,7 +772,9 @@ public final class DynamicConcatenatingMediaSourceTest { FakeMediaSource childSource = createFakeMediaSource(); mediaSource.addMediaSource(childSource); testRunner.prepareSource(); - MediaPeriod mediaPeriod = testRunner.createPeriod(new MediaPeriodId(/* periodIndex= */ 0)); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); mediaSource.removeMediaSource(/* index= */ 0); testRunner.assertTimelineChangeBlocking(); testRunner.releasePeriod(mediaPeriod); 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 cf0cc342f8..16389112ca 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 @@ -244,16 +244,18 @@ public class MediaSourceTestRunner { /** * Creates and releases all periods (including ad periods) defined in the last timeline to be * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or {@link - * #assertTimelineChangeBlocking()}. + * #assertTimelineChangeBlocking()}. The {@link MediaPeriodId#windowSequenceNumber} is set to the + * index of the window. */ public void assertPrepareAndReleaseAllPeriods() throws InterruptedException { Timeline.Period period = new Timeline.Period(); for (int i = 0; i < timeline.getPeriodCount(); i++) { - assertPrepareAndReleasePeriod(new MediaPeriodId(i)); timeline.getPeriod(i, period); + assertPrepareAndReleasePeriod(new MediaPeriodId(i, period.windowIndex)); for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { - assertPrepareAndReleasePeriod(new MediaPeriodId(i, adGroupIndex, adIndex)); + assertPrepareAndReleasePeriod( + new MediaPeriodId(i, adGroupIndex, adIndex, period.windowIndex)); } } } From 8ffd40ab9a396a9ab3a411d54e1e8d2b26a40bed Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 16 Feb 2018 03:05:55 -0800 Subject: [PATCH 050/376] Add little endian and 14-bit mode support for DtsReader Issue:#3340 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185973510 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/DtsUtil.java | 118 ++++++++++++++++-- .../exoplayer2/extractor/ts/DtsReader.java | 16 ++- .../exoplayer2/util/ParsableBitArray.java | 34 +++++ .../exoplayer2/util/ParsableBitArrayTest.java | 67 ++++++++++ 5 files changed, 219 insertions(+), 18 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b20946d000..7204ee88fd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -118,6 +118,8 @@ completed. * ID3: Better handle malformed ID3 data ([#3792](https://github.com/google/ExoPlayer/issues/3792). +* Support 14-bit mode and little endianness in DTS PES packets + ([#3340](https://github.com/google/ExoPlayer/issues/3340)). ### 2.6.1 ### 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 9e9b927fab..dc07b1a646 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 @@ -20,12 +20,22 @@ import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableBitArray; import java.nio.ByteBuffer; +import java.util.Arrays; /** * Utility methods for parsing DTS frames. */ public final class DtsUtil { + private static final int SYNC_VALUE_BE = 0x7FFE8001; + private static final int SYNC_VALUE_14B_BE = 0x1FFFE800; + private static final int SYNC_VALUE_LE = 0xFE7F0180; + private static final int SYNC_VALUE_14B_LE = 0xFF1F00E8; + private static final byte FIRST_BYTE_BE = (byte) (SYNC_VALUE_BE >>> 24); + private static final byte FIRST_BYTE_14B_BE = (byte) (SYNC_VALUE_14B_BE >>> 24); + private static final byte FIRST_BYTE_LE = (byte) (SYNC_VALUE_LE >>> 24); + private static final byte FIRST_BYTE_14B_LE = (byte) (SYNC_VALUE_14B_LE >>> 24); + /** * Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4. */ @@ -45,6 +55,20 @@ public final class DtsUtil { 384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816, 2823, 2944, 3072, 3840, 4096, 6144, 7680}; + /** + * Returns whether a given integer matches a DTS sync word. Synchronization and storage modes are + * defined in ETSI TS 102 114 V1.1.1 (2002-08), Section 5.3. + * + * @param word An integer. + * @return Whether a given integer matches a DTS sync word. + */ + public static boolean isSyncWord(int word) { + return word == SYNC_VALUE_BE + || word == SYNC_VALUE_LE + || word == SYNC_VALUE_14B_BE + || word == SYNC_VALUE_14B_LE; + } + /** * Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114 * subsections 5.3/5.4. @@ -57,8 +81,8 @@ public final class DtsUtil { */ public static Format parseDtsFormat(byte[] frame, String trackId, String language, DrmInitData drmInitData) { - ParsableBitArray frameBits = new ParsableBitArray(frame); - frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE + ParsableBitArray frameBits = getNormalizedFrameHeader(frame); + frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE int amode = frameBits.readBits(6); int channelCount = CHANNELS_BY_AMODE[amode]; int sfreq = frameBits.readBits(4); @@ -79,8 +103,21 @@ public final class DtsUtil { * @return The number of audio samples represented by the frame. */ public static int parseDtsAudioSampleCount(byte[] data) { - // See ETSI TS 102 114 subsection 5.4.1. - int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + int nblks; + switch (data[0]) { + case FIRST_BYTE_LE: + nblks = ((data[5] & 0x01) << 6) | ((data[4] & 0xFC) >> 2); + break; + case FIRST_BYTE_14B_LE: + nblks = ((data[4] & 0x07) << 4) | ((data[7] & 0x3C) >> 2); + break; + case FIRST_BYTE_14B_BE: + nblks = ((data[5] & 0x07) << 4) | ((data[6] & 0x3C) >> 2); + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2); + } return (nblks + 1) * 32; } @@ -94,8 +131,21 @@ public final class DtsUtil { public static int parseDtsAudioSampleCount(ByteBuffer buffer) { // See ETSI TS 102 114 subsection 5.4.1. int position = buffer.position(); - int nblks = ((buffer.get(position + 4) & 0x01) << 6) - | ((buffer.get(position + 5) & 0xFC) >> 2); + int nblks; + switch (buffer.get(position)) { + case FIRST_BYTE_LE: + nblks = ((buffer.get(position + 5) & 0x01) << 6) | ((buffer.get(position + 4) & 0xFC) >> 2); + break; + case FIRST_BYTE_14B_LE: + nblks = ((buffer.get(position + 4) & 0x07) << 4) | ((buffer.get(position + 7) & 0x3C) >> 2); + break; + case FIRST_BYTE_14B_BE: + nblks = ((buffer.get(position + 5) & 0x07) << 4) | ((buffer.get(position + 6) & 0x3C) >> 2); + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + nblks = ((buffer.get(position + 4) & 0x01) << 6) | ((buffer.get(position + 5) & 0xFC) >> 2); + } return (nblks + 1) * 32; } @@ -106,9 +156,59 @@ public final class DtsUtil { * @return The frame's size in bytes. */ public static int getDtsFrameSize(byte[] data) { - return (((data[5] & 0x02) << 12) - | ((data[6] & 0xFF) << 4) - | ((data[7] & 0xF0) >> 4)) + 1; + int fsize; + boolean uses14BitPerWord = false; + switch (data[0]) { + case FIRST_BYTE_14B_BE: + fsize = (((data[6] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[8] & 0x3C) >> 2)) + 1; + uses14BitPerWord = true; + break; + case FIRST_BYTE_LE: + fsize = (((data[4] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[6] & 0xF0) >> 4)) + 1; + break; + case FIRST_BYTE_14B_LE: + fsize = (((data[7] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[9] & 0x3C) >> 2)) + 1; + uses14BitPerWord = true; + break; + default: + // We blindly assume FIRST_BYTE_BE if none of the others match. + fsize = (((data[5] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[7] & 0xF0) >> 4)) + 1; + } + + // If the frame is stored in 14-bit mode, adjust the frame size to reflect the actual byte size. + return uses14BitPerWord ? fsize * 16 / 14 : fsize; + } + + private static ParsableBitArray getNormalizedFrameHeader(byte[] frameHeader) { + if (frameHeader[0] == FIRST_BYTE_BE) { + // The frame is already 16-bit mode, big endian. + return new ParsableBitArray(frameHeader); + } + // Data is not normalized, but we don't want to modify frameHeader. + frameHeader = Arrays.copyOf(frameHeader, frameHeader.length); + if (isLittleEndianFrameHeader(frameHeader)) { + // Change endianness. + for (int i = 0; i < frameHeader.length - 1; i += 2) { + byte temp = frameHeader[i]; + frameHeader[i] = frameHeader[i + 1]; + frameHeader[i + 1] = temp; + } + } + ParsableBitArray frameBits = new ParsableBitArray(frameHeader); + if (frameHeader[0] == (byte) (SYNC_VALUE_14B_BE >> 24)) { + // Discard the 2 most significant bits of each 16 bit word. + ParsableBitArray scratchBits = new ParsableBitArray(frameHeader); + while (scratchBits.bitsLeft() >= 16) { + scratchBits.skipBits(2); + frameBits.putInt(scratchBits.readBits(14), 14); + } + } + frameBits.reset(frameHeader); + return frameBits; + } + + private static boolean isLittleEndianFrameHeader(byte[] frameHeader) { + return frameHeader[0] == FIRST_BYTE_LE || frameHeader[0] == FIRST_BYTE_14B_LE; } private DtsUtil() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java index df1e8816f0..0fc3383015 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java @@ -32,9 +32,7 @@ public final class DtsReader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 15; - private static final int SYNC_VALUE = 0x7FFE8001; - private static final int SYNC_VALUE_SIZE = 4; + private static final int HEADER_SIZE = 18; private final ParsableByteArray headerScratchBytes; private final String language; @@ -63,10 +61,6 @@ public final class DtsReader implements ElementaryStreamReader { */ public DtsReader(String language) { headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]); - headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF); - headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF); - headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF); - headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF); state = STATE_FINDING_SYNC; this.language = language; } @@ -96,7 +90,6 @@ public final class DtsReader implements ElementaryStreamReader { switch (state) { case STATE_FINDING_SYNC: if (skipToNextSync(data)) { - bytesRead = SYNC_VALUE_SIZE; state = STATE_READING_HEADER; } break; @@ -154,7 +147,12 @@ public final class DtsReader implements ElementaryStreamReader { while (pesBuffer.bytesLeft() > 0) { syncBytes <<= 8; syncBytes |= pesBuffer.readUnsignedByte(); - if (syncBytes == SYNC_VALUE) { + if (DtsUtil.isSyncWord(syncBytes)) { + headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF); + headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF); + headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF); + headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF); + bytesRead = 4; syncBytes = 0; return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 19b303484f..fb5f9525e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -263,6 +263,40 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Overwrites {@code numBits} from this array using the {@code numBits} least significant bits + * from {@code value}. Bits are written in order from most significant to least significant. The + * read position is advanced by {@code numBits}. + * + * @param value The integer whose {@code numBits} least significant bits are written into {@link + * #data}. + * @param numBits The number of bits to write. + */ + public void putInt(int value, int numBits) { + int remainingBitsToRead = numBits; + if (numBits < 32) { + value &= (1 << numBits) - 1; + } + int firstByteReadSize = Math.min(8 - bitOffset, numBits); + int firstByteRightPaddingSize = 8 - bitOffset - firstByteReadSize; + int firstByteBitmask = (0xFF00 >> bitOffset) | ((1 << firstByteRightPaddingSize) - 1); + data[byteOffset] &= firstByteBitmask; + int firstByteInputBits = value >>> (numBits - firstByteReadSize); + data[byteOffset] |= firstByteInputBits << firstByteRightPaddingSize; + remainingBitsToRead -= firstByteReadSize; + int currentByteIndex = byteOffset + 1; + while (remainingBitsToRead > 8) { + data[currentByteIndex++] = (byte) (value >>> (remainingBitsToRead - 8)); + remainingBitsToRead -= 8; + } + int lastByteRightPaddingSize = 8 - remainingBitsToRead; + data[currentByteIndex] &= (1 << lastByteRightPaddingSize) - 1; + int lastByteInput = value & ((1 << remainingBitsToRead) - 1); + data[currentByteIndex] |= lastByteInput << lastByteRightPaddingSize; + skipBits(numBits); + assertValidOffset(); + } + private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index 611584a38c..438643b933 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -169,6 +169,73 @@ public final class ParsableBitArrayTest { assertReadBitsToEnd(16); } + @Test + public void testPutBitsWithinByte() { + ParsableBitArray output = new ParsableBitArray(new byte[4]); + output.skipBits(1); + + output.putInt(0x3F, 5); + + output.setPosition(0); + assertThat(output.readBits(8)).isEqualTo(0x1F << 2); // Check that only 5 bits are modified. + } + + @Test + public void testPutBitsAcrossTwoBytes() { + ParsableBitArray output = new ParsableBitArray(new byte[4]); + output.setPosition(12); + + output.putInt(0xFF, 8); + output.setPosition(8); + + assertThat(output.readBits(16)).isEqualTo(0x0FF0); + } + + @Test + public void testPutBitsAcrossMultipleBytes() { + ParsableBitArray output = new ParsableBitArray(new byte[8]); + output.setPosition(31); // Writing starts at 31 to test the 30th bit is not modified. + + output.putInt(0xFF146098, 30); // Write only 30 to test the 61st bit is not modified. + + output.setPosition(30); + assertThat(output.readBits(32)).isEqualTo(0x3F146098 << 1); + } + + @Test + public void testPut32Bits() { + ParsableBitArray output = new ParsableBitArray(new byte[5]); + output.setPosition(4); + + output.putInt(0xFF146098, 32); + + output.setPosition(4); + assertThat(output.readBits(32)).isEqualTo(0xFF146098); + } + + @Test + public void testPutFullBytes() { + ParsableBitArray output = new ParsableBitArray(new byte[2]); + + output.putInt(0x81, 8); + + output.setPosition(0); + assertThat(output.readBits(8)).isEqualTo(0x81); + } + + @Test + public void testNoOverwriting() { + ParsableBitArray output = + new ParsableBitArray( + new byte[] {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff}); + output.setPosition(1); + + output.putInt(0, 30); + + output.setPosition(0); + assertThat(output.readBits(32)).isEqualTo(0x80000001); + } + private void assertReadBitsToEnd(int expectedStartPosition) { int position = testArray.getPosition(); assertThat(position).isEqualTo(expectedStartPosition); From 25c241e5bc9c179cb5681072faed9b490fd8861e Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 17 Feb 2018 13:08:14 -0500 Subject: [PATCH 051/376] add h263 format support in fourcc codecID --- .../extractor/mkv/MatroskaExtractor.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 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 57128f45f0..0712434555 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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -219,6 +220,7 @@ public final class MatroskaExtractor implements Extractor { private static final int LACING_EBML = 3; private static final int FOURCC_COMPRESSION_VC1 = 0x31435657; + private static final int FOURCC_COMPRESSION_DIVX = 0x58564944; /** * A template for the prefix that must be added to each subrip sample. The 12 byte end timecode @@ -1711,9 +1713,22 @@ public final class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - if (initializationData != null) { - mimeType = MimeTypes.VIDEO_VC1; + initializationData = null; + Pair> pair = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); + if (pair != null) { + initializationData = pair.second; + switch (pair.first) { + case FOURCC_COMPRESSION_VC1: + mimeType = MimeTypes.VIDEO_VC1; + break; + case FOURCC_COMPRESSION_DIVX: + mimeType = MimeTypes.VIDEO_H263; + break; + default: + Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + mimeType = MimeTypes.VIDEO_UNKNOWN; + break; + } } else { Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); mimeType = MimeTypes.VIDEO_UNKNOWN; @@ -1938,12 +1953,15 @@ public final class MatroskaExtractor implements Extractor { * not VC1. * @throws ParserException If the initialization data could not be built. */ - private static List parseFourCcVc1Private(ParsableByteArray buffer) + private static Pair> parseFourCcVc1Private(ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); - if (compression != FOURCC_COMPRESSION_VC1) { + if (compression == FOURCC_COMPRESSION_DIVX) { + return new Pair(FOURCC_COMPRESSION_DIVX, null); + } + if (compression != FOURCC_COMPRESSION_VC1) { return null; } @@ -1956,7 +1974,7 @@ public final class MatroskaExtractor implements Extractor { && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { // We've found the initialization data. byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); - return Collections.singletonList(initializationData); + return new Pair(FOURCC_COMPRESSION_VC1, Collections.singletonList(initializationData)); } } From 9e340291dee9e828150ad948e535e5293afcd167 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sun, 18 Feb 2018 16:53:56 -0500 Subject: [PATCH 052/376] mkv h263 CR changes --- .../extractor/mkv/MatroskaExtractor.java | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 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 0712434555..b32a634bcc 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 @@ -1713,26 +1713,9 @@ public final class MatroskaExtractor implements Extractor { nalUnitLengthFieldLength = hevcConfig.nalUnitLengthFieldLength; break; case CODEC_ID_FOURCC: - initializationData = null; - Pair> pair = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - if (pair != null) { - initializationData = pair.second; - switch (pair.first) { - case FOURCC_COMPRESSION_VC1: - mimeType = MimeTypes.VIDEO_VC1; - break; - case FOURCC_COMPRESSION_DIVX: - mimeType = MimeTypes.VIDEO_H263; - break; - default: - Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); - mimeType = MimeTypes.VIDEO_UNKNOWN; - break; - } - } else { - Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); - mimeType = MimeTypes.VIDEO_UNKNOWN; - } + Pair> pair = parseFourCcPrivate(new ParsableByteArray(codecPrivate)); + mimeType = pair.first; + initializationData = pair.second; break; case CODEC_ID_THEORA: // TODO: This can be set to the real mimeType if/when we work out what initializationData @@ -1947,41 +1930,42 @@ public final class MatroskaExtractor implements Extractor { /** * Builds initialization data for a {@link Format} from FourCC codec private data. *

- * VC1 is the only supported compression type. + * VC1 and H263 are the only supported compression types. * - * @return The initialization data for the {@link Format}, or null if the compression type is - * not VC1. + * @return A pair object with the first object being the codec mime type + * and the second object the initialization data for the {@link Format}, + * or null if the compression type is not VC1. * @throws ParserException If the initialization data could not be built. */ - private static Pair> parseFourCcVc1Private(ParsableByteArray buffer) + private static Pair> parseFourCcPrivate(ParsableByteArray buffer) throws ParserException { try { buffer.skipBytes(16); // size(4), width(4), height(4), planes(2), bitcount(2). long compression = buffer.readLittleEndianUnsignedInt(); if (compression == FOURCC_COMPRESSION_DIVX) { - return new Pair(FOURCC_COMPRESSION_DIVX, null); + return new Pair<>(MimeTypes.VIDEO_H263, null); } - if (compression != FOURCC_COMPRESSION_VC1) { - return null; - } - - // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 - // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). - int startOffset = buffer.getPosition() + 20; - byte[] bufferData = buffer.data; - for (int offset = startOffset; offset < bufferData.length - 4; offset++) { - if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 - && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { - // We've found the initialization data. - byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); - return new Pair(FOURCC_COMPRESSION_VC1, Collections.singletonList(initializationData)); + else if (compression == FOURCC_COMPRESSION_VC1) { + // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 + // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). + int startOffset = buffer.getPosition() + 20; + byte[] bufferData = buffer.data; + for (int offset = startOffset; offset < bufferData.length - 4; offset++) { + if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { + // We've found the initialization data. + byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); + return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData)); + } } + throw new ParserException("Failed to find FourCC VC1 initialization data"); } - - throw new ParserException("Failed to find FourCC VC1 initialization data"); } catch (ArrayIndexOutOfBoundsException e) { - throw new ParserException("Error parsing FourCC VC1 codec private"); + throw new ParserException("Error parsing FourCC private data"); } + + Log.w(TAG, "Unknown FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + return new Pair<>(MimeTypes.VIDEO_UNKNOWN, null); } /** From 6c3b677ddbb39e0e0fd3c09a8557e76980e2f02e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 16 Feb 2018 06:07:55 -0800 Subject: [PATCH 053/376] Catch exceptions in all IMA callbacks If an exception is thrown in an IMA callback it crashes the process with lots of logging from WebView (including several stack traces, etc.). This change wraps ImaAdsLoader code that might throw, skips any remaining ads (as the errors are not recoverable, in general) and notifies a new load error callback so that the application can implement its own handling. The intention is to make the loader robust to unexpected requests from IMA and avoid crashes. Also handle IMA loading an ad in an ad group that has no available ads. In rare cases IMA will try to load an ad for which an error was previously notified, so this drops those load requests allowing playback of the content to continue. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185985850 --- RELEASENOTES.md | 1 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 280 +++++++++++------- .../exoplayer2/source/ads/AdsLoader.java | 12 +- .../exoplayer2/source/ads/AdsMediaSource.java | 33 ++- .../android/exoplayer2/util/EventLogger.java | 5 + 5 files changed, 212 insertions(+), 119 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7204ee88fd..9440e7ca4a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -101,6 +101,7 @@ ([#3715](https://github.com/google/ExoPlayer/issues/3715)). * Propagate ad media preparation errors to IMA so that the ads can be skipped. + * Handle exceptions in IMA callbacks so that can be logged less verbosely. * `EventLogger` moved from the demo app into the core library. * Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, Lenovo K4 Note and Sony Xperia E5. 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 43e3c0d93d..a9eb14c57e 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 @@ -401,7 +401,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); - maybeNotifyAdError(); + maybeNotifyPendingAdLoadError(); if (adPlaybackState != null) { // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState); @@ -447,35 +447,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (player == null) { return; } - if (DEBUG) { - Log.d( - TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); + try { + handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception); + } catch (Exception e) { + maybeNotifyInternalError("handlePrepareError", e); } - if (imaAdState == IMA_AD_STATE_NONE) { - // Send IMA a content position at the ad group so that it will try to play it, at which point - // we can notify that it failed to load. - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - shouldNotifyAdPrepareError = true; - } else { - // We're already playing an ad. - if (adIndexInAdGroup > playingAdIndexInAdGroup) { - // Mark the playing ad as ended so we can notify the error on the next ad and remove it, - // which means that the ad after will load (if any). - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onEnded(); - } - } - playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onError(); - } - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); - updateAdPlaybackState(); } // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. @@ -493,7 +469,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.addAdEventListener(this); if (player != null) { // If a player is attached already, start playback immediately. - startAdPlayback(); + try { + startAdPlayback(); + } catch (Exception e) { + maybeNotifyInternalError("onAdsManagerLoaded", e); + } } } @@ -509,75 +489,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.w(TAG, "Dropping ad event after release: " + adEvent); return; } - Ad ad = adEvent.getAd(); - switch (adEvent.getType()) { - case LOADED: - // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. - AdPodInfo adPodInfo = ad.getAdPodInfo(); - int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = - podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); - int adPosition = adPodInfo.getAdPosition(); - int adCount = adPodInfo.getTotalAds(); - adsManager.start(); - if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); - } - int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; - if (adCount != oldAdCount) { - if (oldAdCount == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); - updateAdPlaybackState(); - } else { - // IMA sometimes unexpectedly decreases the ad count in an ad group. - Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); - } - } - if (adGroupIndex != expectedAdGroupIndex) { - Log.w( - TAG, - "Expected ad group index " - + expectedAdGroupIndex - + ", actual ad group index " - + adGroupIndex); - expectedAdGroupIndex = adGroupIndex; - } - break; - case CONTENT_PAUSE_REQUESTED: - // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads - // before sending CONTENT_RESUME_REQUESTED. - imaPausedContent = true; - pauseContentInternal(); - break; - case STARTED: - if (ad.isSkippable()) { - focusSkipButton(); - } - break; - case TAPPED: - if (eventListener != null) { - eventListener.onAdTapped(); - } - break; - case CLICKED: - if (eventListener != null) { - eventListener.onAdClicked(); - } - break; - case CONTENT_RESUME_REQUESTED: - imaPausedContent = false; - resumeContentInternal(); - break; - case LOG: - Map adData = adEvent.getAdData(); - Log.i(TAG, "Log AdEvent: " + adData); - if ("adLoadError".equals(adData.get("type"))) { - handleAdGroupLoadError(); - } - break; - case ALL_ADS_COMPLETED: - default: - break; + try { + handleAdEvent(adEvent); + } catch (Exception e) { + maybeNotifyInternalError("onAdEvent", e); } } @@ -595,12 +510,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adPlaybackState = new AdPlaybackState(); updateAdPlaybackState(); } else if (isAdGroupLoadError(error)) { - handleAdGroupLoadError(); + try { + handleAdGroupLoadError(); + } catch (Exception e) { + maybeNotifyInternalError("onAdError", e); + } } if (pendingAdErrorEvent == null) { pendingAdErrorEvent = adErrorEvent; } - maybeNotifyAdError(); + maybeNotifyPendingAdLoadError(); } // ContentProgressProvider implementation. @@ -670,10 +589,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "loadAd in ad group " + adGroupIndex); } - int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); - adPlaybackState = - adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); - updateAdPlaybackState(); + try { + int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex); + if (adIndexInAdGroup == C.INDEX_UNSET) { + Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads"); + return; + } + adPlaybackState = + adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString)); + updateAdPlaybackState(); + } catch (Exception e) { + maybeNotifyInternalError("loadAd", e); + } } @Override @@ -739,7 +666,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.w(TAG, "Unexpected stopAd"); return; } - stopAdInternal(); + try { + stopAdInternal(); + } catch (Exception e) { + maybeNotifyInternalError("stopAd", e); + } } @Override @@ -760,7 +691,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void resumeAd() { // This method is never called. See [Internal: b/18931719]. - throw new IllegalStateException(); + maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd")); } // Player.EventListener implementation. @@ -902,12 +833,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - private void maybeNotifyAdError() { - if (eventListener != null && pendingAdErrorEvent != null) { - IOException exception = - new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()); - eventListener.onLoadError(exception); - pendingAdErrorEvent = null; + private void handleAdEvent(AdEvent adEvent) { + Ad ad = adEvent.getAd(); + switch (adEvent.getType()) { + case LOADED: + // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. + AdPodInfo adPodInfo = ad.getAdPodInfo(); + int podIndex = adPodInfo.getPodIndex(); + adGroupIndex = + podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); + int adPosition = adPodInfo.getAdPosition(); + int adCount = adPodInfo.getTotalAds(); + adsManager.start(); + if (DEBUG) { + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); + } + int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count; + if (adCount != oldAdCount) { + if (oldAdCount == C.LENGTH_UNSET) { + adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount); + updateAdPlaybackState(); + } else { + // IMA sometimes unexpectedly decreases the ad count in an ad group. + Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount); + } + } + if (adGroupIndex != expectedAdGroupIndex) { + Log.w( + TAG, + "Expected ad group index " + + expectedAdGroupIndex + + ", actual ad group index " + + adGroupIndex); + expectedAdGroupIndex = adGroupIndex; + } + break; + case CONTENT_PAUSE_REQUESTED: + // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads + // before sending CONTENT_RESUME_REQUESTED. + imaPausedContent = true; + pauseContentInternal(); + break; + case STARTED: + if (ad.isSkippable()) { + focusSkipButton(); + } + break; + case TAPPED: + if (eventListener != null) { + eventListener.onAdTapped(); + } + break; + case CLICKED: + if (eventListener != null) { + eventListener.onAdClicked(); + } + break; + case CONTENT_RESUME_REQUESTED: + imaPausedContent = false; + resumeContentInternal(); + break; + case LOG: + Map adData = adEvent.getAdData(); + Log.i(TAG, "Log AdEvent: " + adData); + if ("adLoadError".equals(adData.get("type"))) { + handleAdGroupLoadError(); + } + break; + case ALL_ADS_COMPLETED: + default: + break; } } @@ -1001,6 +996,38 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A updateAdPlaybackState(); } + private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) { + if (DEBUG) { + Log.d( + TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception); + } + if (imaAdState == IMA_AD_STATE_NONE) { + // Send IMA a content position at the ad group so that it will try to play it, at which point + // we can notify that it failed to load. + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { + fakeContentProgressOffsetMs = contentDurationMs; + } + shouldNotifyAdPrepareError = true; + } else { + // We're already playing an ad. + if (adIndexInAdGroup > playingAdIndexInAdGroup) { + // Mark the playing ad as ended so we can notify the error on the next ad and remove it, + // which means that the ad after will load (if any). + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); + } + } + playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(); + } + } + adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup); + updateAdPlaybackState(); + } + private void checkForContentComplete() { if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs @@ -1044,6 +1071,33 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup; } + private void maybeNotifyPendingAdLoadError() { + if (pendingAdErrorEvent != null) { + if (eventListener != null) { + eventListener.onAdLoadError( + new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError())); + } + pendingAdErrorEvent = null; + } + } + + private void maybeNotifyInternalError(String name, Exception cause) { + String message = "Internal error in " + name; + Log.e(TAG, message, cause); + if (eventListener != null) { + eventListener.onInternalAdLoadError(new RuntimeException(message, cause)); + } + // We can't recover from an unexpected error in general, so skip all remaining ads. + if (adPlaybackState == null) { + adPlaybackState = new AdPlaybackState(); + } else { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + adPlaybackState = adPlaybackState.withSkippedAdGroup(i); + } + } + updateAdPlaybackState(); + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. 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 91111ec0ea..6295ca4229 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 @@ -54,11 +54,19 @@ public interface AdsLoader { void onAdPlaybackState(AdPlaybackState adPlaybackState); /** - * Called when there was an error loading ads. + * Called when there was an error loading ads. The loader will skip the problematic ad(s). * * @param error The error. */ - void onLoadError(IOException error); + void onAdLoadError(IOException error); + + /** + * Called when an unexpected internal error is encountered while loading ads. The loader will + * skip all remaining ads, as the error is not recoverable. + * + * @param error The error. + */ + void onInternalAdLoadError(RuntimeException error); /** * Called when the user clicks through an ad (for example, following a 'learn more' link). 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 854be90d1c..64bab7ed96 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 @@ -73,14 +73,21 @@ public final class AdsMediaSource extends CompositeMediaSource { public interface EventListener extends MediaSourceEventListener { /** - * Called if there was an error loading ads. The media source will load the content without ads - * if ads can't be loaded, so listen for this event if you need to implement additional handling - * (for example, stopping the player). + * Called if there was an error loading one or more ads. The loader will skip the problematic + * ad(s). * * @param error The error. */ void onAdLoadError(IOException error); + /** + * Called when an unexpected internal error is encountered while loading ads. The loader will + * skip all remaining ads, as the error is not recoverable. + * + * @param error The error. + */ + void onInternalAdLoadError(RuntimeException error); + /** * Called when the user clicks through an ad (for example, following a 'learn more' link). */ @@ -418,7 +425,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void onLoadError(final IOException error) { + public void onAdLoadError(final IOException error) { if (released) { return; } @@ -436,6 +443,24 @@ public final class AdsMediaSource extends CompositeMediaSource { } } + @Override + public void onInternalAdLoadError(final RuntimeException error) { + if (released) { + return; + } + Log.w(TAG, "Internal ad load error", error); + if (eventHandler != null && eventListener != null) { + eventHandler.post( + new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onInternalAdLoadError(error); + } + } + }); + } + } } private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener { 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 3a178a7f4a..d95f387996 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 @@ -382,6 +382,11 @@ public class EventLogger printInternalError("adLoadError", error); } + @Override + public void onInternalAdLoadError(RuntimeException error) { + printInternalError("internalAdLoadError", error); + } + @Override public void onAdClicked() { // Do nothing. From 56c9d023faa856fa72bff6ab40637cdef4e032e4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 16 Feb 2018 07:36:08 -0800 Subject: [PATCH 054/376] Fix content progress updates and position faking Occasionally the player could transition from playing content to playing an ad after IMA called playAd. The discontinuity triggered faking the content position, and the fake position was passed to IMA when content resumed causing the wrong ad group to be loaded. Fix this by only faking the position if the player transitions before playAd. Also fix the calculation of the expected ad group index for a postroll ad, and wait for the player to transition back from ad to content before passing a content progress update. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185994229 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 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 a9eb14c57e..8ab05c574d 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 @@ -252,10 +252,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private boolean shouldNotifyAdPrepareError; /** - * If a content period has finished but IMA has not yet sent an ad event with {@link - * AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of {@link - * SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to determine - * a fake, increasing content position. {@link C#TIME_UNSET} otherwise. + * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value + * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to + * determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise. */ private long fakeContentProgressElapsedRealtimeMs; /** @@ -541,18 +540,21 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; expectedAdGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs)); - } else if (imaAdState == IMA_AD_STATE_NONE && hasContentDuration) { + } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) { contentPositionMs = player.getCurrentPosition(); // Update the expected ad group index for the current content position. The update is delayed // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered // just after an ad group isn't incorrectly attributed to the next ad group. int nextAdGroupIndex = adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs)); - if (nextAdGroupIndex != expectedAdGroupIndex - && nextAdGroupIndex != C.INDEX_UNSET - && C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]) - contentPositionMs - < MAXIMUM_PRELOAD_DURATION_MS) { - expectedAdGroupIndex = nextAdGroupIndex; + if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) { + long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]); + if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) { + nextAdGroupTimeMs = contentDurationMs; + } + if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) { + expectedAdGroupIndex = nextAdGroupIndex; + } } } else { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; @@ -567,12 +569,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public VideoProgressUpdate getAdProgress() { if (player == null) { return lastAdProgress; - } else if (imaAdState == IMA_AD_STATE_NONE) { - return VideoProgressUpdate.VIDEO_TIME_NOT_READY; - } else { + } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) { long adDuration = player.getDuration(); return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); + } else { + return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } } @@ -625,6 +627,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.w(TAG, "Unexpected playAd without stopAd"); break; case IMA_AD_STATE_NONE: + // IMA is requesting to play the ad, so stop faking the content position. + fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; + fakeContentProgressOffsetMs = C.TIME_UNSET; imaAdState = IMA_AD_STATE_PLAYING; for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onPlay(); @@ -923,9 +928,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); } } - if (!wasPlayingAd && playingAd) { + if (!wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) { int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + // IMA hasn't called playAd yet, so fake the content position. fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { @@ -955,9 +960,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A pendingContentPositionMs = C.TIME_UNSET; sentPendingContentPositionMs = false; } - // IMA is requesting to pause content, so stop faking the content position. - fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; - fakeContentProgressOffsetMs = C.TIME_UNSET; } private void stopAdInternal() { From 24067851601eea09c786591e4cf3a478df59fb59 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 16 Feb 2018 09:01:50 -0800 Subject: [PATCH 055/376] Move DownloadManagerTest to Robolectric. The waiting for a ConditionVariable to be false was replaced by a CountDownLatch whose count is asserted to be one. The timeout of a ConditionVariable doesn't work in Robolectric unless someone is manually increasing the SystemClock time. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186003125 --- .../offline/DownloadManagerTest.java | 176 ++++++++++-------- 1 file changed, 100 insertions(+), 76 deletions(-) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java (91%) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java similarity index 91% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 10940eec49..9a1efc7df6 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -16,16 +16,16 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.os.ConditionVariable; import android.support.annotation.Nullable; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; import com.google.android.exoplayer2.testutil.DummyMainThread; -import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Util; @@ -36,11 +36,22 @@ import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** Tests {@link DownloadManager}. */ -public class DownloadManagerTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadManagerTest { /* Used to check if condition becomes true in this time interval. */ private static final int ASSERT_TRUE_TIMEOUT = 10000; @@ -56,71 +67,33 @@ public class DownloadManagerTest extends InstrumentationTestCase { private TestDownloadListener testDownloadListener; private DummyMainThread dummyMainThread; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - MockitoUtil.setUpMockito(this); + MockitoAnnotations.initMocks(this); dummyMainThread = new DummyMainThread(); - actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); + actionFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); testDownloadListener = new TestDownloadListener(); setUpDownloadManager(100); } - @Override + @After public void tearDown() throws Exception { releaseDownloadManager(); actionFile.delete(); dummyMainThread.release(); - super.tearDown(); - } - - private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { - if (downloadManager != null) { - releaseDownloadManager(); - } - try { - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager = - new DownloadManager( - new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY), - maxActiveDownloadTasks, - MIN_RETRY_COUNT, - actionFile.getAbsolutePath()); - downloadManager.addListener(testDownloadListener); - downloadManager.startDownloads(); - } - }); - } catch (Throwable throwable) { - throw new Exception(throwable); - } - } - - private void releaseDownloadManager() throws Exception { - try { - runOnMainThread( - new Runnable() { - @Override - public void run() { - downloadManager.release(); - } - }); - } catch (Throwable throwable) { - throw new Exception(throwable); - } } + @Test public void testDownloadActionRuns() throws Throwable { doTestActionRuns(createDownloadAction("media 1")); } + @Test public void testRemoveActionRuns() throws Throwable { doTestActionRuns(createRemoveAction("media 1")); } + @Test public void testDownloadRetriesThenFails() throws Throwable { FakeDownloadAction downloadAction = createDownloadAction("media 1"); downloadAction.post(); @@ -135,6 +108,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testDownloadNoRetryWhenCancelled() throws Throwable { FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); downloadAction.getFakeDownloader().enableDownloadIOException = true; @@ -148,6 +122,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testDownloadRetriesThenContinues() throws Throwable { FakeDownloadAction downloadAction = createDownloadAction("media 1"); downloadAction.post(); @@ -165,6 +140,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test @SuppressWarnings({"NonAtomicVolatileUpdate", "NonAtomicOperationOnVolatileField"}) public void testDownloadRetryCountResetsOnProgress() throws Throwable { FakeDownloadAction downloadAction = createDownloadAction("media 1"); @@ -185,36 +161,37 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 2")); + doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 2")); } + @Test public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createRemoveAction("media 2")); + doTestActionsRunInParallel(createDownloadAction("media 1"), createRemoveAction("media 2")); } + @Test public void testSameMediaDownloadActionsStartInParallel() throws Throwable { - doTestActionsRunInParallel(createDownloadAction("media 1"), - createDownloadAction("media 1")); + doTestActionsRunInParallel(createDownloadAction("media 1"), createDownloadAction("media 1")); } + @Test public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { - doTestActionsRunSequentially(createDownloadAction("media 1"), - createRemoveAction("media 1")); + doTestActionsRunSequentially(createDownloadAction("media 1"), createRemoveAction("media 1")); } + @Test public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createDownloadAction("media 1")); + doTestActionsRunSequentially(createRemoveAction("media 1"), createDownloadAction("media 1")); } + @Test public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { - doTestActionsRunSequentially(createRemoveAction("media 1"), - createRemoveAction("media 1")); + doTestActionsRunSequentially(createRemoveAction("media 1"), createRemoveAction("media 1")); } + @Test public void testSameMediaMultipleActions() throws Throwable { FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); @@ -250,6 +227,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testMultipleRemoveActionWaitsLastCancelsAllOther() throws Throwable { FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); FakeDownloadAction removeAction2 = createRemoveAction("media 1"); @@ -267,6 +245,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testGetTasks() throws Throwable { FakeDownloadAction removeAction = createRemoveAction("media 1"); FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); @@ -283,6 +262,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { assertThat(states[2].downloadAction).isEqualTo(downloadAction2); } + @Test public void testMultipleWaitingDownloadActionStartsInParallel() throws Throwable { FakeDownloadAction removeAction = createRemoveAction("media 1"); FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); @@ -301,6 +281,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testDifferentMediaDownloadActionsPreserveOrder() throws Throwable { FakeDownloadAction removeAction = createRemoveAction("media 1").ignoreInterrupts(); FakeDownloadAction downloadAction1 = createDownloadAction("media 1"); @@ -319,6 +300,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testDifferentMediaRemoveActionsDoNotPreserveOrder() throws Throwable { FakeDownloadAction downloadAction = createDownloadAction("media 1").ignoreInterrupts(); FakeDownloadAction removeAction1 = createRemoveAction("media 1"); @@ -337,6 +319,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testStopAndResume() throws Throwable { FakeDownloadAction download1Action = createDownloadAction("media 1"); FakeDownloadAction remove2Action = createRemoveAction("media 2"); @@ -385,6 +368,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + @Test public void testResumeBeforeTotallyStopped() throws Throwable { setUpDownloadManager(2); FakeDownloadAction download1Action = createDownloadAction("media 1").ignoreInterrupts(); @@ -431,13 +415,52 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } + private void setUpDownloadManager(final int maxActiveDownloadTasks) throws Exception { + if (downloadManager != null) { + releaseDownloadManager(); + } + try { + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager = + new DownloadManager( + new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY), + maxActiveDownloadTasks, + MIN_RETRY_COUNT, + actionFile.getAbsolutePath()); + downloadManager.addListener(testDownloadListener); + downloadManager.startDownloads(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + + private void releaseDownloadManager() throws Exception { + try { + runOnMainThread( + new Runnable() { + @Override + public void run() { + downloadManager.release(); + } + }); + } catch (Throwable throwable) { + throw new Exception(throwable); + } + } + private void doTestActionRuns(FakeDownloadAction action) throws Throwable { action.post().assertStarted().unblock().assertEnded(); testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private void doTestActionsRunSequentially(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { + private void doTestActionsRunSequentially(FakeDownloadAction action1, FakeDownloadAction action2) + throws Throwable { action1.ignoreInterrupts().post().assertStarted(); action2.post().assertDoesNotStart(); @@ -448,8 +471,8 @@ public class DownloadManagerTest extends InstrumentationTestCase { testDownloadListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); } - private void doTestActionsRunInParallel(FakeDownloadAction action1, - FakeDownloadAction action2) throws Throwable { + private void doTestActionsRunInParallel(FakeDownloadAction action1, FakeDownloadAction action2) + throws Throwable { action1.post().assertStarted(); action2.post().assertStarted(); action1.unblock().assertEnded(); @@ -502,7 +525,6 @@ public class DownloadManagerTest extends InstrumentationTestCase { throw new Exception(downloadError); } } - } private class FakeDownloadAction extends DownloadAction { @@ -561,12 +583,13 @@ public class DownloadManagerTest extends InstrumentationTestCase { return this; } - private FakeDownloadAction assertDoesNotStart() { - assertThat(downloader.started.block(ASSERT_FALSE_TIME)).isFalse(); + private FakeDownloadAction assertDoesNotStart() throws InterruptedException { + Thread.sleep(ASSERT_FALSE_TIME); + assertThat(downloader.started.getCount()).isEqualTo(1); return this; } - private FakeDownloadAction assertStarted() { + private FakeDownloadAction assertStarted() throws InterruptedException { downloader.assertStarted(ASSERT_TRUE_TIMEOUT); return assertState(DownloadState.STATE_STARTED); } @@ -636,16 +659,18 @@ public class DownloadManagerTest extends InstrumentationTestCase { } private static class FakeDownloader implements Downloader { - private final ConditionVariable started; + private final com.google.android.exoplayer2.util.ConditionVariable blocker; private final boolean removeAction; + + private CountDownLatch started; private boolean ignoreInterrupts; private volatile boolean enableDownloadIOException; private volatile int downloadedBytes = C.LENGTH_UNSET; private FakeDownloader(boolean removeAction) { this.removeAction = removeAction; - this.started = new ConditionVariable(); + this.started = new CountDownLatch(1); this.blocker = new com.google.android.exoplayer2.util.ConditionVariable(); } @@ -658,7 +683,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { public void download(@Nullable ProgressListener listener) throws InterruptedException, IOException { assertThat(removeAction).isFalse(); - started.open(); + started.countDown(); block(); if (enableDownloadIOException) { throw new IOException(); @@ -668,7 +693,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { @Override public void remove() throws InterruptedException { assertThat(removeAction).isTrue(); - started.open(); + started.countDown(); block(); } @@ -689,9 +714,9 @@ public class DownloadManagerTest extends InstrumentationTestCase { } } - private FakeDownloader assertStarted(int timeout) { - assertThat(started.block(timeout)).isTrue(); - started.close(); + private FakeDownloader assertStarted(int timeout) throws InterruptedException { + assertThat(started.await(timeout, TimeUnit.MILLISECONDS)).isTrue(); + started = new CountDownLatch(1); return this; } @@ -710,5 +735,4 @@ public class DownloadManagerTest extends InstrumentationTestCase { return Float.NaN; } } - } From 69496eb17efd5cb2f7a572a17bb8608523792159 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Feb 2018 03:02:12 -0800 Subject: [PATCH 056/376] Bump to 2.7.0 and prepare release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186200840 --- RELEASENOTES.md | 78 ++++++++++--------- constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 +- demos/ima/src/main/AndroidManifest.xml | 4 +- demos/main/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +- 6 files changed, 53 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9440e7ca4a..b71b54575b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,14 +2,12 @@ ### dev-v2 (not yet released) ### -* SimpleExoPlayerView: Automatically apply video rotation if - `SimpleExoPlayerView` is configured to use `TextureView` - ([#91](https://github.com/google/ExoPlayer/issues/91)). +* Downloading: Add `DownloadService`, `DownloadManager` and + related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). + +### 2.7.0 ### + * Player interface: - * Add `Player.VideoComponent`, `Player.TextComponent` and - `Player.MetadataComponent` interfaces that define optional video, text and - metadata output functionality. New `getVideoComponent`, `getTextComponent` - and `getMetadataComponent` methods provide access to this functionality. * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. @@ -21,18 +19,23 @@ more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler ([#2189](https://github.com/google/ExoPlayer/issues/2189)). -* UI components: - * Generalized player and control views to allow them to bind with any - `Player`, and renamed them to `PlayerView` and `PlayerControlView` - respectively. - * Made `PlayerView`'s play button behave correctly when the player is ended - ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a - `PlaybackPreparer` when the player is idle. + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. `SeekParameters` are not currently supported when playing HLS + streams. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. - * Add ability for `SequenceableLoader` to reevaluate its buffer and discard + * Add ability for `SequenceableLoader` to re-evaluate its buffer and discard buffered media so that it can be re-buffered in a different quality. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` @@ -40,29 +43,33 @@ `SsMediaSource.Factory`, and `MergingMediaSource`. * Play out existing buffer before retrying for progressive live streams ([#1606](https://github.com/google/ExoPlayer/issues/1606)). -* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are - performed. The `SeekParameters` class contains defaults for exact seeking and - seeking to the closest sync points before, either side or after specified seek - positions. - * Note: `SeekParameters` are not currently supported when playing HLS streams. +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. + * Made `PlayerView` automatically apply video rotation when configured to use + `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). + * Made `PlayerView` play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. * DRM: Optimistically attempt playback of DRM protected content that does not declare scheme specific init data in the manifest. If playback of clear samples without keys is allowed, delay DRM session error propagation until keys are actually needed ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: - * Support in-band Emsg events targeting player with scheme id - "urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3". - * Support DASH manifest EventStream elements. -* DefaultTrackSelector: - * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. - * Support disabling of individual text track selection flags. + * Support in-band Emsg events targeting the player with scheme id + "urn:mpeg:dash:event:2012" and scheme values "1", "2" and "3". + * Support EventStream elements in DASH manifests. * HLS: * Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/3149)). - * Fail on loss of sync with Transport Stream. + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). More details + can be found + [here](https://medium.com/google-exoplayer/faster-hls-preparation-f6611aa15ea6). + * Fail if unable to sync with the Transport Stream, rather than entering + stuck in an indefinite buffering state. * Fix mime type propagation ([#3653](https://github.com/google/ExoPlayer/issues/3653)). * Fix ID3 context reuse across segment format changes @@ -70,7 +77,6 @@ * Use long for media sequence numbers ([#3747](https://github.com/google/ExoPlayer/issues/3747)) * Add initial support for the EXT-X-GAP tag. -* New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: * Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). @@ -78,13 +84,16 @@ resolution output in `DefaultAudioSink` ([#3635](https://github.com/google/ExoPlayer/pull/3635)). * Captions: - * Initial support for PGS subtitles + * Basic support for PGS subtitles ([#3008](https://github.com/google/ExoPlayer/issues/3008)). - * Fix issue handling CEA-608 captions where multiple buffers have the same + * Fix handling of CEA-608 captions where multiple buffers have the same presentation timestamp ([#3782](https://github.com/google/ExoPlayer/issues/3782)). -* CacheDataSource: Check periodically if it's possible to read from/write to - cache after deciding to bypass cache. +* Caching: + * Fix cache corruption issue + ([#3762](https://github.com/google/ExoPlayer/issues/3762)). + * Implement periodic check in `CacheDataSource` to see whether it's possible + to switch to reading/writing the cache having initially bypassed it. * IMA extension: * Fix the player getting stuck when an ad group fails to load ([#3584](https://github.com/google/ExoPlayer/issues/3584)). @@ -102,6 +111,7 @@ * Propagate ad media preparation errors to IMA so that the ads can be skipped. * Handle exceptions in IMA callbacks so that can be logged less verbosely. +* New Cast extension. Simplifies toggling between local and Cast playbacks. * `EventLogger` moved from the demo app into the core library. * Fix ANR issue on the Huawei P8 Lite, Huawei Y6II, Moto C+, Meizu M5C, Lenovo K4 Note and Sony Xperia E5. @@ -110,8 +120,6 @@ * Fix potential NPE when removing media sources from a DynamicConcatenatingMediaSource ([#3796](https://github.com/google/ExoPlayer/issues/3796)). -* Open source DownloadService, DownloadManager and related classes - ([#2643](https://github.com/google/ExoPlayer/issues/2643)). * Check `sys.display-size` on Philips ATVs ([#3807](https://github.com/google/ExoPlayer/issues/3807)). * Release `Extractor`s on the loading thread to avoid potentially leaking diff --git a/constants.gradle b/constants.gradle index c18fb28d4d..e0e8bbcc69 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = '2.6.1' + releaseVersion = '2.7.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index e12e27fa4c..d23576572a 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2700" + android:versionName="2.7.0"> diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 0efeaf6f7f..7f169b8095 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2700" + android:versionName="2.7.0"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 00326157a2..a98176d93b 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2700" + android:versionName="2.7.0"> 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 b2200b6671..1dec506ec9 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 @@ -31,13 +31,13 @@ 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.6.1"; + public static final String VERSION = "2.7.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.6.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.0"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,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 = 2006001; + public static final int VERSION_INT = 2007000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From eae1606b904935ce9641cfa1a37574b08ce59855 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Mon, 19 Feb 2018 16:57:54 -0500 Subject: [PATCH 057/376] comment and newline cleanup --- .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 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 b32a634bcc..c2b3735265 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 @@ -1934,7 +1934,7 @@ public final class MatroskaExtractor implements Extractor { * * @return A pair object with the first object being the codec mime type * and the second object the initialization data for the {@link Format}, - * or null if the compression type is not VC1. + * or null if the compression type is not a currently supported type (VC1 or H263). * @throws ParserException If the initialization data could not be built. */ private static Pair> parseFourCcPrivate(ParsableByteArray buffer) @@ -1944,8 +1944,7 @@ public final class MatroskaExtractor implements Extractor { long compression = buffer.readLittleEndianUnsignedInt(); if (compression == FOURCC_COMPRESSION_DIVX) { return new Pair<>(MimeTypes.VIDEO_H263, null); - } - else if (compression == FOURCC_COMPRESSION_VC1) { + } else if (compression == FOURCC_COMPRESSION_VC1) { // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). int startOffset = buffer.getPosition() + 20; From 44f683e04ef668be9ec0689a4649e002d435eb17 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Feb 2018 05:46:26 -0800 Subject: [PATCH 058/376] Fix robolectric tests when running with gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186291489 --- ISSUE_TEMPLATE | 2 ++ constants.gradle | 4 ++-- library/core/src/test/resources/robolectric.properties | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 library/core/src/test/resources/robolectric.properties diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index e85c0c28c7..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,3 +1,5 @@ +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** + Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. diff --git a/constants.gradle b/constants.gradle index e0e8bbcc69..b02e2d4c37 100644 --- a/constants.gradle +++ b/constants.gradle @@ -26,8 +26,8 @@ project.ext { dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' - truthVersion = '0.35' - robolectricVersion = '3.4.2' + truthVersion = '0.39' + robolectricVersion = '3.7.1' releaseVersion = '2.7.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { diff --git a/library/core/src/test/resources/robolectric.properties b/library/core/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/library/core/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml From 356e97918785ca84a714d1d2cd7d71a78a0d79f9 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 20 Feb 2018 14:36:11 +0000 Subject: [PATCH 059/376] Fix issue template --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From fba2b46a8b547072e3e1391005aba123221549a7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 20 Feb 2018 08:21:19 -0800 Subject: [PATCH 060/376] Fix bad @see Javadoc in CastPlayer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186305613 --- .../exoplayer2/ext/cast/CastPlayer.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 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 d7756b8272..50c883c3f6 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 @@ -195,14 +195,14 @@ public final class CastPlayer implements Player { } /** - * Inserts a sequence of items into the media queue. If no media queue or period with id - * {@code periodId} exist, does nothing. + * Inserts a sequence of items into the media queue. If no media queue or period with id {@code + * periodId} exist, does nothing. * - * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item * that will follow immediately after the inserted items. * @param items The items to insert. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id - * {@code periodId} exist. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code + * periodId} exist. */ public PendingResult addItems(int periodId, MediaQueueItem... items) { if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID @@ -216,10 +216,10 @@ public final class CastPlayer implements Player { * Removes an item from the media queue. If no media queue or period with id {@code periodId} * exist, does nothing. * - * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item * to remove. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id - * {@code periodId} exist. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code + * periodId} exist. */ public PendingResult removeItem(int periodId) { if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { @@ -229,16 +229,15 @@ public final class CastPlayer implements Player { } /** - * Moves an existing item within the media queue. If no media queue or period with id - * {@code periodId} exist, does nothing. + * Moves an existing item within the media queue. If no media queue or period with id {@code + * periodId} exist, does nothing. * - * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item * to move. - * @param newIndex The target index of the item in the media queue. Must be in the range - * 0 <= index < {@link Timeline#getPeriodCount()}, as provided by - * {@link #getCurrentTimeline()}. - * @return The Cast {@code PendingResult}, or null if no media queue or no period with id - * {@code periodId} exist. + * @param newIndex The target index of the item in the media queue. Must be in the range 0 <= + * index < {@link Timeline#getPeriodCount()}, as provided by {@link #getCurrentTimeline()}. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id {@code + * periodId} exist. */ public PendingResult moveItem(int periodId, int newIndex) { Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); @@ -252,7 +251,7 @@ public final class CastPlayer implements Player { * Returns the item that corresponds to the period with the given id, or null if no media queue or * period with id {@code periodId} exist. * - * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * @param periodId The id of the period ({@link #getCurrentTimeline}) that corresponds to the item * to get. * @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. From e3d44646356342a5d4abd0afbd657be0dc184d7b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 21 Feb 2018 07:14:25 -0800 Subject: [PATCH 061/376] Drop EMSG atoms before MOOV ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186454715 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9a70dfbf90..7e40f6d2ee 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 @@ -549,7 +549,7 @@ public final class FragmentedMp4Extractor implements Extractor { * Parses an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - if (emsgTrackOutputs.length == 0) { + if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) { return; } From afd81146f5627d97b061ea56cbcd6b5b0a465cd6 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Thu, 22 Feb 2018 13:47:10 -0800 Subject: [PATCH 062/376] Add abstract logging methods to EventLogger --- .../android/exoplayer2/util/EventLogger.java | 119 +++++++++--------- 1 file changed, 63 insertions(+), 56 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 d95f387996..9af46d88c2 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 @@ -89,33 +89,33 @@ public class EventLogger @Override public void onLoadingChanged(boolean isLoading) { - Log.d(TAG, "loading [" + isLoading + "]"); + logd(TAG, "loading [" + isLoading + "]"); } @Override public void onPlayerStateChanged(boolean playWhenReady, int state) { - Log.d(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " + logd(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " + getStateString(state) + "]"); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); + logd(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); + logd(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); } @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - Log.d(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); + logd(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - Log.d(TAG, "playbackParameters " + String.format( + logd(TAG, "playbackParameters " + String.format( "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); } @@ -124,138 +124,138 @@ public class EventLogger @Player.TimelineChangeReason int reason) { int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); - Log.d(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount + logd(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount + ", reason=" + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getPeriod(i, period); - Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); + logd(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); } if (periodCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); + logd(TAG, " ..."); } for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getWindow(i, window); - Log.d(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " + logd(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " + window.isSeekable + ", " + window.isDynamic + "]"); } if (windowCount > MAX_TIMELINE_ITEM_LINES) { - Log.d(TAG, " ..."); + logd(TAG, " ..."); } - Log.d(TAG, "]"); + logd(TAG, "]"); } @Override public void onPlayerError(ExoPlaybackException e) { - Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e); + loge(TAG, "playerFailed [" + getSessionTimeString() + "]", e); } @Override public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo == null) { - Log.d(TAG, "Tracks []"); + logd(TAG, "Tracks []"); return; } - Log.d(TAG, "Tracks ["); + logd(TAG, "Tracks ["); // Log tracks associated to renderers. for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); TrackSelection trackSelection = trackSelections.get(rendererIndex); if (rendererTrackGroups.length > 0) { - Log.d(TAG, " Renderer:" + rendererIndex + " ["); + logd(TAG, " Renderer:" + rendererIndex + " ["); for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); - Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + logd(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); String formatSupport = getFormatSupportString( mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " + logd(TAG, " " + status + " Track:" + trackIndex + ", " + Format.toLogString(trackGroup.getFormat(trackIndex)) + ", supported=" + formatSupport); } - Log.d(TAG, " ]"); + logd(TAG, " ]"); } // Log metadata for at most one of the tracks selected for the renderer. if (trackSelection != null) { for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; if (metadata != null) { - Log.d(TAG, " Metadata ["); + logd(TAG, " Metadata ["); printMetadata(metadata, " "); - Log.d(TAG, " ]"); + logd(TAG, " ]"); break; } } } - Log.d(TAG, " ]"); + logd(TAG, " ]"); } } // Log tracks not associated with a renderer. TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); if (unassociatedTrackGroups.length > 0) { - Log.d(TAG, " Renderer:None ["); + logd(TAG, " Renderer:None ["); for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { - Log.d(TAG, " Group:" + groupIndex + " ["); + logd(TAG, " Group:" + groupIndex + " ["); TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); String formatSupport = getFormatSupportString( RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); - Log.d(TAG, " " + status + " Track:" + trackIndex + ", " + logd(TAG, " " + status + " Track:" + trackIndex + ", " + Format.toLogString(trackGroup.getFormat(trackIndex)) + ", supported=" + formatSupport); } - Log.d(TAG, " ]"); + logd(TAG, " ]"); } - Log.d(TAG, " ]"); + logd(TAG, " ]"); } - Log.d(TAG, "]"); + logd(TAG, "]"); } @Override public void onSeekProcessed() { - Log.d(TAG, "seekProcessed"); + logd(TAG, "seekProcessed"); } // MetadataOutput @Override public void onMetadata(Metadata metadata) { - Log.d(TAG, "onMetadata ["); + logd(TAG, "onMetadata ["); printMetadata(metadata, " "); - Log.d(TAG, "]"); + logd(TAG, "]"); } // AudioRendererEventListener @Override public void onAudioEnabled(DecoderCounters counters) { - Log.d(TAG, "audioEnabled [" + getSessionTimeString() + "]"); + logd(TAG, "audioEnabled [" + getSessionTimeString() + "]"); } @Override public void onAudioSessionId(int audioSessionId) { - Log.d(TAG, "audioSessionId [" + audioSessionId + "]"); + logd(TAG, "audioSessionId [" + audioSessionId + "]"); } @Override public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { - Log.d(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + logd(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); } @Override public void onAudioInputFormatChanged(Format format) { - Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + logd(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); } @Override public void onAudioDisabled(DecoderCounters counters) { - Log.d(TAG, "audioDisabled [" + getSessionTimeString() + "]"); + logd(TAG, "audioDisabled [" + getSessionTimeString() + "]"); } @Override @@ -268,40 +268,40 @@ public class EventLogger @Override public void onVideoEnabled(DecoderCounters counters) { - Log.d(TAG, "videoEnabled [" + getSessionTimeString() + "]"); + logd(TAG, "videoEnabled [" + getSessionTimeString() + "]"); } @Override public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { - Log.d(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + logd(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); } @Override public void onVideoInputFormatChanged(Format format) { - Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + logd(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); } @Override public void onVideoDisabled(DecoderCounters counters) { - Log.d(TAG, "videoDisabled [" + getSessionTimeString() + "]"); + logd(TAG, "videoDisabled [" + getSessionTimeString() + "]"); } @Override public void onDroppedFrames(int count, long elapsed) { - Log.d(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + logd(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); } @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - Log.d(TAG, "videoSizeChanged [" + width + ", " + height + "]"); + logd(TAG, "videoSizeChanged [" + width + ", " + height + "]"); } @Override public void onRenderedFirstFrame(Surface surface) { - Log.d(TAG, "renderedFirstFrame [" + surface + "]"); + logd(TAG, "renderedFirstFrame [" + surface + "]"); } // DefaultDrmSessionManager.EventListener @@ -313,17 +313,17 @@ public class EventLogger @Override public void onDrmKeysRestored() { - Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); + logd(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); } @Override public void onDrmKeysRemoved() { - Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); + logd(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); } @Override public void onDrmKeysLoaded() { - Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); + logd(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } // MediaSourceEventListener @@ -400,7 +400,7 @@ public class EventLogger // Internal methods private void printInternalError(String type, Exception e) { - Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); + loge(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); } private void printMetadata(Metadata metadata, String prefix) { @@ -408,37 +408,37 @@ public class EventLogger Metadata.Entry entry = metadata.get(i); if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, + logd(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, textInformationFrame.value)); } else if (entry instanceof UrlLinkFrame) { UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry; - Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); + logd(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); + logd(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", + logd(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", + logd(TAG, prefix + String.format("%s: mimeType=%s, description=%s", apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, + logd(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, commentFrame.language, commentFrame.description)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - Log.d(TAG, prefix + String.format("%s", id3Frame.id)); + logd(TAG, prefix + String.format("%s", id3Frame.id)); } else if (entry instanceof EventMessage) { EventMessage eventMessage = (EventMessage) entry; - Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", + logd(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); } else if (entry instanceof SpliceCommand) { String description = String.format("SCTE-35 splice command: type=%s.", entry.getClass().getSimpleName()); - Log.d(TAG, prefix + description); + logd(TAG, prefix + description); } } } @@ -555,4 +555,11 @@ public class EventLogger } } + protected void logd(String tag, String msg) { + Log.d(tag, msg); + } + + protected void loge(String tag, String msg, Throwable tr) { + Log.d(tag, msg, tr); + } } From 9f2d53dc218f2c841927c025d0e03379b80e1fb9 Mon Sep 17 00:00:00 2001 From: Cory Charlton Date: Fri, 23 Feb 2018 09:15:14 -0800 Subject: [PATCH 063/376] Fixed the Log.d call in loge --- .../java/com/google/android/exoplayer2/util/EventLogger.java | 4 ++-- 1 file changed, 2 insertions(+), 2 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 9af46d88c2..1cac8958c4 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 @@ -556,10 +556,10 @@ public class EventLogger } protected void logd(String tag, String msg) { - Log.d(tag, msg); + Log.d(tag, msg); } protected void loge(String tag, String msg, Throwable tr) { - Log.d(tag, msg, tr); + Log.e(tag, msg, tr); } } From 707b481f2c3fd56e6f8d8a6cc9ac766bde8dc857 Mon Sep 17 00:00:00 2001 From: Aditya Anand <13274079+AdityaAnand1@users.noreply.github.com> Date: Mon, 26 Feb 2018 09:11:02 +0530 Subject: [PATCH 064/376] replace 'compile' with 'implementation' --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7f35329516..8755ac588d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Next add a gradle compile dependency to the `build.gradle` file of your app module. The following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` where `2.X.X` is your preferred version. Alternatively, you can depend on only @@ -51,9 +51,9 @@ dependencies on the Core, DASH and UI library modules, as might be required for an app that plays DASH content: ```gradle -compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' -compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' -compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full @@ -105,9 +105,9 @@ You should now see the ExoPlayer modules appear as part of your project. You can depend on them as you would on any other local module, for example: ```gradle -compile project(':exoplayer-library-core') -compile project(':exoplayer-library-dash') -compile project(':exoplayer-library-ui') +implementation project(':exoplayer-library-core') +implementation project(':exoplayer-library-dash') +implementation project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## From f2f5aac7c5d1ba7aed0ea03994e604f4088114cf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Feb 2018 02:02:32 -0800 Subject: [PATCH 065/376] Allow audio position to jump on first input buffer Allow the position to jump on receiving the first presentable input buffer, if and only if the buffer timestamp differs significantly from what was expected. This prevents a stuck buffering case for streams that are thought to start at t=0, but actually start at t=large_value. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186990214 --- .../audio/MediaCodecAudioRenderer.java | 16 ++++++++++++++++ .../audio/SimpleDecoderAudioRenderer.java | 15 +++++++++++++++ 2 files changed, 31 insertions(+) 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 a7063e5a7f..33a67554a5 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -70,6 +71,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderDelay; private int encoderPadding; private long currentPositionUs; + private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; /** @@ -366,6 +368,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media super.onPositionReset(positionUs, joining); audioSink.reset(); currentPositionUs = positionUs; + allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; } @@ -424,6 +427,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return audioSink.getPlaybackParameters(); } + @Override + protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) { + // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314]. + // Allow the position to jump if the first presentable input buffer has a timestamp that + // differs significantly from what was expected. + if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) { + currentPositionUs = buffer.timeUs; + } + allowFirstBufferPositionDiscontinuity = false; + } + } + @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, 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 2f5e7bcf97..83c33ee6d7 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 @@ -105,6 +105,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private boolean audioTrackNeedsConfigure; private long currentPositionUs; + private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; private boolean inputStreamEnded; private boolean outputStreamEnded; @@ -416,6 +417,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return false; } inputBuffer.flip(); + onQueueInputBuffer(inputBuffer); decoder.queueInputBuffer(inputBuffer); decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; @@ -504,6 +506,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { audioSink.reset(); currentPositionUs = positionUs; + allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; inputStreamEnded = false; outputStreamEnded = false; @@ -654,6 +657,18 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher.inputFormatChanged(newFormat); } + private void onQueueInputBuffer(DecoderInputBuffer buffer) { + if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) { + // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314]. + // Allow the position to jump if the first presentable input buffer has a timestamp that + // differs significantly from what was expected. + if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) { + currentPositionUs = buffer.timeUs; + } + allowFirstBufferPositionDiscontinuity = false; + } + } + private void updateCurrentPosition() { long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { From c4f82514bf4513b16b6545f17cd2aa2b5aed8efb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 26 Feb 2018 02:08:28 -0800 Subject: [PATCH 066/376] Fix ImaAdsMediaSource isTopLevelSource AdsMediaSource must be top-level. Currently the (deprecated) ImaAdsMediaSource can't be used because it prepares its contained AdsMediaSource as a child source. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186990882 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 981e8352e0..1899c815da 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -20,12 +20,12 @@ import android.support.annotation.Nullable; import android.view.ViewGroup; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import java.io.IOException; /** * A {@link MediaSource} that inserts ads linearly with a provided content media source. @@ -33,10 +33,9 @@ import com.google.android.exoplayer2.upstream.DataSource; * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ @Deprecated -public final class ImaAdsMediaSource extends CompositeMediaSource { +public final class ImaAdsMediaSource implements MediaSource { private final AdsMediaSource adsMediaSource; - private Listener listener; /** * Constructs a new source that inserts ads linearly with the content specified by @@ -75,10 +74,23 @@ public final class ImaAdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; - prepareChildSource(/* id= */ null, adsMediaSource); + public void prepareSource( + final ExoPlayer player, boolean isTopLevelSource, final Listener listener) { + adsMediaSource.prepareSource( + player, + isTopLevelSource, + new Listener() { + @Override + public void onSourceInfoRefreshed( + MediaSource source, Timeline timeline, @Nullable Object manifest) { + listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + adsMediaSource.maybeThrowSourceInfoRefreshError(); } @Override @@ -92,8 +104,7 @@ public final class ImaAdsMediaSource extends CompositeMediaSource { } @Override - protected void onChildSourceInfoRefreshed( - Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) { - listener.onSourceInfoRefreshed(this, timeline, manifest); + public void releaseSource() { + adsMediaSource.releaseSource(); } } From 0de6bb28a25d7730ddb47d16332b282becaa5209 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Feb 2018 03:49:15 -0800 Subject: [PATCH 067/376] Fix broken Javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=186999051 --- .../android/exoplayer2/drm/DrmInitData.java | 19 ++++++---------- .../google/android/exoplayer2/util/Clock.java | 2 +- .../exoplayer2/util/HandlerWrapper.java | 22 +++++++++---------- .../google/android/exoplayer2/util/Util.java | 2 +- .../dash/manifest/DashManifestParser.java | 19 +++++++++------- .../source/dash/offline/DashDownloader.java | 13 +++++------ .../smoothstreaming/offline/SsDownloader.java | 2 +- 7 files changed, 38 insertions(+), 41 deletions(-) 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 5662730650..4a59667dc8 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 @@ -39,18 +39,13 @@ public final class DrmInitData implements Comparator, Parcelable { *

The result is generated as follows. * *

    - *
      - * Include all {@link SchemeData}s from {@code manifestData} where {@link - * SchemeData#hasData()} is true. - *
    - *
      - * Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} is - * true and for which we did not include an entry from the manifest targeting the same UUID. - *
    - *
      - * If available, the scheme type from the manifest is used. If not, the scheme type from the - * media is used. - *
    + *
  1. Include all {@link SchemeData}s from {@code manifestData} where {@link + * SchemeData#hasData()} is true. + *
  2. Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} + * is true and for which we did not include an entry from the manifest targeting the same + * UUID. + *
  3. If available, the scheme type from the manifest is used. If not, the scheme type from the + * media is used. *
* * @param manifestData DRM session acquisition data obtained from the manifest. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index dced6752eb..36fc3b1bf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -43,7 +43,7 @@ public interface Clock { * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling * messages. * - * @see Handler#Handler(Looper, Handler.Callback). + * @see Handler#Handler(Looper, Handler.Callback) */ HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); } 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 3ce93f9370..8f1a6544ca 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 @@ -25,36 +25,36 @@ import android.os.Message; */ public interface HandlerWrapper { - /** @see Handler#getLooper(). */ + /** @see Handler#getLooper() */ Looper getLooper(); - /** @see Handler#obtainMessage(int). */ + /** @see Handler#obtainMessage(int) */ Message obtainMessage(int what); - /** @see Handler#obtainMessage(int, Object). */ + /** @see Handler#obtainMessage(int, Object) */ Message obtainMessage(int what, Object obj); - /** @see Handler#obtainMessage(int, int, int). */ + /** @see Handler#obtainMessage(int, int, int) */ Message obtainMessage(int what, int arg1, int arg2); - /** @see Handler#obtainMessage(int, int, int, Object). */ + /** @see Handler#obtainMessage(int, int, int, Object) */ Message obtainMessage(int what, int arg1, int arg2, Object obj); - /** @see Handler#sendEmptyMessage(int). */ + /** @see Handler#sendEmptyMessage(int) */ boolean sendEmptyMessage(int what); - /** @see Handler#sendEmptyMessageAtTime(int, long). */ + /** @see Handler#sendEmptyMessageAtTime(int, long) */ boolean sendEmptyMessageAtTime(int what, long uptimeMs); - /** @see Handler#removeMessages(int). */ + /** @see Handler#removeMessages(int) */ void removeMessages(int what); - /** @see Handler#removeCallbacksAndMessages(Object). */ + /** @see Handler#removeCallbacksAndMessages(Object) */ void removeCallbacksAndMessages(Object token); - /** @see Handler#post(Runnable). */ + /** @see Handler#post(Runnable) */ boolean post(Runnable runnable); - /** @see Handler#postDelayed(Runnable, long). */ + /** @see Handler#postDelayed(Runnable, long) */ boolean postDelayed(Runnable runnable, long delayMs); } 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 cd643f2df4..2761cc3ce6 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 @@ -942,7 +942,7 @@ public final class Util { } /** - * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. + * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. * * @param encoding The encoding of the audio data. * @return Whether the encoding is high resolution integer PCM. 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 1416e9beeb..2d3819c2ac 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 @@ -732,19 +732,23 @@ public class DashManifestParser extends DefaultHandler /** * Parses a single Event node in the manifest. - *

+ * * @param xpp The current xml parser. * @param schemeIdUri The schemeIdUri of the parent EventStream. * @param value The schemeIdUri of the parent EventStream. * @param timescale The timescale of the parent EventStream. - * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize data - * in between and tags into. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used when parsing event + * objects. * @return The {@link EventMessage} parsed from this EventStream node. * @throws XmlPullParserException If there is any error parsing this node. * @throws IOException If there is any error reading from the underlying input stream. */ - protected EventMessage parseEvent(XmlPullParser xpp, String schemeIdUri, String value, - long timescale, ByteArrayOutputStream scratchOutputStream) + protected EventMessage parseEvent( + XmlPullParser xpp, + String schemeIdUri, + String value, + long timescale, + ByteArrayOutputStream scratchOutputStream) throws IOException, XmlPullParserException { long id = parseLong(xpp, "id", 0); long duration = parseLong(xpp, "duration", C.TIME_UNSET); @@ -757,11 +761,10 @@ public class DashManifestParser extends DefaultHandler } /** - * Parses everything between as a byte array string. + * Parses an event object. * * @param xpp The current xml parser. - * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize byte - * array data into. + * @param scratchOutputStream A {@link ByteArrayOutputStream} that's used when parsing the object. * @return The serialized byte array. * @throws XmlPullParserException If there is any error parsing this node. * @throws IOException If there is any error reading from the underlying input stream. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 2b2c37be3d..5c02fccad0 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -39,13 +39,12 @@ import java.util.List; /** * Helper class to download DASH streams. * - *

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and - * {@link #getDownloadedBytes()}, this class isn't thread safe. + *

Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. * *

Example usage: * - *

- * {@code
+ * 
{@code
  * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
  * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
  * DownloaderConstructorHelper constructorHelper =
@@ -54,7 +53,7 @@ import java.util.List;
  * // Select the first representation of the first adaptation set of the first period
  * dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
  * dashDownloader.download(new ProgressListener() {
- *   @Override
+ *   {@literal @}Override
  *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
  *       long downloadedBytes) {
  *     // Invoked periodically during the download.
@@ -62,8 +61,8 @@ import java.util.List;
  * });
  * // Access downloaded data using CacheDataSource
  * CacheDataSource cacheDataSource =
- *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
- * 
+ * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE); + * }
*/ public final class DashDownloader extends SegmentDownloader { diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 12cfe2ee36..7988523bed 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -48,7 +48,7 @@ import java.util.List; * // Select the first track of the first stream element * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)}); * ssDownloader.download(new ProgressListener() { - * @Override + * {@literal @}Override * public void onDownloadProgress(Downloader downloader, float downloadPercentage, * long downloadedBytes) { * // Invoked periodically during the download. From 099cc384a81e3f7a5e8de31da438246abdd6afe3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Feb 2018 06:16:09 -0800 Subject: [PATCH 068/376] Move scheduler into its own top level package The util package is, in practice, for things that are misc enough to not warrant their own package. If something is deserving of a package, it's IMO best placed somewhere else (I know you could argue it's a util, but you could argue that about almost anything else as well). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187010018 --- .../ext/jobdispatcher/JobDispatcherScheduler.java | 4 ++-- .../google/android/exoplayer2/offline/DownloadService.java | 6 +++--- .../exoplayer2/{util => }/scheduler/PlatformScheduler.java | 2 +- .../exoplayer2/{util => }/scheduler/Requirements.java | 2 +- .../{util => }/scheduler/RequirementsWatcher.java | 2 +- .../android/exoplayer2/{util => }/scheduler/Scheduler.java | 2 +- .../source/dash/offline/DownloadServiceDashTest.java | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/{util => }/scheduler/PlatformScheduler.java (99%) rename library/core/src/main/java/com/google/android/exoplayer2/{util => }/scheduler/Requirements.java (99%) rename library/core/src/main/java/com/google/android/exoplayer2/{util => }/scheduler/RequirementsWatcher.java (99%) rename library/core/src/main/java/com/google/android/exoplayer2/{util => }/scheduler/Scheduler.java (95%) 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 908e7f26c7..9232efaaa9 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 @@ -29,9 +29,9 @@ import com.firebase.jobdispatcher.Job.Builder; import com.firebase.jobdispatcher.JobParameters; import com.firebase.jobdispatcher.JobService; import com.firebase.jobdispatcher.Lifetime; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.util.scheduler.Requirements; -import com.google.android.exoplayer2.util.scheduler.Scheduler; /** * A {@link Scheduler} which uses {@link com.firebase.jobdispatcher.FirebaseJobDispatcher} to 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 b6899299c9..0a6bc062f1 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 @@ -30,10 +30,10 @@ import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.RequirementsWatcher; +import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.util.scheduler.Requirements; -import com.google.android.exoplayer2.util.scheduler.RequirementsWatcher; -import com.google.android.exoplayer2.util.scheduler.Scheduler; import java.io.IOException; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java rename to library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java index 2bc0034c9b..cd1e12520e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/PlatformScheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/PlatformScheduler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util.scheduler; +package com.google.android.exoplayer2.scheduler; import android.annotation.TargetApi; import android.app.Notification; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java rename to library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 64b4ccdc31..8c8cf6a3b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util.scheduler; +package com.google.android.exoplayer2.scheduler; import android.content.Context; import android.content.Intent; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java similarity index 99% rename from library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java rename to library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 05a1e7a493..9509c7e5b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util.scheduler; +package com.google.android.exoplayer2.scheduler; import android.annotation.TargetApi; import android.content.BroadcastReceiver; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java similarity index 95% rename from library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java rename to library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java index 395c4c7090..9a9c57443f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/scheduler/Scheduler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Scheduler.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.util.scheduler; +package com.google.android.exoplayer2.scheduler; /** * Implementer of this interface schedules one implementation specific job to be run when some diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 13c5a5c42f..2a6d127aa0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -26,6 +26,8 @@ import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -36,8 +38,6 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.util.scheduler.Requirements; -import com.google.android.exoplayer2.util.scheduler.Scheduler; import java.io.File; import java.io.IOException; From d400efd0f7da2350498af96f4e8db237672166a2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Feb 2018 07:56:13 -0800 Subject: [PATCH 069/376] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187021535 --- ISSUE_TEMPLATE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index e85c0c28c7..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,3 +1,5 @@ +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** + Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From b47fb2826be2230bd81e5102d93cb5717f49ffc9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 26 Feb 2018 07:58:29 -0800 Subject: [PATCH 070/376] Move extension tests to Robolectric. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187021822 --- core_settings.gradle | 2 + extensions/cast/build.gradle | 5 +- extensions/cast/src/test/AndroidManifest.xml | 23 + .../ext/cast/CastTimelineTrackerTest.java | 28 +- .../src/test/resources/robolectric.properties | 1 + extensions/cronet/build.gradle | 8 +- .../{androidTest => test}/AndroidManifest.xml | 12 +- .../ByteArrayUploadDataProviderTest.java | 14 +- .../ext/cronet/CronetDataSourceTest.java | 524 ++++---- .../src/test/resources/robolectric.properties | 1 + library/core/build.gradle | 1 + .../upstream/ContentDataSourceTest.java | 2 +- .../android/exoplayer2/ExoPlayerTest.java | 1 + .../drm/OfflineLicenseHelperTest.java | 2 +- .../offline/DownloadManagerTest.java | 2 +- .../source/ClippingMediaSourceTest.java | 2 +- .../source/ConcatenatingMediaSourceTest.java | 2 +- .../DynamicConcatenatingMediaSourceTest.java | 2 +- .../source/LoopingMediaSourceTest.java | 2 +- .../source/MergingMediaSourceTest.java | 2 +- library/dash/build.gradle | 10 +- .../dash/offline/DashDownloadTestData.java | 100 -- .../source/dash/DashMediaSource.java | 5 +- .../{androidTest => test}/AndroidManifest.xml | 12 +- .../{androidTest => test}/assets/sample_mpd_1 | 0 .../assets/sample_mpd_2_unknown_mime_type | 0 .../assets/sample_mpd_3_segment_template | 0 .../assets/sample_mpd_4_event_stream | 0 .../source/dash/DashMediaSourceTest.java | 76 ++ .../exoplayer2/source/dash/DashUtilTest.java | 38 +- .../source/dash/EventSampleStreamTest.java | 356 ++++++ .../dash/manifest/DashManifestParserTest.java | 59 +- .../dash/manifest/DashManifestTest.java | 193 +-- .../source/dash/manifest/RangedUriTest.java | 16 +- .../dash/manifest/RepresentationTest.java | 50 +- .../source/dash/manifest/UrlTemplateTest.java | 17 +- .../dash/offline/DashDownloadActionTest.java | 162 +++ .../dash/offline/DashDownloadTestData.java | 103 ++ .../dash/offline/DashDownloaderTest.java | 283 +++-- .../dash/offline/DownloadManagerDashTest.java | 129 +- .../dash/offline/DownloadServiceDashTest.java | 82 +- .../dash/offline/TestDownloadListener.java | 20 +- .../src/test/resources/robolectric.properties | 1 + library/hls/build.gradle | 5 +- .../hls/offline/HlsDownloadTestData.java | 77 -- .../{androidTest => test}/AndroidManifest.xml | 12 +- .../hls/offline/HlsDownloadTestData.java | 83 ++ .../source/hls/offline/HlsDownloaderTest.java | 95 +- .../playlist/HlsMasterPlaylistParserTest.java | 138 ++- .../playlist/HlsMediaPlaylistParserTest.java | 86 +- .../src/test/resources/robolectric.properties | 1 + library/smoothstreaming/build.gradle | 5 +- .../{androidTest => test}/AndroidManifest.xml | 12 +- .../assets/sample_ismc_1 | 0 .../assets/sample_ismc_2 | 0 .../manifest/SsManifestParserTest.java | 28 +- .../manifest/SsManifestTest.java | 63 +- .../src/test/resources/robolectric.properties | 1 + testutils/build.gradle | 1 + .../exoplayer2/testutil/MockitoUtil.java | 54 - .../exoplayer2/testutil/OggTestData.java | 1073 ----------------- .../android/exoplayer2/testutil/TestUtil.java | 11 - testutils/src/test/AndroidManifest.xml | 23 + .../testutil/FakeAdaptiveDataSetTest.java | 147 +++ .../exoplayer2/testutil/FakeClockTest.java | 201 +++ .../exoplayer2/testutil/FakeDataSetTest.java | 119 ++ .../testutil/FakeDataSourceTest.java | 251 ++++ .../src/test/resources/robolectric.properties | 1 + testutils_robolectric/build.gradle | 37 + .../src/main/AndroidManifest.xml | 17 + .../exoplayer2/testutil/CacheAsserts.java | 5 +- .../exoplayer2/testutil/DummyMainThread.java | 25 +- .../testutil/FakeMediaClockRenderer.java | 5 +- .../exoplayer2/testutil/FakeShuffleOrder.java | 0 .../testutil/FakeTrackSelection.java | 9 +- .../testutil/FakeTrackSelector.java | 21 +- .../testutil/MediaSourceTestRunner.java | 107 +- .../exoplayer2/testutil/OggTestData.java | 1070 ++++++++++++++++ .../exoplayer2/testutil}/RobolectricUtil.java | 2 +- .../exoplayer2/testutil/StubExoPlayer.java | 1 - .../exoplayer2/testutil/TimelineAsserts.java | 44 +- 81 files changed, 3851 insertions(+), 2327 deletions(-) create mode 100644 extensions/cast/src/test/AndroidManifest.xml create mode 100644 extensions/cast/src/test/resources/robolectric.properties rename extensions/cronet/src/{androidTest => test}/AndroidManifest.xml (71%) rename extensions/cronet/src/{androidTest => test}/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java (90%) rename extensions/cronet/src/{androidTest => test}/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java (71%) create mode 100644 extensions/cronet/src/test/resources/robolectric.properties delete mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java rename library/dash/src/{androidTest => test}/AndroidManifest.xml (71%) rename library/dash/src/{androidTest => test}/assets/sample_mpd_1 (100%) rename library/dash/src/{androidTest => test}/assets/sample_mpd_2_unknown_mime_type (100%) rename library/dash/src/{androidTest => test}/assets/sample_mpd_3_segment_template (100%) rename library/dash/src/{androidTest => test}/assets/sample_mpd_4_event_stream (100%) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java (81%) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java (85%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java (54%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java (92%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java (54%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java (89%) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java (64%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java (74%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java (80%) rename library/dash/src/{androidTest => test}/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java (80%) create mode 100644 library/dash/src/test/resources/robolectric.properties delete mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java rename library/hls/src/{androidTest => test}/AndroidManifest.xml (71%) create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java rename library/hls/src/{androidTest => test}/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java (77%) rename library/hls/src/{androidTest => test}/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java (59%) rename library/hls/src/{androidTest => test}/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java (76%) create mode 100644 library/hls/src/test/resources/robolectric.properties rename library/smoothstreaming/src/{androidTest => test}/AndroidManifest.xml (70%) rename library/smoothstreaming/src/{androidTest => test}/assets/sample_ismc_1 (100%) rename library/smoothstreaming/src/{androidTest => test}/assets/sample_ismc_2 (100%) rename library/smoothstreaming/src/{androidTest => test}/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java (60%) rename library/smoothstreaming/src/{androidTest => test}/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java (77%) create mode 100644 library/smoothstreaming/src/test/resources/robolectric.properties delete mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java delete mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java create mode 100644 testutils/src/test/AndroidManifest.xml create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java create mode 100644 testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSourceTest.java create mode 100644 testutils/src/test/resources/robolectric.properties create mode 100644 testutils_robolectric/build.gradle create mode 100644 testutils_robolectric/src/main/AndroidManifest.xml rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java (99%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java (81%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java (93%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java (100%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java (94%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java (83%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java (85%) create mode 100644 testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java rename {library/core/src/test/java/com/google/android/exoplayer2 => testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil}/RobolectricUtil.java (99%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java (99%) rename {testutils => testutils_robolectric}/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java (84%) diff --git a/core_settings.gradle b/core_settings.gradle index 5687f19396..fc738c8476 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -24,6 +24,7 @@ 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' @@ -44,6 +45,7 @@ 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 d11f166c89..2f79c7a0ee 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -38,10 +38,7 @@ dependencies { compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') - testCompile project(modulePrefix + 'testutils') - testCompile 'junit:junit:' + junitVersion - testCompile 'org.mockito:mockito-core:' + mockitoVersion - testCompile 'org.robolectric:robolectric:' + robolectricVersion + testCompile project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/extensions/cast/src/test/AndroidManifest.xml b/extensions/cast/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..057efdc245 --- /dev/null +++ b/extensions/cast/src/test/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java index bf4b20e156..4c60e7c0b3 100644 --- a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java +++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.cast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; @@ -25,11 +26,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; /** Tests for {@link CastTimelineTracker}. */ @RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class CastTimelineTrackerTest { private static final long DURATION_1_MS = 1000; @@ -49,12 +48,12 @@ public class CastTimelineTrackerTest { new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION}); CastTimelineTracker tracker = new CastTimelineTracker(); - mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS); + mediaInfo = getMediaInfo("contentId1", DURATION_1_MS); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET); - mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS); + mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(status), @@ -62,7 +61,7 @@ public class CastTimelineTrackerTest { C.TIME_UNSET, C.msToUs(DURATION_3_MS)); - mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS); + mediaInfo = getMediaInfo("contentId2", DURATION_2_MS); Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(status), @@ -80,7 +79,7 @@ public class CastTimelineTrackerTest { DURATION_5_MS, MediaInfo.UNKNOWN_DURATION }); - mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS); + mediaInfo = getMediaInfo("contentId5", DURATION_5_MS); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(newStatus), @@ -89,7 +88,7 @@ public class CastTimelineTrackerTest { C.msToUs(DURATION_5_MS), C.msToUs(DURATION_3_MS)); - mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS); + mediaInfo = getMediaInfo("contentId3", DURATION_3_MS); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(newStatus), @@ -98,7 +97,7 @@ public class CastTimelineTrackerTest { C.msToUs(DURATION_5_MS), C.msToUs(DURATION_3_MS)); - mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS); + mediaInfo = getMediaInfo("contentId4", DURATION_4_MS); Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo); TimelineAsserts.assertPeriodDurations( tracker.getCastTimeline(newStatus), @@ -112,7 +111,7 @@ public class CastTimelineTrackerTest { int[] itemIds, String[] contentIds, long[] durationsMs) { ArrayList items = new ArrayList<>(); for (int i = 0; i < contentIds.length; i++) { - MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]); + MediaInfo mediaInfo = getMediaInfo(contentIds[i], durationsMs[i]); MediaQueueItem item = Mockito.mock(MediaQueueItem.class); Mockito.when(item.getMedia()).thenReturn(mediaInfo); Mockito.when(item.getItemId()).thenReturn(itemIds[i]); @@ -123,10 +122,11 @@ public class CastTimelineTrackerTest { return status; } - private static MediaInfo mockMediaInfo(String contentId, long durationMs) { - MediaInfo mediaInfo = Mockito.mock(MediaInfo.class); - Mockito.when(mediaInfo.getContentId()).thenReturn(contentId); - Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs); - return mediaInfo; + private static MediaInfo getMediaInfo(String contentId, long durationMs) { + return new MediaInfo.Builder(contentId) + .setStreamDuration(durationMs) + .setContentType(MimeTypes.APPLICATION_MP4) + .setStreamType(MediaInfo.STREAM_TYPE_NONE) + .build(); } } diff --git a/extensions/cast/src/test/resources/robolectric.properties b/extensions/cast/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/cast/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 0b6f9a587c..2d25c7299c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -39,12 +39,8 @@ dependencies { compile files('libs/cronet_api.jar') compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') - androidTestCompile project(modulePrefix + 'library') - androidTestCompile project(modulePrefix + 'testutils') - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion - androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion + testCompile project(modulePrefix + 'library') + testCompile project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/test/AndroidManifest.xml similarity index 71% rename from extensions/cronet/src/androidTest/AndroidManifest.xml rename to extensions/cronet/src/test/AndroidManifest.xml index 453cc68478..52be9aa157 100644 --- a/extensions/cronet/src/androidTest/AndroidManifest.xml +++ b/extensions/cronet/src/test/AndroidManifest.xml @@ -18,16 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.ext.cronet"> - - - - - - - +
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java similarity index 90% rename from extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java rename to extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 28d22b91a5..291e73fcc1 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -19,9 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import com.google.android.exoplayer2.testutil.MockitoUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -30,11 +27,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; -/** - * Tests for {@link ByteArrayUploadDataProvider}. - */ -@RunWith(AndroidJUnit4.class) +/** Tests for {@link ByteArrayUploadDataProvider}. */ +@RunWith(RobolectricTestRunner.class) public final class ByteArrayUploadDataProviderTest { private static final byte[] TEST_DATA = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; @@ -45,7 +42,7 @@ public final class ByteArrayUploadDataProviderTest { @Before public void setUp() { - MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); + MockitoAnnotations.initMocks(this); byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); } @@ -90,5 +87,4 @@ public final class ByteArrayUploadDataProviderTest { assertThat(byteBuffer.array()).isEqualTo(TEST_DATA); verify(mockUploadDataSink).onRewindSucceeded(); } - } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java similarity index 71% rename from extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java rename to extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 79be44398e..4e990cd027 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/test/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -31,10 +31,8 @@ import static org.mockito.Mockito.when; import android.net.Uri; import android.os.ConditionVariable; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import android.os.SystemClock; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -50,6 +48,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; import org.chromium.net.CronetEngine; @@ -61,13 +60,14 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowSystemClock; -/** - * Tests for {@link CronetDataSource}. - */ -@RunWith(AndroidJUnit4.class) +/** Tests for {@link CronetDataSource}. */ +@RunWith(RobolectricTestRunner.class) public final class CronetDataSourceTest { private static final int TEST_CONNECT_TIMEOUT_MS = 100; @@ -85,18 +85,11 @@ public final class CronetDataSourceTest { private UrlResponseInfo testUrlResponseInfo; @Mock private UrlRequest.Builder mockUrlRequestBuilder; - @Mock - private UrlRequest mockUrlRequest; - @Mock - private Predicate mockContentTypePredicate; - @Mock - private TransferListener mockTransferListener; - @Mock - private Clock mockClock; - @Mock - private Executor mockExecutor; - @Mock - private NetworkException mockNetworkException; + @Mock private UrlRequest mockUrlRequest; + @Mock private Predicate mockContentTypePredicate; + @Mock private TransferListener mockTransferListener; + @Mock private Executor mockExecutor; + @Mock private NetworkException mockNetworkException; @Mock private CronetEngine mockCronetEngine; private CronetDataSource dataSourceUnderTest; @@ -104,30 +97,31 @@ public final class CronetDataSourceTest { @Before public void setUp() throws Exception { - MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); - dataSourceUnderTest = spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - mockClock, - null, - false)); + MockitoAnnotations.initMocks(this); + dataSourceUnderTest = + spy( + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + false)); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( - anyString(), any(UrlRequest.Callback.class), any(Executor.class))) + anyString(), any(UrlRequest.Callback.class), any(Executor.class))) .thenReturn(mockUrlRequestBuilder); when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest); mockStatusResponse(); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); - testPostDataSpec = new DataSpec( - Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0); + testPostDataSpec = + new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0); testResponseHeader = new HashMap<>(); testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE); // This value can be anything since the DataSpec is unset. @@ -173,20 +167,19 @@ public final class CronetDataSourceTest { // Prepare a mock UrlRequest to be used in the second open() call. final UrlRequest mockUrlRequest2 = mock(UrlRequest.class); when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - // Invoke the callback for the previous request. - dataSourceUnderTest.onFailed( - mockUrlRequest, - testUrlResponseInfo, - mockNetworkException); - dataSourceUnderTest.onResponseStarted( - mockUrlRequest2, - testUrlResponseInfo); - return null; - } - }).when(mockUrlRequest2).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + // Invoke the callback for the previous request. + dataSourceUnderTest.onFailed( + mockUrlRequest, testUrlResponseInfo, mockNetworkException); + dataSourceUnderTest.onResponseStarted(mockUrlRequest2, testUrlResponseInfo); + return null; + } + }) + .when(mockUrlRequest2) + .start(); dataSourceUnderTest.open(testDataSpec); } @@ -253,8 +246,8 @@ public final class CronetDataSourceTest { @Test public void testRequestOpenFailDueToDnsFailure() { mockResponseStartFailure(); - when(mockNetworkException.getErrorCode()).thenReturn( - NetworkException.ERROR_HOSTNAME_NOT_RESOLVED); + when(mockNetworkException.getErrorCode()) + .thenReturn(NetworkException.ERROR_HOSTNAME_NOT_RESOLVED); try { dataSourceUnderTest.open(testDataSpec); @@ -524,8 +517,8 @@ public final class CronetDataSourceTest { assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT); assertThat(returnedBuffer).isEqualTo(new byte[16]); // C.RESULT_END_OF_INPUT should not be reported though the TransferListener. - verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest, - C.RESULT_END_OF_INPUT); + verify(mockTransferListener, never()) + .onBytesTransferred(dataSourceUnderTest, C.RESULT_END_OF_INPUT); // There should still be only one call to read on cronet. verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class)); // Check for connection not automatically closed. @@ -534,10 +527,10 @@ public final class CronetDataSourceTest { } @Test - public void testConnectTimeout() { - when(mockClock.elapsedRealtime()).thenReturn(0L); + public void testConnectTimeout() throws InterruptedException { + long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); - final ConditionVariable timedOutCondition = new ConditionVariable(); + final CountDownLatch timedOutLatch = new CountDownLatch(1); new Thread() { @Override @@ -551,29 +544,29 @@ public final class CronetDataSourceTest { assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_CONNECTION_STATUS); - timedOutCondition.open(); + timedOutLatch.countDown(); } } }.start(); startCondition.block(); // We should still be trying to open. - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); - assertThat(timedOutCondition.block(50)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + assertNotCountedDown(timedOutLatch); // Now we timeout. - when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS); - timedOutCondition.block(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10); + timedOutLatch.await(); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); } @Test - public void testConnectInterrupted() { - when(mockClock.elapsedRealtime()).thenReturn(0L); + public void testConnectInterrupted() throws InterruptedException { + long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); - final ConditionVariable timedOutCondition = new ConditionVariable(); + final CountDownLatch timedOutLatch = new CountDownLatch(1); Thread thread = new Thread() { @@ -588,7 +581,7 @@ public final class CronetDataSourceTest { assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus) .isEqualTo(TEST_INVALID_CONNECTION_STATUS); - timedOutCondition.open(); + timedOutLatch.countDown(); } } }; @@ -596,29 +589,29 @@ public final class CronetDataSourceTest { startCondition.block(); // We should still be trying to open. - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); - assertThat(timedOutCondition.block(50)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + assertNotCountedDown(timedOutLatch); // Now we interrupt. thread.interrupt(); - timedOutCondition.block(); + timedOutLatch.await(); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); } @Test - public void testConnectResponseBeforeTimeout() { - when(mockClock.elapsedRealtime()).thenReturn(0L); + public void testConnectResponseBeforeTimeout() throws InterruptedException { + long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); - final ConditionVariable openCondition = new ConditionVariable(); + final CountDownLatch openLatch = new CountDownLatch(1); new Thread() { @Override public void run() { try { dataSourceUnderTest.open(testDataSpec); - openCondition.open(); + openLatch.countDown(); } catch (HttpDataSourceException e) { fail(); } @@ -627,20 +620,20 @@ public final class CronetDataSourceTest { startCondition.block(); // We should still be trying to open. - assertThat(openCondition.block(50)).isFalse(); + assertNotCountedDown(openLatch); // We should still be trying to open as we approach the timeout. - when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); - assertThat(openCondition.block(50)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + assertNotCountedDown(openLatch); // The response arrives just in time. dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); - openCondition.block(); + openLatch.await(); } @Test public void testRedirectIncreasesConnectionTimeout() throws InterruptedException { - when(mockClock.elapsedRealtime()).thenReturn(0L); + long startTimeMs = SystemClock.elapsedRealtime(); final ConditionVariable startCondition = buildUrlRequestStartedCondition(); - final ConditionVariable timedOutCondition = new ConditionVariable(); + final CountDownLatch timedOutLatch = new CountDownLatch(1); final AtomicInteger openExceptions = new AtomicInteger(0); new Thread() { @@ -654,40 +647,36 @@ public final class CronetDataSourceTest { assertThat(e instanceof CronetDataSource.OpenException).isTrue(); assertThat(e.getCause() instanceof SocketTimeoutException).isTrue(); openExceptions.getAndIncrement(); - timedOutCondition.open(); + timedOutLatch.countDown(); } } }.start(); startCondition.block(); // We should still be trying to open. - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // We should still be trying to open as we approach the timeout. - when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1); - assertThat(timedOutCondition.block(50)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1); + assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, - "RandomRedirectedUrl1"); + dataSourceUnderTest.onRedirectReceived( + mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1"); long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1; - when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1); - // Give the thread some time to run. - assertThat(timedOutCondition.block(newTimeoutMs)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // A redirect arrives just in time. - dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo, - "RandomRedirectedUrl2"); + dataSourceUnderTest.onRedirectReceived( + mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2"); newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2; - when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1); - // Give the thread some time to run. - assertThat(timedOutCondition.block(newTimeoutMs)).isFalse(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs - 1); // We should still be trying to open as we approach the new timeout. - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // Now we timeout. - when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs); - timedOutCondition.block(); + ShadowSystemClock.setCurrentTimeMillis(startTimeMs + newTimeoutMs + 10); + timedOutLatch.await(); verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec); assertThat(openExceptions.get()).isEqualTo(1); @@ -707,20 +696,22 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() - throws HttpDataSourceException { - dataSourceUnderTest = spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - mockClock, - null, - true)); + public void + testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders() + throws HttpDataSourceException { + dataSourceUnderTest = + spy( + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true)); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -736,21 +727,23 @@ public final class CronetDataSourceTest { } @Test - public void testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader() - throws HttpDataSourceException { + public void + testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader() + throws HttpDataSourceException { testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000, null); - dataSourceUnderTest = spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - mockClock, - null, - true)); + dataSourceUnderTest = + spy( + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true)); dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE); mockSingleRedirectSuccess(); @@ -778,18 +771,19 @@ public final class CronetDataSourceTest { @Test public void testRedirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie() throws HttpDataSourceException { - dataSourceUnderTest = spy( - new CronetDataSource( - mockCronetEngine, - mockExecutor, - mockContentTypePredicate, - mockTransferListener, - TEST_CONNECT_TIMEOUT_MS, - TEST_READ_TIMEOUT_MS, - true, // resetTimeoutOnRedirects - mockClock, - null, - true)); + dataSourceUnderTest = + spy( + new CronetDataSource( + mockCronetEngine, + mockExecutor, + mockContentTypePredicate, + mockTransferListener, + TEST_CONNECT_TIMEOUT_MS, + TEST_READ_TIMEOUT_MS, + true, // resetTimeoutOnRedirects + Clock.DEFAULT, + null, + true)); mockSingleRedirectSuccess(); mockFollowRedirectSuccess(); @@ -804,8 +798,9 @@ public final class CronetDataSourceTest { // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that // the subsequent open() call succeeds. - doThrow(new NullPointerException()).when(mockTransferListener).onTransferEnd( - dataSourceUnderTest); + doThrow(new NullPointerException()) + .when(mockTransferListener) + .onTransferEnd(dataSourceUnderTest); dataSourceUnderTest.open(testDataSpec); try { dataSourceUnderTest.close(); @@ -833,13 +828,12 @@ public final class CronetDataSourceTest { } @Test - public void testReadInterrupted() throws HttpDataSourceException { - when(mockClock.elapsedRealtime()).thenReturn(0L); + public void testReadInterrupted() throws HttpDataSourceException, InterruptedException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); final ConditionVariable startCondition = buildReadStartedCondition(); - final ConditionVariable timedOutCondition = new ConditionVariable(); + final CountDownLatch timedOutLatch = new CountDownLatch(1); byte[] returnedBuffer = new byte[8]; Thread thread = new Thread() { @@ -851,17 +845,17 @@ public final class CronetDataSourceTest { } catch (HttpDataSourceException e) { // Expected. assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue(); - timedOutCondition.open(); + timedOutLatch.countDown(); } } }; thread.start(); startCondition.block(); - assertThat(timedOutCondition.block(50)).isFalse(); + assertNotCountedDown(timedOutLatch); // Now we interrupt. thread.interrupt(); - timedOutCondition.block(); + timedOutLatch.await(); } @Test @@ -876,122 +870,135 @@ public final class CronetDataSourceTest { // Helper methods. private void mockStatusResponse() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - UrlRequest.StatusListener statusListener = - (UrlRequest.StatusListener) invocation.getArguments()[0]; - statusListener.onStatus(TEST_CONNECTION_STATUS); - return null; - } - }).when(mockUrlRequest).getStatus(any(UrlRequest.StatusListener.class)); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + UrlRequest.StatusListener statusListener = + (UrlRequest.StatusListener) invocation.getArguments()[0]; + statusListener.onStatus(TEST_CONNECTION_STATUS); + return null; + } + }) + .when(mockUrlRequest) + .getStatus(any(UrlRequest.StatusListener.class)); } private void mockResponseStartSuccess() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted( - mockUrlRequest, - testUrlResponseInfo); - return null; - } - }).when(mockUrlRequest).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + return null; + } + }) + .when(mockUrlRequest) + .start(); } private void mockResponseStartRedirect() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onRedirectReceived( - mockUrlRequest, - createUrlResponseInfo(307), // statusCode - "http://redirect.location.com"); - return null; - } - }).when(mockUrlRequest).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onRedirectReceived( + mockUrlRequest, + createUrlResponseInfo(307), // statusCode + "http://redirect.location.com"); + return null; + } + }) + .when(mockUrlRequest) + .start(); } private void mockSingleRedirectSuccess() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - if (!redirectCalled) { - redirectCalled = true; - dataSourceUnderTest.onRedirectReceived( - mockUrlRequest, - createUrlResponseInfoWithUrl("http://example.com/video", 300), - "http://example.com/video/redirect"); - } else { - dataSourceUnderTest.onResponseStarted( - mockUrlRequest, - testUrlResponseInfo); - } - return null; - } - }).when(mockUrlRequest).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + if (!redirectCalled) { + redirectCalled = true; + dataSourceUnderTest.onRedirectReceived( + mockUrlRequest, + createUrlResponseInfoWithUrl("http://example.com/video", 300), + "http://example.com/video/redirect"); + } else { + dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + } + return null; + } + }) + .when(mockUrlRequest) + .start(); } private void mockFollowRedirectSuccess() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onResponseStarted( - mockUrlRequest, - testUrlResponseInfo); - return null; - } - }).when(mockUrlRequest).followRedirect(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo); + return null; + } + }) + .when(mockUrlRequest) + .followRedirect(); } private void mockResponseStartFailure() { - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( - mockUrlRequest, - createUrlResponseInfo(500), // statusCode - mockNetworkException); - return null; - } - }).when(mockUrlRequest).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onFailed( + mockUrlRequest, + createUrlResponseInfo(500), // statusCode + mockNetworkException); + return null; + } + }) + .when(mockUrlRequest) + .start(); } private void mockReadSuccess(int position, int length) { final int[] positionAndRemaining = new int[] {position, length}; - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - if (positionAndRemaining[1] == 0) { - dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); - } else { - ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; - int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); - inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); - positionAndRemaining[0] += readLength; - positionAndRemaining[1] -= readLength; - dataSourceUnderTest.onReadCompleted( - mockUrlRequest, - testUrlResponseInfo, - inputBuffer); - } - return null; - } - }).when(mockUrlRequest).read(any(ByteBuffer.class)); + doAnswer( + new Answer() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + if (positionAndRemaining[1] == 0) { + dataSourceUnderTest.onSucceeded(mockUrlRequest, testUrlResponseInfo); + } else { + ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0]; + int readLength = Math.min(positionAndRemaining[1], inputBuffer.remaining()); + inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength)); + positionAndRemaining[0] += readLength; + positionAndRemaining[1] -= readLength; + dataSourceUnderTest.onReadCompleted( + mockUrlRequest, testUrlResponseInfo, inputBuffer); + } + return null; + } + }) + .when(mockUrlRequest) + .read(any(ByteBuffer.class)); } private void mockReadFailure() { doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - dataSourceUnderTest.onFailed( - mockUrlRequest, - createUrlResponseInfo(500), // statusCode - mockNetworkException); - return null; - } - }) + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onFailed( + mockUrlRequest, + createUrlResponseInfo(500), // statusCode + mockNetworkException); + return null; + } + }) .when(mockUrlRequest) .read(any(ByteBuffer.class)); } @@ -999,13 +1006,13 @@ public final class CronetDataSourceTest { private ConditionVariable buildReadStartedCondition() { final ConditionVariable startedCondition = new ConditionVariable(); doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - startedCondition.open(); - return null; - } - }) + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + startedCondition.open(); + return null; + } + }) .when(mockUrlRequest) .read(any(ByteBuffer.class)); return startedCondition; @@ -1013,16 +1020,26 @@ public final class CronetDataSourceTest { private ConditionVariable buildUrlRequestStartedCondition() { final ConditionVariable startedCondition = new ConditionVariable(); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - startedCondition.open(); - return null; - } - }).when(mockUrlRequest).start(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + startedCondition.open(); + return null; + } + }) + .when(mockUrlRequest) + .start(); return startedCondition; } + private void assertNotCountedDown(CountDownLatch countDownLatch) throws InterruptedException { + // We are asserting that another thread does not count down the latch. We therefore sleep some + // time to give the other thread the chance to fail this test. + Thread.sleep(50); + assertThat(countDownLatch.getCount()).isGreaterThan(0L); + } + private static byte[] buildTestDataArray(int position, int length) { return buildTestDataBuffer(position, length).array(); } @@ -1045,5 +1062,4 @@ public final class CronetDataSourceTest { testBuffer.flip(); return testBuffer; } - } diff --git a/extensions/cronet/src/test/resources/robolectric.properties b/extensions/cronet/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/extensions/cronet/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/library/core/build.gradle b/library/core/build.gradle index a87e11065f..3d655ba543 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -31,6 +31,7 @@ android { } test { java.srcDirs += "../../testutils/src/main/java/" + java.srcDirs += "../../testutils_robolectric/src/main/java/" } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 83a978219e..3465393853 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -89,7 +89,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ContentDataSource dataSource = new ContentDataSource(instrumentation.getContext()); try { DataSpec dataSpec = new DataSpec(contentUri, offset, length, null); - byte[] completeData = TestUtil.getByteArray(instrumentation, DATA_PATH); + byte[] completeData = TestUtil.getByteArray(instrumentation.getContext(), DATA_PATH); byte[] expectedData = Arrays.copyOfRange(completeData, offset, length == C.LENGTH_UNSET ? completeData.length : offset + length); TestUtil.assertDataSourceContent(dataSource, dataSpec, expectedData, !pipeMode); 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 3d0cde5df8..fd7577b5ec 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 @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; import java.util.Arrays; 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 f67301f017..6f62b7fcfc 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 @@ -22,8 +22,8 @@ import static org.mockito.Mockito.when; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import java.util.HashMap; import org.junit.After; import org.junit.Before; 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 9a1efc7df6..12369e6e28 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 @@ -21,11 +21,11 @@ import static org.junit.Assert.fail; import android.os.ConditionVariable; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState.State; import com.google.android.exoplayer2.testutil.DummyMainThread; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.Util; 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 1e0d8681c5..a4aa3eb938 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 @@ -20,7 +20,6 @@ import static org.junit.Assert.fail; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -29,6 +28,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import org.junit.Before; 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 ccc3ddea46..465e08b5d2 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 @@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; @@ -27,6 +26,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import org.junit.Test; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index d71b02d39a..4e4628acdf 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -22,7 +22,6 @@ import static org.mockito.Mockito.verify; import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.DummyMainThread; @@ -31,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import java.util.Arrays; 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 f0b4772422..6aa710aff4 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 @@ -17,12 +17,12 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; import org.junit.Before; 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 b03a76c23e..839492f196 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 @@ -19,13 +19,13 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.RobolectricUtil; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 99441a2849..6cf6f64443 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -35,15 +35,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile project(modulePrefix + 'testutils') - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion - testCompile project(modulePrefix + 'testutils') - testCompile 'com.google.truth:truth:' + truthVersion - testCompile 'junit:junit:' + junitVersion - testCompile 'org.mockito:mockito-core:' + mockitoVersion - testCompile 'org.robolectric:robolectric:' + robolectricVersion + testCompile project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java deleted file mode 100644 index 50752c8a72..0000000000 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java +++ /dev/null @@ -1,100 +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.dash.offline; - -import android.net.Uri; - -/** - * Data for DASH downloading tests. - */ -/* package */ interface DashDownloadTestData { - - Uri TEST_MPD_URI = Uri.parse("test.mpd"); - - byte[] TEST_MPD = - ("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - // Bounded range data - + " \n" - // Unbounded range data - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - // This segment list has a 1 second offset to make sure the progressive download order - + " \n" - + " \n" - + " \n" // 1s offset - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + "").getBytes(); - - byte[] TEST_MPD_NO_INDEX = - ("\n" - + "\n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + " \n" - + "").getBytes(); -} 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 eb9b18512c..98783ac93e 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 @@ -50,6 +50,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.nio.charset.Charset; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; @@ -1113,7 +1114,9 @@ public final class DashMediaSource implements MediaSource { @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { - String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); + String firstLine = + new BufferedReader(new InputStreamReader(inputStream, Charset.forName(C.UTF8_NAME))) + .readLine(); try { Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); if (!matcher.matches()) { diff --git a/library/dash/src/androidTest/AndroidManifest.xml b/library/dash/src/test/AndroidManifest.xml similarity index 71% rename from library/dash/src/androidTest/AndroidManifest.xml rename to library/dash/src/test/AndroidManifest.xml index 39596a8165..eecf596b92 100644 --- a/library/dash/src/androidTest/AndroidManifest.xml +++ b/library/dash/src/test/AndroidManifest.xml @@ -18,16 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.source.dash.test"> - - - - - - - + diff --git a/library/dash/src/androidTest/assets/sample_mpd_1 b/library/dash/src/test/assets/sample_mpd_1 similarity index 100% rename from library/dash/src/androidTest/assets/sample_mpd_1 rename to library/dash/src/test/assets/sample_mpd_1 diff --git a/library/dash/src/androidTest/assets/sample_mpd_2_unknown_mime_type b/library/dash/src/test/assets/sample_mpd_2_unknown_mime_type similarity index 100% rename from library/dash/src/androidTest/assets/sample_mpd_2_unknown_mime_type rename to library/dash/src/test/assets/sample_mpd_2_unknown_mime_type diff --git a/library/dash/src/androidTest/assets/sample_mpd_3_segment_template b/library/dash/src/test/assets/sample_mpd_3_segment_template similarity index 100% rename from library/dash/src/androidTest/assets/sample_mpd_3_segment_template rename to library/dash/src/test/assets/sample_mpd_3_segment_template diff --git a/library/dash/src/androidTest/assets/sample_mpd_4_event_stream b/library/dash/src/test/assets/sample_mpd_4_event_stream similarity index 100% rename from library/dash/src/androidTest/assets/sample_mpd_4_event_stream rename to library/dash/src/test/assets/sample_mpd_4_event_stream diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java new file mode 100644 index 0000000000..1c440c70be --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -0,0 +1,76 @@ +/* + * 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.dash; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.Util; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link DashMediaSource}. */ +@RunWith(RobolectricTestRunner.class) +public final class DashMediaSourceTest { + + @Test + public void testIso8601ParserParse() throws IOException { + DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser(); + // UTC. + assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37Z"); + assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+00:00"); + assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+0000"); + assertParseStringToLong(1512381697000L, parser, "2017-12-04T10:01:37+00"); + // Positive timezone offsets. + assertParseStringToLong(1512381697000L - 4980000L, parser, "2017-12-04T10:01:37+01:23"); + assertParseStringToLong(1512381697000L - 4980000L, parser, "2017-12-04T10:01:37+0123"); + assertParseStringToLong(1512381697000L - 3600000L, parser, "2017-12-04T10:01:37+01"); + // Negative timezone offsets with minus character. + assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37-01:23"); + assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37-0123"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-01:00"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-0100"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37-01"); + // Negative timezone offsets with hyphen character. + assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37−01:23"); + assertParseStringToLong(1512381697000L + 4980000L, parser, "2017-12-04T10:01:37−0123"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−01:00"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−0100"); + assertParseStringToLong(1512381697000L + 3600000L, parser, "2017-12-04T10:01:37−01"); + } + + @Test + public void testIso8601ParserParseMissingTimezone() throws IOException { + DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser(); + try { + assertParseStringToLong(0, parser, "2017-12-04T10:01:37"); + fail(); + } catch (ParserException e) { + // Expected. + } + } + + private static void assertParseStringToLong( + long expected, ParsingLoadable.Parser parser, String data) throws IOException { + long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data))); + assertThat(actual).isEqualTo(expected); + } +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java similarity index 81% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index 4ddbf429ab..15fa3b3355 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -28,33 +28,38 @@ import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegm import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit tests for {@link DashUtil}. - */ -public final class DashUtilTest extends TestCase { +/** Unit tests for {@link DashUtil}. */ +@RunWith(RobolectricTestRunner.class) +public final class DashUtilTest { + @Test public void testLoadDrmInitDataFromManifest() throws Exception { Period period = newPeriod(newAdaptationSets(newRepresentations(newDrmInitData()))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isEqualTo(newDrmInitData()); } + @Test public void testLoadDrmInitDataMissing() throws Exception { Period period = newPeriod(newAdaptationSets(newRepresentations(null /* no init data */))); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } + @Test public void testLoadDrmInitDataNoRepresentations() throws Exception { - Period period = newPeriod(newAdaptationSets(/* no representation */)); + Period period = newPeriod(newAdaptationSets(/* no representation */ )); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } + @Test public void testLoadDrmInitDataNoAdaptationSets() throws Exception { - Period period = newPeriod(/* no adaptation set */); + Period period = newPeriod(/* no adaptation set */ ); DrmInitData drmInitData = DashUtil.loadDrmInitData(DummyDataSource.INSTANCE, period); assertThat(drmInitData).isNull(); } @@ -68,8 +73,18 @@ public final class DashUtilTest extends TestCase { } private static Representation newRepresentations(DrmInitData drmInitData) { - Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4, - MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0); + Format format = + Format.createVideoContainerFormat( + "id", + MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, + "", + Format.NO_VALUE, + 1024, + 768, + Format.NO_VALUE, + null, + 0); if (drmInitData != null) { format = format.copyWithDrmInitData(drmInitData); } @@ -77,8 +92,7 @@ public final class DashUtilTest extends TestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", - new byte[] {1, 4, 7, 0, 3, 6})); + return new DrmInitData( + new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } - } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java new file mode 100644 index 0000000000..9c3752551a --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/EventSampleStreamTest.java @@ -0,0 +1,356 @@ +/* + * 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.dash; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageEncoder; +import com.google.android.exoplayer2.source.dash.manifest.EventStream; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit test for {@link EventSampleStream}. + */ +@RunWith(RobolectricTestRunner.class) +public final class EventSampleStreamTest { + + private static final String SCHEME_ID = "urn:test"; + private static final String VALUE = "123"; + private static final Format FORMAT = Format.createSampleFormat("urn:test/123", + MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + private static final byte[] MESSAGE_DATA = new byte[] {1, 2, 3, 4}; + private static final long DURATION_MS = 3000; + private static final long TIME_SCALE = 1000; + + private FormatHolder formatHolder; + private MetadataInputBuffer inputBuffer; + private EventMessageEncoder eventMessageEncoder; + + @Before + public void setUp() { + formatHolder = new FormatHolder(); + inputBuffer = new MetadataInputBuffer(); + eventMessageEncoder = new EventMessageEncoder(); + } + + /** + * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will + * return format for the first call. + */ + @Test + public void testReadDataReturnFormatForFirstRead() { + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[0], new EventMessage[0]); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); + + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_FORMAT_READ); + assertThat(formatHolder.format).isEqualTo(FORMAT); + } + + /** + * Tests that a non-dynamic {@link EventSampleStream} will return a buffer with + * {@link C#BUFFER_FLAG_END_OF_STREAM} when trying to read sample out-of-bound. + */ + @Test + public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForNonDynamicEventSampleStream() { + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[0], new EventMessage[0]); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); + // first read - read format + readData(sampleStream); + + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.isEndOfStream()).isTrue(); + } + + /** + * Tests that a dynamic {@link EventSampleStream} will return {@link C#RESULT_NOTHING_READ} + * when trying to read sample out-of-bound. + */ + @Test + public void testReadDataOutOfBoundReturnEndOfStreamAfterFormatForDynamicEventSampleStream() { + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[0], new EventMessage[0]); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, true); + // first read - read format + readData(sampleStream); + + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_NOTHING_READ); + } + + /** + * Tests that {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} will + * return sample data after the first call. + */ + @Test + public void testReadDataReturnDataAfterFormat() { + long presentationTimeUs = 1000000; + EventMessage eventMessage = newEventMessageWithIdAndTime(1, presentationTimeUs); + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs}, new EventMessage[] {eventMessage}); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); + // first read - read format + readData(sampleStream); + + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage)); + } + + /** + * Tests that {@link EventSampleStream#skipData(long)} will skip until the given position, and + * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from that position. + */ + @Test + public void testSkipDataThenReadDataReturnDataFromSkippedPosition() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2}, + new EventMessage[] {eventMessage1, eventMessage2}); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); + // first read - read format + readData(sampleStream); + + int skipped = sampleStream.skipData(presentationTimeUs2); + int result = readData(sampleStream); + assertThat(skipped).isEqualTo(1); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage2)); + } + + /** + * Tests that {@link EventSampleStream#seekToUs(long)} (long)} will seek to the given position, + * and the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from that position. + */ + @Test + public void testSeekToUsThenReadDataReturnDataFromSeekPosition() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventStream eventStream = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2}, + new EventMessage[] {eventMessage1, eventMessage2}); + EventSampleStream sampleStream = new EventSampleStream(eventStream, FORMAT, false); + // first read - read format + readData(sampleStream); + + sampleStream.seekToUs(presentationTimeUs2); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage2)); + } + + /** + * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the + * underlying event stream, but keep the read timestamp, so the next + * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from after the last read sample timestamp. + */ + @Test + public void testUpdateEventStreamContinueToReadAfterLastReadSamplePresentationTime() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + long presentationTimeUs3 = 3000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3); + EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2}, + new EventMessage[] {eventMessage1, eventMessage2}); + EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3}, + new EventMessage[] {eventMessage1, eventMessage2, eventMessage3}); + EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true); + // first read - read format + readData(sampleStream); + // read first and second sample. + readData(sampleStream); + readData(sampleStream); + + sampleStream.updateEventStream(eventStream2, true); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage3)); + } + + /** + * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the + * underlying event stream, but keep the timestamp the stream has skipped to, so the next + * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from the skipped position. + */ + @Test + public void testSkipDataThenUpdateStreamContinueToReadFromSkippedPosition() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + long presentationTimeUs3 = 3000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3); + EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2}, + new EventMessage[] {eventMessage1, eventMessage2}); + EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3}, + new EventMessage[] {eventMessage1, eventMessage2, eventMessage3}); + EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true); + // first read - read format + readData(sampleStream); + sampleStream.skipData(presentationTimeUs2 + 1); + + sampleStream.updateEventStream(eventStream2, true); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage3)); + } + + /** + * Tests that {@link EventSampleStream#skipData(long)} will only skip to the point right after + * it last event. A following {@link EventSampleStream#updateEventStream(EventStream, boolean)} + * will update the underlying event stream and keep the timestamp the stream has skipped to, so + * the next {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from the skipped position. + */ + @Test + public void testSkipDataThenUpdateStreamContinueToReadDoNotSkippedMoreThanAvailable() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + long presentationTimeUs3 = 3000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3); + EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1}, + new EventMessage[] {eventMessage1}); + EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3}, + new EventMessage[] {eventMessage1, eventMessage2, eventMessage3}); + EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true); + // first read - read format + readData(sampleStream); + // even though the skip call is to 2000001, since eventStream1 only contains sample until + // 1000000, it will only skip to 1000001. + sampleStream.skipData(presentationTimeUs2 + 1); + + sampleStream.updateEventStream(eventStream2, true); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage2)); + } + + /** + * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the + * underlying event stream, but keep the timestamp the stream has seek to, so the next + * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from the seek position. + */ + @Test + public void testSeekToUsThenUpdateStreamContinueToReadFromSeekPosition() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + long presentationTimeUs3 = 3000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3); + EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2}, + new EventMessage[] {eventMessage1, eventMessage2}); + EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3}, + new EventMessage[] {eventMessage1, eventMessage2, eventMessage3}); + EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true); + // first read - read format + readData(sampleStream); + sampleStream.seekToUs(presentationTimeUs2); + + sampleStream.updateEventStream(eventStream2, true); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage2)); + } + + /** + * Tests that {@link EventSampleStream#updateEventStream(EventStream, boolean)} will update the + * underlying event stream, but keep the timestamp the stream has seek to, so the next + * {@link EventSampleStream#readData(FormatHolder, DecoderInputBuffer, boolean)} call + * will return sample data from the seek position. + */ + @Test + public void testSeekToThenUpdateStreamContinueToReadFromSeekPositionEvenSeekMoreThanAvailable() { + long presentationTimeUs1 = 1000000; + long presentationTimeUs2 = 2000000; + long presentationTimeUs3 = 3000000; + EventMessage eventMessage1 = newEventMessageWithIdAndTime(1, presentationTimeUs1); + EventMessage eventMessage2 = newEventMessageWithIdAndTime(2, presentationTimeUs2); + EventMessage eventMessage3 = newEventMessageWithIdAndTime(3, presentationTimeUs3); + EventStream eventStream1 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1}, + new EventMessage[] {eventMessage1}); + EventStream eventStream2 = new EventStream(SCHEME_ID, VALUE, TIME_SCALE, + new long[] {presentationTimeUs1, presentationTimeUs2, presentationTimeUs3}, + new EventMessage[] {eventMessage1, eventMessage2, eventMessage3}); + EventSampleStream sampleStream = new EventSampleStream(eventStream1, FORMAT, true); + // first read - read format + readData(sampleStream); + sampleStream.seekToUs(presentationTimeUs2 + 1); + + sampleStream.updateEventStream(eventStream2, true); + int result = readData(sampleStream); + assertThat(result).isEqualTo(C.RESULT_BUFFER_READ); + assertThat(inputBuffer.data.array()) + .isEqualTo(getEncodedMessage(eventMessage3)); + } + + private int readData(EventSampleStream sampleStream) { + inputBuffer.clear(); + return sampleStream.readData(formatHolder, inputBuffer, false); + } + + private EventMessage newEventMessageWithIdAndTime(int id, long presentationTimeUs) { + return new EventMessage(SCHEME_ID, VALUE, DURATION_MS, id, MESSAGE_DATA, presentationTimeUs); + } + + private byte[] getEncodedMessage(EventMessage eventMessage) { + return eventMessageEncoder.encode(eventMessage, TIME_SCALE); + } + +} diff --git a/library/dash/src/androidTest/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 similarity index 85% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParserTest.java index 3b7982592d..6f14c8790f 100644 --- a/library/dash/src/androidTest/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 @@ -18,39 +18,47 @@ package com.google.android.exoplayer2.source.dash.manifest; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; +import java.nio.charset.Charset; import java.util.Collections; import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit tests for {@link DashManifestParser}. - */ -public class DashManifestParserTest extends InstrumentationTestCase { +/** Unit tests for {@link DashManifestParser}. */ +@RunWith(RobolectricTestRunner.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"; - /** - * Simple test to ensure the sample manifests parse without any exceptions being thrown. - */ + /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */ + @Test public void testParseMediaPresentationDescription() throws IOException { DashManifestParser parser = new DashManifestParser(); - parser.parse(Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_1)); - parser.parse(Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_1)); + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); } + @Test public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException { DashManifestParser parser = new DashManifestParser(); - DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE)); + DashManifest mpd = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_3_SEGMENT_TEMPLATE)); assertThat(mpd.getPeriodCount()).isEqualTo(1); @@ -75,11 +83,13 @@ public class DashManifestParserTest extends InstrumentationTestCase { } } - public void testParseMediaPresentationDescriptionCanParseEventStream() - throws IOException { + @Test + public void testParseMediaPresentationDescriptionCanParseEventStream() throws IOException { DashManifestParser parser = new DashManifestParser(); - DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_4_EVENT_STREAM)); + DashManifest mpd = + parser.parse( + Uri.parse("https://example.com/test.mpd"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_MPD_4_EVENT_STREAM)); Period period = mpd.getPeriod(0); assertThat(period.eventStreams).hasSize(3); @@ -87,8 +97,14 @@ public class DashManifestParserTest extends InstrumentationTestCase { // assert text-only event stream EventStream eventStream1 = period.eventStreams.get(0); assertThat(eventStream1.events.length).isEqualTo(1); - EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0, - "+ 1 800 10101010".getBytes(), 0); + EventMessage expectedEvent1 = + new EventMessage( + "urn:uuid:XYZY", + "call", + 10000, + 0, + "+ 1 800 10101010".getBytes(Charset.forName(C.UTF8_NAME)), + 0); assertThat(eventStream1.events[0]).isEqualTo(expectedEvent1); // assert CData-structured event stream @@ -135,6 +151,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { 1000000000)); } + @Test public void testParseCea608AccessibilityChannel() { assertThat( DashManifestParser.parseCea608AccessibilityChannel( @@ -175,6 +192,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { .isEqualTo(Format.NO_VALUE); } + @Test public void testParseCea708AccessibilityChannel() { assertThat( DashManifestParser.parseCea708AccessibilityChannel( @@ -226,5 +244,4 @@ public class DashManifestParserTest extends InstrumentationTestCase { private static List buildCea708AccessibilityDescriptors(String value) { return Collections.singletonList(new Descriptor("urn:scte:dash:cc:cea-708:2015", value, null)); } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java similarity index 54% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java index c6060a7caa..9cf3594116 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestTest.java @@ -24,109 +24,143 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit tests for {@link DashManifest}. - */ -public class DashManifestTest extends TestCase { +/** Unit tests for {@link DashManifest}. */ +@RunWith(RobolectricTestRunner.class) +public class DashManifestTest { private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", ""); private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase(); private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0); + @Test public void testCopy() throws Exception { Representation[][][] representations = newRepresentations(3, 2, 3); - DashManifest sourceManifest = newDashManifest(10, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0]), - newAdaptationSet(3, representations[0][1])), - newPeriod("4", 4, - newAdaptationSet(5, representations[1][0]), - newAdaptationSet(6, representations[1][1])), - newPeriod("7", 7, - newAdaptationSet(8, representations[2][0]), - newAdaptationSet(9, representations[2][1]))); + DashManifest sourceManifest = + newDashManifest( + 10, + newPeriod( + "1", + 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod( + "4", + 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod( + "7", + 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); - List keys = Arrays.asList( - new RepresentationKey(0, 0, 0), - new RepresentationKey(0, 0, 1), - new RepresentationKey(0, 1, 2), - - new RepresentationKey(1, 0, 1), - new RepresentationKey(1, 1, 0), - new RepresentationKey(1, 1, 2), - - new RepresentationKey(2, 0, 1), - new RepresentationKey(2, 0, 2), - new RepresentationKey(2, 1, 0)); + List keys = + Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + new RepresentationKey(1, 0, 1), + new RepresentationKey(1, 1, 0), + new RepresentationKey(1, 1, 2), + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); DashManifest copyManifest = sourceManifest.copy(keys); - DashManifest expectedManifest = newDashManifest(10, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), - newAdaptationSet(3, representations[0][1][2])), - newPeriod("4", 4, - newAdaptationSet(5, representations[1][0][1]), - newAdaptationSet(6, representations[1][1][0], representations[1][1][2])), - newPeriod("7", 7, - newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), - newAdaptationSet(9, representations[2][1][0]))); + DashManifest expectedManifest = + newDashManifest( + 10, + newPeriod( + "1", + 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod( + "4", + 4, + newAdaptationSet(5, representations[1][0][1]), + newAdaptationSet(6, representations[1][1][0], representations[1][1][2])), + newPeriod( + "7", + 7, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); assertManifestEquals(expectedManifest, copyManifest); } + @Test public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception { Representation[][][] representations = newRepresentations(2, 1, 1); - DashManifest sourceManifest = newDashManifest(10, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0])), - newPeriod("4", 4, - newAdaptationSet(5, representations[1][0]))); + DashManifest sourceManifest = + newDashManifest( + 10, + newPeriod("1", 1, newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, newAdaptationSet(5, representations[1][0]))); - DashManifest copyManifest = sourceManifest.copy(Arrays.asList( - new RepresentationKey(0, 0, 0), - new RepresentationKey(1, 0, 0))); + DashManifest copyManifest = + sourceManifest.copy( + Arrays.asList(new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0))); - DashManifest expectedManifest = newDashManifest(10, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0])), - newPeriod("4", 4, - newAdaptationSet(5, representations[1][0]))); + DashManifest expectedManifest = + newDashManifest( + 10, + newPeriod("1", 1, newAdaptationSet(2, representations[0][0])), + newPeriod("4", 4, newAdaptationSet(5, representations[1][0]))); assertManifestEquals(expectedManifest, copyManifest); } + @Test public void testCopySkipPeriod() throws Exception { Representation[][][] representations = newRepresentations(3, 2, 3); - DashManifest sourceManifest = newDashManifest(10, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0]), - newAdaptationSet(3, representations[0][1])), - newPeriod("4", 4, - newAdaptationSet(5, representations[1][0]), - newAdaptationSet(6, representations[1][1])), - newPeriod("7", 7, - newAdaptationSet(8, representations[2][0]), - newAdaptationSet(9, representations[2][1]))); + DashManifest sourceManifest = + newDashManifest( + 10, + newPeriod( + "1", + 1, + newAdaptationSet(2, representations[0][0]), + newAdaptationSet(3, representations[0][1])), + newPeriod( + "4", + 4, + newAdaptationSet(5, representations[1][0]), + newAdaptationSet(6, representations[1][1])), + newPeriod( + "7", + 7, + newAdaptationSet(8, representations[2][0]), + newAdaptationSet(9, representations[2][1]))); - DashManifest copyManifest = sourceManifest.copy(Arrays.asList( - new RepresentationKey(0, 0, 0), - new RepresentationKey(0, 0, 1), - new RepresentationKey(0, 1, 2), + DashManifest copyManifest = + sourceManifest.copy( + Arrays.asList( + new RepresentationKey(0, 0, 0), + new RepresentationKey(0, 0, 1), + new RepresentationKey(0, 1, 2), + new RepresentationKey(2, 0, 1), + new RepresentationKey(2, 0, 2), + new RepresentationKey(2, 1, 0))); - new RepresentationKey(2, 0, 1), - new RepresentationKey(2, 0, 2), - new RepresentationKey(2, 1, 0))); - - DashManifest expectedManifest = newDashManifest(7, - newPeriod("1", 1, - newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), - newAdaptationSet(3, representations[0][1][2])), - newPeriod("7", 4, - newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), - newAdaptationSet(9, representations[2][1][0]))); + DashManifest expectedManifest = + newDashManifest( + 7, + newPeriod( + "1", + 1, + newAdaptationSet(2, representations[0][0][0], representations[0][0][1]), + newAdaptationSet(3, representations[0][1][2])), + newPeriod( + "7", + 4, + newAdaptationSet(8, representations[2][0][1], representations[2][0][2]), + newAdaptationSet(9, representations[2][1][0]))); assertManifestEquals(expectedManifest, copyManifest); } @@ -164,8 +198,8 @@ public class DashManifestTest extends TestCase { } } - private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts, - int representationCounts) { + private static Representation[][][] newRepresentations( + int periodCount, int adaptationSetCounts, int representationCounts) { Representation[][][] representations = new Representation[periodCount][][]; for (int i = 0; i < periodCount; i++) { representations[i] = new Representation[adaptationSetCounts][]; @@ -184,8 +218,8 @@ public class DashManifestTest extends TestCase { } private static DashManifest newDashManifest(int duration, Period... periods) { - return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, - Arrays.asList(periods)); + return new DashManifest( + 0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods)); } private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) { @@ -195,5 +229,4 @@ public class DashManifestTest extends TestCase { private static AdaptationSet newAdaptationSet(int seed, Representation... representations) { return new AdaptationSet(++seed, ++seed, Arrays.asList(representations), null, null); } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java similarity index 92% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java index 4073625cd1..16c9a4706e 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RangedUriTest.java @@ -18,17 +18,19 @@ package com.google.android.exoplayer2.source.dash.manifest; import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link RangedUri}. - */ -public class RangedUriTest extends TestCase { +/** Unit test for {@link RangedUri}. */ +@RunWith(RobolectricTestRunner.class) +public class RangedUriTest { private static final String BASE_URI = "http://www.test.com/"; private static final String PARTIAL_URI = "path/file.ext"; private static final String FULL_URI = BASE_URI + PARTIAL_URI; + @Test public void testMerge() { RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); @@ -36,6 +38,7 @@ public class RangedUriTest extends TestCase { assertMerge(rangeA, rangeB, expected, null); } + @Test public void testMergeUnbounded() { RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET); @@ -43,6 +46,7 @@ public class RangedUriTest extends TestCase { assertMerge(rangeA, rangeB, expected, null); } + @Test public void testNonMerge() { // A and B do not overlap, so should not merge RangedUri rangeA = new RangedUri(FULL_URI, 0, 10); @@ -65,6 +69,7 @@ public class RangedUriTest extends TestCase { assertNonMerge(rangeA, rangeB, null); } + @Test public void testMergeWithBaseUri() { RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10); @@ -85,5 +90,4 @@ public class RangedUriTest extends TestCase { merged = rangeB.attemptMerge(rangeA, baseUrl); assertThat(merged).isNull(); } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java similarity index 54% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java index 2ceb0f0506..309e6c8eb0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/RepresentationTest.java @@ -20,27 +20,49 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.util.MimeTypes; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link Representation}. - */ -public class RepresentationTest extends TestCase { +/** Unit test for {@link Representation}. */ +@RunWith(RobolectricTestRunner.class) +public class RepresentationTest { + @Test public void testGetCacheKey() { String uri = "http://www.google.com"; SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1); - Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null, - MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0); - Representation representation = Representation.newInstance("test_stream_1", 3, format, uri, - base); + Format format = + Format.createVideoContainerFormat( + "0", + MimeTypes.APPLICATION_MP4, + null, + MimeTypes.VIDEO_H264, + 2500000, + 1920, + 1080, + Format.NO_VALUE, + null, + 0); + Representation representation = + Representation.newInstance("test_stream_1", 3, format, uri, base); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.0.3"); - format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null, - MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0); - representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT, - format, uri, base); + format = + Format.createVideoContainerFormat( + "150", + MimeTypes.APPLICATION_MP4, + null, + MimeTypes.VIDEO_H264, + 2500000, + 1920, + 1080, + Format.NO_VALUE, + null, + 0); + representation = + Representation.newInstance( + "test_stream_1", Representation.REVISION_ID_DEFAULT, format, uri, base); assertThat(representation.getCacheKey()).isEqualTo("test_stream_1.150.-1"); } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java similarity index 89% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java index b3221bbc18..4192280c81 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/manifest/UrlTemplateTest.java @@ -16,14 +16,17 @@ package com.google.android.exoplayer2.source.dash.manifest; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit test for {@link UrlTemplate}. - */ -public class UrlTemplateTest extends TestCase { +/** Unit test for {@link UrlTemplate}. */ +@RunWith(RobolectricTestRunner.class) +public class UrlTemplateTest { + @Test public void testRealExamples() { String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)"; UrlTemplate urlTemplate = UrlTemplate.compile(template); @@ -41,6 +44,7 @@ public class UrlTemplateTest extends TestCase { assertThat(url).isEqualTo("chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s"); } + @Test public void testFull() { String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$"; UrlTemplate urlTemplate = UrlTemplate.compile(template); @@ -48,6 +52,7 @@ public class UrlTemplateTest extends TestCase { assertThat(url).isEqualTo("650000_a_abc1_b_5000_c_10"); } + @Test public void testFullWithDollarEscaping() { String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$"; UrlTemplate urlTemplate = UrlTemplate.compile(template); @@ -55,6 +60,7 @@ public class UrlTemplateTest extends TestCase { assertThat(url).isEqualTo("$650000$_a$_abc1_b_5000_c_10$"); } + @Test public void testInvalidSubstitution() { String template = "$IllegalId$"; try { @@ -64,5 +70,4 @@ public class UrlTemplateTest extends TestCase { // Expected. } } - } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java new file mode 100644 index 0000000000..ea47722b69 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadActionTest.java @@ -0,0 +1,162 @@ +/* + * 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.dash.offline; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +/** + * Unit tests for {@link DashDownloadAction}. + */ +@RunWith(RobolectricTestRunner.class) +public class DashDownloadActionTest { + + @Test + public void testDownloadActionIsNotRemoveAction() throws Exception { + DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); + assertThat(action.isRemoveAction()).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() throws Exception { + DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), true, null); + assertThat(action2.isRemoveAction()).isTrue(); + } + + @Test + public void testCreateDownloader() throws Exception { + MockitoAnnotations.initMocks(this); + DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriDifferentAction_IsSameMedia() throws Exception { + DashDownloadAction action1 = new DashDownloadAction(Uri.parse("uri"), true, null); + DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), false, null); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testDifferentUriAndAction_IsNotSameMedia() throws Exception { + DashDownloadAction action3 = new DashDownloadAction(Uri.parse("uri2"), true, null); + DashDownloadAction action4 = new DashDownloadAction(Uri.parse("uri"), false, null); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @SuppressWarnings("EqualsWithItself") + @Test + public void testEquals() throws Exception { + DashDownloadAction action1 = new DashDownloadAction(Uri.parse("uri"), true, null); + assertThat(action1.equals(action1)).isTrue(); + + DashDownloadAction action2 = new DashDownloadAction(Uri.parse("uri"), true, null); + DashDownloadAction action3 = new DashDownloadAction(Uri.parse("uri"), true, null); + assertEqual(action2, action3); + + DashDownloadAction action4 = new DashDownloadAction(Uri.parse("uri"), true, null); + DashDownloadAction action5 = new DashDownloadAction(Uri.parse("uri"), false, null); + assertNotEqual(action4, action5); + + DashDownloadAction action6 = new DashDownloadAction(Uri.parse("uri"), false, null); + DashDownloadAction action7 = + new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(0, 0, 0)); + assertNotEqual(action6, action7); + + DashDownloadAction action8 = + new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(1, 1, 1)); + DashDownloadAction action9 = + new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey(0, 0, 0)); + assertNotEqual(action8, action9); + + DashDownloadAction action10 = new DashDownloadAction(Uri.parse("uri"), true, null); + DashDownloadAction action11 = new DashDownloadAction(Uri.parse("uri2"), true, null); + assertNotEqual(action10, action11); + + DashDownloadAction action12 = new DashDownloadAction(Uri.parse("uri"), false, null, + new RepresentationKey(0, 0, 0), new RepresentationKey(1, 1, 1)); + DashDownloadAction action13 = new DashDownloadAction(Uri.parse("uri"), false, null, + new RepresentationKey(1, 1, 1), new RepresentationKey(0, 0, 0)); + assertEqual(action12, action13); + + DashDownloadAction action14 = new DashDownloadAction(Uri.parse("uri"), false, null, + new RepresentationKey(0, 0, 0)); + DashDownloadAction action15 = new DashDownloadAction(Uri.parse("uri"), false, null, + new RepresentationKey(1, 1, 1), new RepresentationKey(0, 0, 0)); + assertNotEqual(action14, action15); + + DashDownloadAction action16 = new DashDownloadAction(Uri.parse("uri"), false, null); + DashDownloadAction action17 = + new DashDownloadAction(Uri.parse("uri"), false, null, new RepresentationKey[0]); + assertEqual(action16, action17); + } + + @Test + public void testSerializerGetType() throws Exception { + DashDownloadAction action = new DashDownloadAction(Uri.parse("uri"), false, null); + assertThat(action.getType()).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri"), false, null)); + doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri"), true, null)); + doTestSerializationRoundTrip(new DashDownloadAction(Uri.parse("uri2"), false, null, + new RepresentationKey(0, 0, 0), new RepresentationKey(1, 1, 1))); + } + + private static void assertNotEqual(DashDownloadAction action1, DashDownloadAction action2) { + assertThat(action1).isNotEqualTo(action2); + assertThat(action2).isNotEqualTo(action1); + } + + private static void assertEqual(DashDownloadAction action1, DashDownloadAction action2) { + assertThat(action1).isEqualTo(action2); + assertThat(action2).isEqualTo(action1); + } + + private static void doTestSerializationRoundTrip(DashDownloadAction action1) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + action1.writeToStream(output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = + DashDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); + + assertThat(action1).isEqualTo(action2); + } + +} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java new file mode 100644 index 0000000000..a215347f15 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java @@ -0,0 +1,103 @@ +/* + * 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.dash.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; + +/** Data for DASH downloading tests. */ +/* package */ interface DashDownloadTestData { + + Uri TEST_MPD_URI = Uri.parse("test.mpd"); + + byte[] TEST_MPD = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // Bounded range data + + " \n" + // Unbounded range data + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // This segment list has a 1 second offset to make sure the progressive download order + + " \n" + + " \n" + + " \n" // 1s offset + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "") + .getBytes(Charset.forName(C.UTF8_NAME)); + + byte[] TEST_MPD_NO_INDEX = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "") + .getBytes(Charset.forName(C.UTF8_NAME)); +} diff --git a/library/dash/src/androidTest/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 similarity index 64% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 868166673a..1d3fcb2135 100644 --- a/library/dash/src/androidTest/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 @@ -22,10 +22,10 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmp import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; @@ -35,7 +35,6 @@ import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; -import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; @@ -44,34 +43,38 @@ import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit tests for {@link DashDownloader}. - */ -public class DashDownloaderTest extends InstrumentationTestCase { +/** Unit tests for {@link DashDownloader}. */ +@RunWith(RobolectricTestRunner.class) +public class DashDownloaderTest { private SimpleCache cache; private File tempFolder; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - MockitoUtil.setUpMockito(this); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + MockitoAnnotations.initMocks(this); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testGetManifest() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD); + FakeDataSet fakeDataSet = new FakeDataSet().setData(TEST_MPD_URI, TEST_MPD); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); DashManifest manifest = dashDownloader.getManifest(); @@ -80,15 +83,17 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadManifestFailure() throws Exception { byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10); byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length); - FakeDataSet fakeDataSet = new FakeDataSet() - .newData(TEST_MPD_URI) - .appendReadData(testMpdFirstPart) - .appendReadError(new IOException()) - .appendReadData(testMpdSecondPart) - .endData(); + FakeDataSet fakeDataSet = + new FakeDataSet() + .newData(TEST_MPD_URI) + .appendReadData(testMpdFirstPart) + .appendReadError(new IOException()) + .appendReadData(testMpdSecondPart) + .endData(); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); // fails on the first try @@ -108,13 +113,15 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadRepresentation() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); @@ -123,17 +130,19 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadRepresentationInSmallParts() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .newData("audio_segment_1") - .appendReadData(TestUtil.buildTestData(10)) - .appendReadData(TestUtil.buildTestData(10)) - .appendReadData(TestUtil.buildTestData(10)) - .endData() - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .newData("audio_segment_1") + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); @@ -142,16 +151,18 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadRepresentations() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations( @@ -161,19 +172,21 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadAllRepresentations() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3) - .setRandomData("period_2_segment_1", 1) - .setRandomData("period_2_segment_2", 2) - .setRandomData("period_2_segment_3", 3); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); // dashDownloader.selectRepresentations() isn't called @@ -190,21 +203,23 @@ public class DashDownloaderTest extends InstrumentationTestCase { dashDownloader.remove(); } + @Test public void testProgressiveDownload() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); Factory factory = mock(Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); - DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, - new DownloaderConstructorHelper(cache, factory)); + DashDownloader dashDownloader = + new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); dashDownloader.selectRepresentations( new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); @@ -222,21 +237,23 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("text_segment_3"); } + @Test public void testProgressiveDownloadSeparatePeriods() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("period_2_segment_1", 1) - .setRandomData("period_2_segment_2", 2) - .setRandomData("period_2_segment_3", 3); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); Factory factory = mock(Factory.class); when(factory.createDataSource()).thenReturn(fakeDataSource); - DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, - new DownloaderConstructorHelper(cache, factory)); + DashDownloader dashDownloader = + new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); dashDownloader.selectRepresentations( new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)}); @@ -254,17 +271,19 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertThat(openedDataSpecs[7].uri.getPath()).isEqualTo("period_2_segment_3"); } + @Test public void testDownloadRepresentationFailure() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .newData("audio_segment_2") - .appendReadData(TestUtil.buildTestData(2)) - .appendReadError(new IOException()) - .appendReadData(TestUtil.buildTestData(3)) - .endData() - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); @@ -280,17 +299,19 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCounters() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .newData("audio_segment_2") - .appendReadData(TestUtil.buildTestData(2)) - .appendReadError(new IOException()) - .appendReadData(TestUtil.buildTestData(3)) - .endData() - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); @@ -314,13 +335,15 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6); } + @Test public void testListener() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); @@ -335,16 +358,18 @@ public class DashDownloaderTest extends InstrumentationTestCase { inOrder.verifyNoMoreInteractions(); } + @Test public void testRemoveAll() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations( new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); @@ -355,10 +380,12 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCacheEmpty(cache); } + @Test public void testRepresentationWithoutIndex() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) - .setRandomData("test_segment_1", 4); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) + .setRandomData("test_segment_1", 4); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); @@ -374,13 +401,15 @@ public class DashDownloaderTest extends InstrumentationTestCase { assertCacheEmpty(cache); } + @Test public void testSelectRepresentationsClearsPreviousSelection() throws Exception { - FakeDataSet fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6); + FakeDataSet fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); DashDownloader dashDownloader = getDashDownloader(fakeDataSet); dashDownloader.selectRepresentations( @@ -396,11 +425,13 @@ public class DashDownloaderTest extends InstrumentationTestCase { return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); } - private static void assertCounters(DashDownloader dashDownloader, int totalSegments, - int downloadedSegments, int downloadedBytes) { + private static void assertCounters( + DashDownloader dashDownloader, + int totalSegments, + int downloadedSegments, + int downloadedBytes) { assertThat(dashDownloader.getTotalSegments()).isEqualTo(totalSegments); assertThat(dashDownloader.getDownloadedSegments()).isEqualTo(downloadedSegments); assertThat(dashDownloader.getDownloadedBytes()).isEqualTo(downloadedBytes); } - } diff --git a/library/dash/src/androidTest/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 similarity index 74% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 0d3c6ed0f7..84622f4828 100644 --- a/library/dash/src/androidTest/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 @@ -23,26 +23,33 @@ import static com.google.common.truth.Truth.assertThat; import android.content.Context; import android.os.ConditionVariable; -import android.test.InstrumentationTestCase; -import android.test.UiThreadTest; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; -import com.google.android.exoplayer2.testutil.MockitoUtil; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; -/** - * Tests {@link DownloadManager}. - */ -public class DownloadManagerDashTest extends InstrumentationTestCase { +/** Tests {@link DownloadManager}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadManagerDashTest { private static final int ASSERT_TRUE_TIMEOUT = 1000; @@ -56,26 +63,25 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { private File actionFile; private DummyMainThread dummyMainThread; - @UiThreadTest - @Override + @Before public void setUp() throws Exception { - super.setUp(); dummyMainThread = new DummyMainThread(); - Context context = getInstrumentation().getContext(); + Context context = RuntimeEnvironment.application; tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); File cacheFolder = new File(tempFolder, "cache"); cacheFolder.mkdir(); cache = new SimpleCache(cacheFolder, new NoOpCacheEvictor()); - MockitoUtil.setUpMockito(this); - fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .setRandomData("audio_init_data", 10) - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3); + MockitoAnnotations.initMocks(this); + fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); @@ -83,33 +89,35 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { createDownloadManager(); } - @UiThreadTest - @Override + @After public void tearDown() throws Exception { downloadManager.release(); Util.recursiveDelete(tempFolder); dummyMainThread.release(); - super.tearDown(); } // Disabled due to flakiness. - public void disabledTestSaveAndLoadActionFile() throws Throwable { + @Ignore + @Test + public void testSaveAndLoadActionFile() throws Throwable { // Configure fakeDataSet to block until interrupted when TEST_MPD is read. - fakeDataSet.newData(TEST_MPD_URI) - .appendReadAction(new Runnable() { - @SuppressWarnings("InfiniteLoopStatement") - @Override - public void run() { - try { - // Wait until interrupted. - while (true) { - Thread.sleep(100000); + fakeDataSet + .newData(TEST_MPD_URI) + .appendReadAction( + new Runnable() { + @SuppressWarnings("InfiniteLoopStatement") + @Override + public void run() { + try { + // Wait until interrupted. + while (true) { + Thread.sleep(100000); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } } - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - }) + }) .appendReadData(TEST_MPD) .endData(); @@ -122,15 +130,20 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { // Setup an Action and immediately release the DM. handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); downloadManager.release(); + } + }); - assertThat(actionFile.exists()).isTrue(); - assertThat(actionFile.length()).isGreaterThan(0L); + assertThat(actionFile.exists()).isTrue(); + assertThat(actionFile.length()).isGreaterThan(0L); + assertCacheEmpty(cache); - assertCacheEmpty(cache); - - // Revert fakeDataSet to normal. - fakeDataSet.setData(TEST_MPD_URI, TEST_MPD); + // Revert fakeDataSet to normal. + fakeDataSet.setData(TEST_MPD_URI, TEST_MPD); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { createDownloadManager(); } }); @@ -140,12 +153,14 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testHandleDownloadAction() throws Throwable { handleDownloadAction(fakeRepresentationKey1, fakeRepresentationKey2); blockUntilTasksCompleteAndThrowAnyDownloadError(); assertCachedData(cache, fakeDataSet); } + @Test public void testHandleMultipleDownloadAction() throws Throwable { handleDownloadAction(fakeRepresentationKey1); handleDownloadAction(fakeRepresentationKey2); @@ -153,6 +168,7 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testHandleInterferingDownloadAction() throws Throwable { fakeDataSet .newData("audio_segment_2") @@ -172,6 +188,7 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testHandleRemoveAction() throws Throwable { handleDownloadAction(fakeRepresentationKey1); @@ -185,7 +202,9 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { } // Disabled due to flakiness. - public void disabledTestHandleRemoveActionBeforeDownloadFinish() throws Throwable { + @Ignore + @Test + public void testHandleRemoveActionBeforeDownloadFinish() throws Throwable { handleDownloadAction(fakeRepresentationKey1); handleRemoveAction(); @@ -194,15 +213,18 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { assertCacheEmpty(cache); } + @Test public void testHandleInterferingRemoveAction() throws Throwable { final ConditionVariable downloadInProgressCondition = new ConditionVariable(); - fakeDataSet.newData("audio_segment_2") - .appendReadAction(new Runnable() { - @Override - public void run() { - downloadInProgressCondition.open(); - } - }) + fakeDataSet + .newData("audio_segment_2") + .appendReadAction( + new Runnable() { + @Override + public void run() { + downloadInProgressCondition.open(); + } + }) .appendReadData(TestUtil.buildTestData(5)) .endData(); @@ -250,5 +272,4 @@ public class DownloadManagerDashTest extends InstrumentationTestCase { } }); } - } diff --git a/library/dash/src/androidTest/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 similarity index 80% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index 2a6d127aa0..e8044e57c1 100644 --- a/library/dash/src/androidTest/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 @@ -22,7 +22,6 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa import android.content.Context; import android.content.Intent; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; @@ -32,6 +31,7 @@ import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; @@ -40,11 +40,18 @@ import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; -/** - * Unit tests for {@link DownloadService}. - */ -public class DownloadServiceDashTest extends InstrumentationTestCase { +/** Unit tests for {@link DownloadService}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public class DownloadServiceDashTest { private SimpleCache cache; private File tempFolder; @@ -57,44 +64,44 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { private TestDownloadListener testDownloadListener; private DummyMainThread dummyMainThread; - @Override + @Before public void setUp() throws Exception { - super.setUp(); dummyMainThread = new DummyMainThread(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + context = RuntimeEnvironment.application; + tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); - Runnable pauseAction = new Runnable() { - @Override - public void run() { - if (pauseDownloadCondition != null) { - try { - pauseDownloadCondition.block(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + Runnable pauseAction = + new Runnable() { + @Override + public void run() { + if (pauseDownloadCondition != null) { + try { + pauseDownloadCondition.block(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } - } - } - }; - fakeDataSet = new FakeDataSet() - .setData(TEST_MPD_URI, TEST_MPD) - .newData("audio_init_data") - .appendReadAction(pauseAction) - .appendReadData(TestUtil.buildTestData(10)) - .endData() - .setRandomData("audio_segment_1", 4) - .setRandomData("audio_segment_2", 5) - .setRandomData("audio_segment_3", 6) - .setRandomData("text_segment_1", 1) - .setRandomData("text_segment_2", 2) - .setRandomData("text_segment_3", 3); + }; + fakeDataSet = + new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .newData("audio_init_data") + .appendReadAction(pauseAction) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); final DataSource.Factory fakeDataSourceFactory = new FakeDataSource.Factory(null).setFakeDataSet(fakeDataSet); fakeRepresentationKey1 = new RepresentationKey(0, 0, 0); fakeRepresentationKey2 = new RepresentationKey(0, 1, 0); - context = getInstrumentation().getContext(); - try { dummyMainThread.runOnMainThread( new Runnable() { @@ -128,7 +135,7 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { @Override protected String getNotificationChannelId() { - return null; + return ""; } @Override @@ -149,7 +156,7 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { } } - @Override + @After public void tearDown() throws Exception { try { dummyMainThread.runOnMainThread( @@ -164,9 +171,9 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { } Util.recursiveDelete(tempFolder); dummyMainThread.release(); - super.tearDown(); } + @Test public void testMultipleDownloadAction() throws Throwable { downloadKeys(fakeRepresentationKey1); downloadKeys(fakeRepresentationKey2); @@ -176,6 +183,7 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testRemoveAction() throws Throwable { downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); @@ -188,6 +196,7 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { assertCacheEmpty(cache); } + @Test public void testRemoveBeforeDownloadComplete() throws Throwable { pauseDownloadCondition = new ConditionVariable(); downloadKeys(fakeRepresentationKey1, fakeRepresentationKey2); @@ -219,5 +228,4 @@ public class DownloadServiceDashTest extends InstrumentationTestCase { } }); } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java similarity index 80% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java rename to library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java index 6fb89697c7..21af7e42c1 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/TestDownloadListener.java @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; import com.google.android.exoplayer2.offline.DownloadManager.DownloadState; import com.google.android.exoplayer2.testutil.DummyMainThread; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** A {@link DownloadListener} for testing. */ /*package*/ final class TestDownloadListener implements DownloadListener { @@ -29,13 +31,12 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; private final DownloadManager downloadManager; private final DummyMainThread dummyMainThread; - private final android.os.ConditionVariable downloadFinishedCondition; + private CountDownLatch downloadFinishedCondition; private Throwable downloadError; public TestDownloadListener(DownloadManager downloadManager, DummyMainThread dummyMainThread) { this.downloadManager = downloadManager; this.dummyMainThread = dummyMainThread; - this.downloadFinishedCondition = new android.os.ConditionVariable(); } @Override @@ -46,8 +47,10 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; } @Override - public void onIdle(DownloadManager downloadManager) { - downloadFinishedCondition.open(); + public synchronized void onIdle(DownloadManager downloadManager) { + if (downloadFinishedCondition != null) { + downloadFinishedCondition.countDown(); + } } /** @@ -55,18 +58,19 @@ import com.google.android.exoplayer2.testutil.DummyMainThread; * error. */ public void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + synchronized (this) { + downloadFinishedCondition = new CountDownLatch(1); + } dummyMainThread.runOnMainThread( new Runnable() { @Override public void run() { if (downloadManager.isIdle()) { - downloadFinishedCondition.open(); - } else { - downloadFinishedCondition.close(); + downloadFinishedCondition.countDown(); } } }); - assertThat(downloadFinishedCondition.block(TIMEOUT)).isTrue(); + assertThat(downloadFinishedCondition.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue(); if (downloadError != null) { throw new Exception(downloadError); } diff --git a/library/dash/src/test/resources/robolectric.properties b/library/dash/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/library/dash/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 5471eacec6..41e0b71da2 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -35,10 +35,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile project(modulePrefix + 'testutils') - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java deleted file mode 100644 index ec70fb1200..0000000000 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java +++ /dev/null @@ -1,77 +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.hls.offline; - -/** - * Data for HLS downloading tests. - */ -/* package */ interface HlsDownloadTestData { - - String MASTER_PLAYLIST_URI = "test.m3u8"; - - String MEDIA_PLAYLIST_0_DIR = "gear0/"; - String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; - String MEDIA_PLAYLIST_1_DIR = "gear1/"; - String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8"; - String MEDIA_PLAYLIST_2_DIR = "gear2/"; - String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8"; - String MEDIA_PLAYLIST_3_DIR = "gear3/"; - String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; - - byte[] MASTER_PLAYLIST_DATA = - ("#EXTM3U\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" - + MEDIA_PLAYLIST_1_URI + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" - + MEDIA_PLAYLIST_2_URI + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" - + MEDIA_PLAYLIST_3_URI + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n" - + MEDIA_PLAYLIST_0_URI).getBytes(); - - byte[] MEDIA_PLAYLIST_DATA = - ("#EXTM3U\n" - + "#EXT-X-TARGETDURATION:10\n" - + "#EXT-X-VERSION:3\n" - + "#EXT-X-MEDIA-SEQUENCE:0\n" - + "#EXT-X-PLAYLIST-TYPE:VOD\n" - + "#EXTINF:9.97667,\n" - + "fileSequence0.ts\n" - + "#EXTINF:9.97667,\n" - + "fileSequence1.ts\n" - + "#EXTINF:9.97667,\n" - + "fileSequence2.ts\n" - + "#EXT-X-ENDLIST").getBytes(); - - String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8"; - - byte[] ENC_MEDIA_PLAYLIST_DATA = - ("#EXTM3U\n" - + "#EXT-X-TARGETDURATION:10\n" - + "#EXT-X-VERSION:3\n" - + "#EXT-X-MEDIA-SEQUENCE:0\n" - + "#EXT-X-PLAYLIST-TYPE:VOD\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n" - + "#EXTINF:9.97667,\n" - + "fileSequence0.ts\n" - + "#EXTINF:9.97667,\n" - + "fileSequence1.ts\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n" - + "#EXTINF:9.97667,\n" - + "fileSequence2.ts\n" - + "#EXT-X-ENDLIST").getBytes(); - -} diff --git a/library/hls/src/androidTest/AndroidManifest.xml b/library/hls/src/test/AndroidManifest.xml similarity index 71% rename from library/hls/src/androidTest/AndroidManifest.xml rename to library/hls/src/test/AndroidManifest.xml index b1aadd203b..331f3439ad 100644 --- a/library/hls/src/androidTest/AndroidManifest.xml +++ b/library/hls/src/test/AndroidManifest.xml @@ -18,16 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.source.hls.test"> - - - - - - - + diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java new file mode 100644 index 0000000000..55db28a59a --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -0,0 +1,83 @@ +/* + * 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.hls.offline; + +import com.google.android.exoplayer2.C; +import java.nio.charset.Charset; + +/** Data for HLS downloading tests. */ +/* package */ interface HlsDownloadTestData { + + String MASTER_PLAYLIST_URI = "test.m3u8"; + + String MEDIA_PLAYLIST_0_DIR = "gear0/"; + String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_1_DIR = "gear1/"; + String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_2_DIR = "gear2/"; + String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_3_DIR = "gear3/"; + String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; + + byte[] MASTER_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + + MEDIA_PLAYLIST_1_URI + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_2_URI + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_3_URI + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n" + + MEDIA_PLAYLIST_0_URI) + .getBytes(Charset.forName(C.UTF8_NAME)); + + byte[] MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST") + .getBytes(Charset.forName(C.UTF8_NAME)); + + String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8"; + + byte[] ENC_MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST") + .getBytes(Charset.forName(C.UTF8_NAME)); +} diff --git a/library/hls/src/androidTest/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 similarity index 77% rename from library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java rename to library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 13083dc01c..9d8d3d28ee 100644 --- a/library/hls/src/androidTest/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 @@ -33,7 +33,6 @@ import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedDa import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -42,40 +41,47 @@ import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Util; import java.io.File; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; /** Unit tests for {@link HlsDownloader}. */ -public class HlsDownloaderTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +public class HlsDownloaderTest { private SimpleCache cache; private File tempFolder; private FakeDataSet fakeDataSet; private HlsDownloader hlsDownloader; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); - fakeDataSet = new FakeDataSet() - .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) - .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) - .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) - .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) - .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12) - .setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA) - .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13) - .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14) - .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15); + fakeDataSet = + new FakeDataSet() + .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) + .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12) + .setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15); hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testDownloadManifest() throws Exception { HlsMasterPlaylist manifest = hlsDownloader.getManifest(); @@ -83,17 +89,23 @@ public class HlsDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI); } + @Test public void testSelectRepresentationsClearsPreviousSelection() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI}); hlsDownloader.download(null); - assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_2_URI, + assertCachedData( + cache, + fakeDataSet, + MASTER_PLAYLIST_URI, + MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts"); } + @Test public void testCounterMethods() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.download(null); @@ -104,12 +116,12 @@ public class HlsDownloaderTest extends InstrumentationTestCase { .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); } + @Test public void testInitStatus() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.download(null); - HlsDownloader newHlsDownloader = - getHlsDownloader(MASTER_PLAYLIST_URI); + HlsDownloader newHlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); newHlsDownloader.init(); @@ -119,16 +131,22 @@ public class HlsDownloaderTest extends InstrumentationTestCase { .isEqualTo(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12); } + @Test public void testDownloadRepresentation() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.download(null); - assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI, + 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"); } + @Test public void testDownloadMultipleRepresentations() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); hlsDownloader.download(null); @@ -136,9 +154,11 @@ public class HlsDownloaderTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testDownloadAllRepresentations() throws Exception { // Add data for the rest of the playlists - fakeDataSet.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) + fakeDataSet + .setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11) .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12) @@ -162,6 +182,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase { hlsDownloader.remove(); } + @Test public void testRemoveAll() throws Exception { hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); hlsDownloader.download(null); @@ -170,27 +191,32 @@ public class HlsDownloaderTest extends InstrumentationTestCase { assertCacheEmpty(cache); } + @Test public void testDownloadMediaPlaylist() throws Exception { hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI); hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); hlsDownloader.download(null); - assertCachedData(cache, fakeDataSet, MEDIA_PLAYLIST_1_URI, + 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"); } + @Test public void testDownloadEncMediaPlaylist() throws Exception { - fakeDataSet = new FakeDataSet() - .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) - .setRandomData("enc.key", 8) - .setRandomData("enc2.key", 9) - .setRandomData("fileSequence0.ts", 10) - .setRandomData("fileSequence1.ts", 11) - .setRandomData("fileSequence2.ts", 12); - hlsDownloader = - getHlsDownloader(ENC_MEDIA_PLAYLIST_URI); + fakeDataSet = + new FakeDataSet() + .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) + .setRandomData("enc.key", 8) + .setRandomData("enc2.key", 9) + .setRandomData("fileSequence0.ts", 10) + .setRandomData("fileSequence1.ts", 11) + .setRandomData("fileSequence2.ts", 12); + hlsDownloader = getHlsDownloader(ENC_MEDIA_PLAYLIST_URI); hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI}); hlsDownloader.download(null); @@ -199,8 +225,7 @@ public class HlsDownloaderTest extends InstrumentationTestCase { private HlsDownloader getHlsDownloader(String mediaPlaylistUri) { Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); - return new HlsDownloader(Uri.parse(mediaPlaylistUri), - new DownloaderConstructorHelper(cache, factory)); + return new HlsDownloader( + Uri.parse(mediaPlaylistUri), new DownloaderConstructorHelper(cache, factory)); } - } diff --git a/library/hls/src/androidTest/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 similarity index 59% rename from library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java rename to library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 2944d01b57..86426e1f94 100644 --- a/library/hls/src/androidTest/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.net.Uri; import com.google.android.exoplayer2.C; @@ -26,70 +27,85 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Test for {@link HlsMasterPlaylistParserTest}. - */ -public class HlsMasterPlaylistParserTest extends TestCase { +/** Test for {@link HlsMasterPlaylistParserTest}. */ +@RunWith(RobolectricTestRunner.class) +public class HlsMasterPlaylistParserTest { private static final String PLAYLIST_URI = "https://example.com/test.m3u8"; - private static final String PLAYLIST_SIMPLE = " #EXTM3U \n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" - + "http://example.com/spaces_in_codecs.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" - + "http://example.com/mid.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" - + "http://example.com/hi.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" - + "http://example.com/audio-only.m3u8"; + private static final String PLAYLIST_SIMPLE = + " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" + + "http://example.com/hi.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + + "http://example.com/audio-only.m3u8"; - private static final String PLAYLIST_WITH_AVG_BANDWIDTH = " #EXTM3U \n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000," - + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" - + "http://example.com/spaces_in_codecs.m3u8\n"; + private static final String PLAYLIST_WITH_AVG_BANDWIDTH = + " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000," + + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n"; - private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITH_INVALID_HEADER = + "#EXTMU3\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; - private static final String PLAYLIST_WITH_CC = " #EXTM3U \n" - + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITH_CC = + " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," + + "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; - private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n" - + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," - + "CLOSED-CAPTIONS=NONE\n" - + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITHOUT_CC = + " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," + + "LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," + + "CLOSED-CAPTIONS=NONE\n" + + "http://example.com/low.m3u8\n"; - private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = "#EXTM3U\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n" - + "uri1.m3u8\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n" - + "uri2.m3u8\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n" - + "uri1.m3u8\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n" - + "uri2.m3u8\n" - + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\"," - + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n" - + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\"," - + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n"; + private static final String PLAYLIST_WITH_AUDIO_MEDIA_TAG = + "#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2227464,CODECS=\"avc1.640020,mp4a.40.2\",AUDIO=\"aud1\"\n" + + "uri1.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8178040,CODECS=\"avc1.64002a,mp4a.40.2\",AUDIO=\"aud1\"\n" + + "uri2.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2448841,CODECS=\"avc1.640020,ac-3\",AUDIO=\"aud2\"\n" + + "uri1.m3u8\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=8399417,CODECS=\"avc1.64002a,ac-3\",AUDIO=\"aud2\"\n" + + "uri2.m3u8\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"en\",NAME=\"English\"," + + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"2\",URI=\"a1/prog_index.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud2\",LANGUAGE=\"en\",NAME=\"English\"," + + "AUTOSELECT=YES,DEFAULT=YES,CHANNELS=\"6\",URI=\"a2/prog_index.m3u8\"\n"; + @Test public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -129,9 +145,10 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertThat(variants.get(4).url).isEqualTo("http://example.com/audio-only.m3u8"); } + @Test public void testMasterPlaylistWithBandwdithAverage() throws IOException { - HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, - PLAYLIST_WITH_AVG_BANDWIDTH); + HlsMasterPlaylist masterPlaylist = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AVG_BANDWIDTH); List variants = masterPlaylist.variants; @@ -139,6 +156,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertThat(variants.get(1).format.bitrate).isEqualTo(1270000); } + @Test public void testPlaylistWithInvalidHeader() throws IOException { try { parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); @@ -148,6 +166,7 @@ public class HlsMasterPlaylistParserTest extends TestCase { } } + @Test public void testPlaylistWithClosedCaption() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CC); assertThat(playlist.muxedCaptionFormats).hasSize(1); @@ -157,11 +176,13 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertThat(closedCaptionFormat.language).isEqualTo("es"); } + @Test public void testPlaylistWithoutClosedCaptions() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); assertThat(playlist.muxedCaptionFormats).isEmpty(); } + @Test public void testCodecPropagation() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_AUDIO_MEDIA_TAG); @@ -177,9 +198,8 @@ public class HlsMasterPlaylistParserTest extends TestCase { private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); - ByteArrayInputStream inputStream = new ByteArrayInputStream( - playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + ByteArrayInputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); } - } diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java similarity index 76% rename from library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java rename to library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java index 97a5386b04..b10997cfe9 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylistParserTest.java @@ -26,49 +26,53 @@ import java.io.InputStream; import java.nio.charset.Charset; import java.util.List; import java.util.Locale; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Test for {@link HlsMediaPlaylistParserTest}. - */ -public class HlsMediaPlaylistParserTest extends TestCase { +/** Test for {@link HlsMediaPlaylistParserTest}. */ +@RunWith(RobolectricTestRunner.class) +public class HlsMediaPlaylistParserTest { - public void testParseMediaPlaylist() throws IOException { + @Test + public void testParseMediaPlaylist() throws Exception { Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - String playlistString = "#EXTM3U\n" - + "#EXT-X-VERSION:3\n" - + "#EXT-X-PLAYLIST-TYPE:VOD\n" - + "#EXT-X-START:TIME-OFFSET=-25" - + "#EXT-X-TARGETDURATION:8\n" - + "#EXT-X-MEDIA-SEQUENCE:2679\n" - + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" - + "#EXT-X-ALLOW-CACHE:YES\n" - + "\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51370@0\n" - + "https://priv.example.com/fileSequence2679.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51501@2147483648\n" - + "https://priv.example.com/fileSequence2680.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=NONE\n" - + "#EXTINF:7.941,\n" - + "#EXT-X-BYTERANGE:51501\n" // @2147535149 - + "https://priv.example.com/fileSequence2681.ts\n" - + "\n" - + "#EXT-X-DISCONTINUITY\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51740\n" // @2147586650 - + "https://priv.example.com/fileSequence2682.ts\n" - + "\n" - + "#EXTINF:7.975,\n" - + "https://priv.example.com/fileSequence2683.ts\n" - + "#EXT-X-ENDLIST"; - InputStream inputStream = new ByteArrayInputStream( - playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + String playlistString = + "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + + "#EXT-X-TARGETDURATION:8\n" + + "#EXT-X-MEDIA-SEQUENCE:2679\n" + + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + + "#EXT-X-ALLOW-CACHE:YES\n" + + "\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51370@0\n" + + "https://priv.example.com/fileSequence2679.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=AES-128," + + "URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51501@2147483648\n" + + "https://priv.example.com/fileSequence2680.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:7.941,\n" + + "#EXT-X-BYTERANGE:51501\n" // @2147535149 + + "https://priv.example.com/fileSequence2681.ts\n" + + "\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51740\n" // @2147586650 + + "https://priv.example.com/fileSequence2682.ts\n" + + "\n" + + "#EXTINF:7.975,\n" + + "https://priv.example.com/fileSequence2683.ts\n" + + "#EXT-X-ENDLIST"; + InputStream inputStream = + new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; @@ -136,6 +140,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts"); } + @Test public void testGapTag() throws IOException { Uri playlistUri = Uri.parse("https://example.com/test2.m3u8"); String playlistString = @@ -170,5 +175,4 @@ public class HlsMediaPlaylistParserTest extends TestCase { assertThat(playlist.segments.get(2).hasGapTag).isTrue(); assertThat(playlist.segments.get(3).hasGapTag).isFalse(); } - } diff --git a/library/hls/src/test/resources/robolectric.properties b/library/hls/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/library/hls/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index ee5a8c4e73..b85f25e656 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -35,10 +35,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile project(modulePrefix + 'testutils') - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/smoothstreaming/src/androidTest/AndroidManifest.xml b/library/smoothstreaming/src/test/AndroidManifest.xml similarity index 70% rename from library/smoothstreaming/src/androidTest/AndroidManifest.xml rename to library/smoothstreaming/src/test/AndroidManifest.xml index 8e1e69509d..1a8f8ee9c4 100644 --- a/library/smoothstreaming/src/androidTest/AndroidManifest.xml +++ b/library/smoothstreaming/src/test/AndroidManifest.xml @@ -18,16 +18,6 @@ xmlns:tools="http://schemas.android.com/tools" package="com.google.android.exoplayer2.source.smoothstreaming.test"> - - - - - - - + diff --git a/library/smoothstreaming/src/androidTest/assets/sample_ismc_1 b/library/smoothstreaming/src/test/assets/sample_ismc_1 similarity index 100% rename from library/smoothstreaming/src/androidTest/assets/sample_ismc_1 rename to library/smoothstreaming/src/test/assets/sample_ismc_1 diff --git a/library/smoothstreaming/src/androidTest/assets/sample_ismc_2 b/library/smoothstreaming/src/test/assets/sample_ismc_2 similarity index 100% rename from library/smoothstreaming/src/androidTest/assets/sample_ismc_2 rename to library/smoothstreaming/src/test/assets/sample_ismc_2 diff --git a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java similarity index 60% rename from library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java rename to library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java index 4663f014ff..2ce9fec970 100644 --- a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParserTest.java @@ -16,27 +16,29 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; -/** - * Unit tests for {@link SsManifestParser}. - */ -public final class SsManifestParserTest extends InstrumentationTestCase { +/** Unit tests for {@link SsManifestParser}. */ +@RunWith(RobolectricTestRunner.class) +public final class SsManifestParserTest { private static final String SAMPLE_ISMC_1 = "sample_ismc_1"; private static final String SAMPLE_ISMC_2 = "sample_ismc_2"; - /** - * Simple test to ensure the sample manifests parse without any exceptions being thrown. - */ + /** Simple test to ensure the sample manifests parse without any exceptions being thrown. */ + @Test public void testParseSmoothStreamingManifest() throws IOException { SsManifestParser parser = new SsManifestParser(); - parser.parse(Uri.parse("https://example.com/test.ismc"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_1)); - parser.parse(Uri.parse("https://example.com/test.ismc"), - TestUtil.getInputStream(getInstrumentation(), SAMPLE_ISMC_2)); + parser.parse( + Uri.parse("https://example.com/test.ismc"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_ISMC_1)); + parser.parse( + Uri.parse("https://example.com/test.ismc"), + TestUtil.getInputStream(RuntimeEnvironment.application, SAMPLE_ISMC_2)); } - } diff --git a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java similarity index 77% rename from library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java rename to library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index ecf9e77d28..fbb2c3d4c4 100644 --- a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -26,52 +26,49 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; -/** - * Unit tests for {@link SsManifest}. - */ -public class SsManifestTest extends TestCase { +/** Unit tests for {@link SsManifest}. */ +@RunWith(RobolectricTestRunner.class) +public class SsManifestTest { private static final ProtectionElement DUMMY_PROTECTION_ELEMENT = new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2}); + @Test public void testCopy() throws Exception { Format[][] formats = newFormats(2, 3); - SsManifest sourceManifest = newSsManifest( - newStreamElement("1",formats[0]), - newStreamElement("2", formats[1])); + SsManifest sourceManifest = + newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList( - new TrackKey(0, 0), - new TrackKey(0, 2), - new TrackKey(1, 0)); + List keys = Arrays.asList(new TrackKey(0, 0), new TrackKey(0, 2), new TrackKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); SsManifest copyManifest = sourceManifest.copy(keys); - SsManifest expectedManifest = newSsManifest( - newStreamElement("1", formats[0][0], formats[0][2]), - newStreamElement("2", formats[1][0])); + SsManifest expectedManifest = + newSsManifest( + newStreamElement("1", formats[0][0], formats[0][2]), + newStreamElement("2", formats[1][0])); assertManifestEquals(expectedManifest, copyManifest); } + @Test public void testCopyRemoveStreamElement() throws Exception { Format[][] formats = newFormats(2, 3); - SsManifest sourceManifest = newSsManifest( - newStreamElement("1", formats[0]), - newStreamElement("2", formats[1])); + SsManifest sourceManifest = + newSsManifest(newStreamElement("1", formats[0]), newStreamElement("2", formats[1])); - List keys = Arrays.asList( - new TrackKey(1, 0)); + List keys = Arrays.asList(new TrackKey(1, 0)); // Keys don't need to be in any particular order Collections.shuffle(keys, new Random(0)); SsManifest copyManifest = sourceManifest.copy(keys); - SsManifest expectedManifest = newSsManifest( - newStreamElement("2", formats[1][0])); + SsManifest expectedManifest = newSsManifest(newStreamElement("2", formats[1][0])); assertManifestEquals(expectedManifest, copyManifest); } @@ -117,13 +114,25 @@ public class SsManifestTest extends TestCase { } private static StreamElement newStreamElement(String name, Format... formats) { - return new StreamElement("baseUri", "chunkTemplate", C.TRACK_TYPE_VIDEO, "subType", - 1000, name, 1024, 768, 1024, 768, null, formats, Collections.emptyList(), 0); + return new StreamElement( + "baseUri", + "chunkTemplate", + C.TRACK_TYPE_VIDEO, + "subType", + 1000, + name, + 1024, + 768, + 1024, + 768, + null, + formats, + Collections.emptyList(), + 0); } private static Format newFormat(String id) { - return Format.createContainerFormat(id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, - Format.NO_VALUE, 0, null); + return Format.createContainerFormat( + id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, 0, null); } - } diff --git a/library/smoothstreaming/src/test/resources/robolectric.properties b/library/smoothstreaming/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/library/smoothstreaming/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/testutils/build.gradle b/testutils/build.gradle index 7c4b163713..11ec55c047 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -35,4 +35,5 @@ dependencies { compile project(modulePrefix + 'library-core') compile 'org.mockito:mockito-core:' + mockitoVersion compile 'com.google.truth:truth:' + truthVersion + testCompile project(modulePrefix + 'testutils-robolectric') } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java deleted file mode 100644 index 6bd1048bc0..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java +++ /dev/null @@ -1,54 +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.testutil; - -import android.content.Context; -import android.test.InstrumentationTestCase; -import org.mockito.MockitoAnnotations; - -/** - * Utility for setting up Mockito for instrumentation tests. - */ -public final class MockitoUtil { - - /** - * Sets up Mockito for an instrumentation test. - * - * @param instrumentationTestCase The instrumentation test case class. - */ - public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - - /** - * Sets up Mockito for a JUnit4 test. - * - * @param targetContext The target context. Usually obtained from - * {@code InstrumentationRegistry.getTargetContext()} - * @param testClass The JUnit4 test class. - */ - public static void setUpMockito(Context targetContext, Object testClass) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath()); - MockitoAnnotations.initMocks(testClass); - } - - private MockitoUtil() {} - -} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java deleted file mode 100644 index 7cae709438..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java +++ /dev/null @@ -1,1073 +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.testutil; - -/** - * Provides ogg/vorbis test data in bytes for unit tests. - */ -public final class OggTestData { - - public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { - return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build(); - } - - public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, - int pageSegmentCount) { - return TestUtil.createByteArray( - 0x4F, 0x67, 0x67, 0x53, // Oggs. - 0x00, // Stream revision. - headerType, - (int) (granule) & 0xFF, - (int) (granule >> 8) & 0xFF, - (int) (granule >> 16) & 0xFF, - (int) (granule >> 24) & 0xFF, - (int) (granule >> 32) & 0xFF, - (int) (granule >> 40) & 0xFF, - (int) (granule >> 48) & 0xFF, - (int) (granule >> 56) & 0xFF, - 0x00, // LSB of data serial number. - 0x10, - 0x00, - 0x00, // MSB of data serial number. - (pageSequenceCounter) & 0xFF, - (pageSequenceCounter >> 8) & 0xFF, - (pageSequenceCounter >> 16) & 0xFF, - (pageSequenceCounter >> 24) & 0xFF, - 0x00, // LSB of page checksum. - 0x00, - 0x10, - 0x00, // MSB of page checksum. - pageSegmentCount); - } - - /** - * Returns the initial two pages of bytes which by spec contain the three vorbis header packets: - * identification, comment and setup header. - */ - public static byte[] getVorbisHeaderPages() { - byte[] data = new byte[VORBIS_HEADER_PAGES.length]; - System.arraycopy(VORBIS_HEADER_PAGES, 0, data, 0, - VORBIS_HEADER_PAGES.length); - return data; - } - - /** - * Returns a valid vorbis identification header in bytes. - */ - public static byte[] getIdentificationHeaderData() { - int idHeaderStart = 28; - int idHeaderLength = 30; - byte[] idHeaderData = new byte[idHeaderLength]; - System.arraycopy(VORBIS_HEADER_PAGES, idHeaderStart, idHeaderData, 0, idHeaderLength); - return idHeaderData; - } - - /** - * Returns a valid vorbis comment header with 3 comments including utf8 chars in bytes. - */ - public static byte[] getCommentHeaderDataUTF8() { - byte[] commentHeaderData = new byte[COMMENT_HEADER_WITH_UTF8.length]; - System.arraycopy(COMMENT_HEADER_WITH_UTF8, 0, commentHeaderData, 0, - COMMENT_HEADER_WITH_UTF8.length); - return commentHeaderData; - } - - /** - * Returns a valid vorbis setup header in bytes. - */ - public static byte[] getSetupHeaderData() { - int setupHeaderStart = 146; - int setupHeaderLength = VORBIS_HEADER_PAGES.length - setupHeaderStart; - byte[] setupHeaderData = new byte[setupHeaderLength]; - System.arraycopy(VORBIS_HEADER_PAGES, setupHeaderStart, setupHeaderData, 0, setupHeaderLength); - return setupHeaderData; - } - - private static final byte[] COMMENT_HEADER_WITH_UTF8 = { - (byte) 0x03, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 3, v, o, r, - (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x2b, // b, i, s, . - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x58, - (byte) 0x69, (byte) 0x70, (byte) 0x68, (byte) 0x2e, - (byte) 0x4f, (byte) 0x72, (byte) 0x67, (byte) 0x20, - (byte) 0x6c, (byte) 0x69, (byte) 0x62, (byte) 0x56, - (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69, - (byte) 0x73, (byte) 0x20, (byte) 0x49, (byte) 0x20, - (byte) 0x32, (byte) 0x30, (byte) 0x31, (byte) 0x32, - (byte) 0x30, (byte) 0x32, (byte) 0x30, (byte) 0x33, - (byte) 0x20, (byte) 0x28, (byte) 0x4f, (byte) 0x6d, - (byte) 0x6e, (byte) 0x69, (byte) 0x70, (byte) 0x72, - (byte) 0x65, (byte) 0x73, (byte) 0x65, (byte) 0x6e, - (byte) 0x74, (byte) 0x29, (byte) 0x03, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x4c, - (byte) 0x42, (byte) 0x55, (byte) 0x4d, (byte) 0x3d, - (byte) 0xc3, (byte) 0xa4, (byte) 0xc3, (byte) 0xb6, - (byte) 0x13, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x54, (byte) 0x49, (byte) 0x54, (byte) 0x4c, - (byte) 0x45, (byte) 0x3d, (byte) 0x41, (byte) 0x20, - (byte) 0x73, (byte) 0x61, (byte) 0x6d, (byte) 0x70, - (byte) 0x6c, (byte) 0x65, (byte) 0x20, (byte) 0x73, - (byte) 0x6f, (byte) 0x6e, (byte) 0x67, (byte) 0x0d, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, - (byte) 0x52, (byte) 0x54, (byte) 0x49, (byte) 0x53, - (byte) 0x54, (byte) 0x3d, (byte) 0x47, (byte) 0x6f, - (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, - (byte) 0x01 - }; - - // two OGG pages with 3 packets (id, comment and setup header) - // length: 3743 bytes - private static final byte[] VORBIS_HEADER_PAGES = { /* capture pattern ogg header 1 */ - (byte) 0x4f, (byte) 0x67, (byte) 0x67, (byte) 0x53, // O,g,g,S : start pos 0 - (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x5e, (byte) 0x5f, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x83, (byte) 0x36, - (byte) 0xe3, (byte) 0x49, (byte) 0x01, (byte) 0x1e, /* capture pattern vorbis id header */ - (byte) 0x01, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 1,v,o,r : start pos 28 - (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x00, // b,i,s,. - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, - (byte) 0x22, (byte) 0x56, (byte) 0x00, (byte) 0x00, - (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, - (byte) 0x6a, (byte) 0x04, (byte) 0x01, (byte) 0x00, - (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern ogg header 2 */ - (byte) 0xa9, (byte) 0x01, (byte) 0x4f, (byte) 0x67, // .,.,O,g : start pos 86 - (byte) 0x67, (byte) 0x53, (byte) 0x00, (byte) 0x00, // g,S,.,. - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x5e, (byte) 0x5f, (byte) 0x00, (byte) 0x00, - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x69, (byte) 0xf8, (byte) 0xeb, (byte) 0xe1, - (byte) 0x10, (byte) 0x2d, (byte) 0xff, (byte) 0xff, - (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, - (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, - (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern vorbis comment header*/ - (byte) 0x1b, (byte) 0x03, (byte) 0x76, (byte) 0x6f, // .,3,v,o : start pos 101 - (byte) 0x72, (byte) 0x62, (byte) 0x69, (byte) 0x73, // r,b,i,s - (byte) 0x1d, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x58, (byte) 0x69, (byte) 0x70, (byte) 0x68, - (byte) 0x2e, (byte) 0x4f, (byte) 0x72, (byte) 0x67, - (byte) 0x20, (byte) 0x6c, (byte) 0x69, (byte) 0x62, - (byte) 0x56, (byte) 0x6f, (byte) 0x72, (byte) 0x62, - (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x49, - (byte) 0x20, (byte) 0x32, (byte) 0x30, (byte) 0x30, - (byte) 0x33, (byte) 0x30, (byte) 0x39, (byte) 0x30, - (byte) 0x39, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* capture pattern vorbis setup header */ - (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x76, // .,.,5,v : start pos 146 - (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69, // o,r,b,i - (byte) 0x73, (byte) 0x22, (byte) 0x42, (byte) 0x43, // s,. - (byte) 0x56, (byte) 0x01, (byte) 0x00, (byte) 0x40, - (byte) 0x00, (byte) 0x00, (byte) 0x18, (byte) 0x42, - (byte) 0x10, (byte) 0x2a, (byte) 0x05, (byte) 0xad, - (byte) 0x63, (byte) 0x8e, (byte) 0x3a, (byte) 0xc8, - (byte) 0x15, (byte) 0x21, (byte) 0x8c, (byte) 0x19, - (byte) 0xa2, (byte) 0xa0, (byte) 0x42, (byte) 0xca, - (byte) 0x29, (byte) 0xc7, (byte) 0x1d, (byte) 0x42, - (byte) 0xd0, (byte) 0x21, (byte) 0xa3, (byte) 0x24, - (byte) 0x43, (byte) 0x88, (byte) 0x3a, (byte) 0xc6, - (byte) 0x35, (byte) 0xc7, (byte) 0x18, (byte) 0x63, - (byte) 0x47, (byte) 0xb9, (byte) 0x64, (byte) 0x8a, - (byte) 0x42, (byte) 0xc9, (byte) 0x81, (byte) 0xd0, - (byte) 0x90, (byte) 0x55, (byte) 0x00, (byte) 0x00, - (byte) 0x40, (byte) 0x00, (byte) 0x00, (byte) 0xa4, - (byte) 0x1c, (byte) 0x57, (byte) 0x50, (byte) 0x72, - (byte) 0x49, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0xa3, (byte) 0x18, (byte) 0x57, - (byte) 0xcc, (byte) 0x71, (byte) 0xe8, (byte) 0x20, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe5, - (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71, - (byte) 0x09, (byte) 0x25, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0x8e, (byte) 0x39, (byte) 0xe7, - (byte) 0x92, (byte) 0x72, (byte) 0x8e, (byte) 0x31, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa3, - (byte) 0x18, (byte) 0x57, (byte) 0x0e, (byte) 0x72, - (byte) 0x29, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0x81, (byte) 0x14, (byte) 0x47, - (byte) 0x8a, (byte) 0x71, (byte) 0xa7, (byte) 0x18, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa4, - (byte) 0x1c, (byte) 0x47, (byte) 0x8a, (byte) 0x71, - (byte) 0xa8, (byte) 0x18, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0x6d, (byte) 0x31, (byte) 0xb7, - (byte) 0x92, (byte) 0x72, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe6, - (byte) 0x20, (byte) 0x87, (byte) 0x52, (byte) 0x72, - (byte) 0xae, (byte) 0x35, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0xa4, (byte) 0x18, (byte) 0x67, - (byte) 0x0e, (byte) 0x72, (byte) 0x0b, (byte) 0x25, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xc6, - (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71, - (byte) 0xeb, (byte) 0x20, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0x8c, (byte) 0x35, (byte) 0xb7, - (byte) 0xd4, (byte) 0x72, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, - (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, - (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0x8c, (byte) 0x31, (byte) 0xe7, - (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x6e, - (byte) 0x31, (byte) 0xe7, (byte) 0x16, (byte) 0x73, - (byte) 0xae, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, - (byte) 0x1c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x20, - (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00, - (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0xa0, - (byte) 0xa1, (byte) 0x28, (byte) 0x8a, (byte) 0xe2, - (byte) 0x28, (byte) 0x0e, (byte) 0x10, (byte) 0x1a, - (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0xc8, - (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x40, - (byte) 0x71, (byte) 0x14, (byte) 0x47, (byte) 0x91, - (byte) 0x14, (byte) 0x4b, (byte) 0xb1, (byte) 0x1c, - (byte) 0xcb, (byte) 0xd1, (byte) 0x24, (byte) 0x0d, - (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05, - (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, - (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0xa0, - (byte) 0x48, (byte) 0x86, (byte) 0xa4, (byte) 0x48, - (byte) 0x8a, (byte) 0xa5, (byte) 0x58, (byte) 0x8e, - (byte) 0x66, (byte) 0x69, (byte) 0x9e, (byte) 0x26, - (byte) 0x7a, (byte) 0xa2, (byte) 0x28, (byte) 0x9a, - (byte) 0xa2, (byte) 0x2a, (byte) 0xab, (byte) 0xb2, - (byte) 0x69, (byte) 0xca, (byte) 0xb2, (byte) 0x2c, - (byte) 0xcb, (byte) 0xb2, (byte) 0xeb, (byte) 0xba, - (byte) 0x2e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2, - (byte) 0x0a, (byte) 0x00, (byte) 0x48, (byte) 0x00, - (byte) 0x00, (byte) 0x50, (byte) 0x51, (byte) 0x14, - (byte) 0xc5, (byte) 0x70, (byte) 0x14, (byte) 0x07, - (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05, - (byte) 0x00, (byte) 0x64, (byte) 0x00, (byte) 0x00, - (byte) 0x08, (byte) 0x60, (byte) 0x28, (byte) 0x8a, - (byte) 0xa3, (byte) 0x38, (byte) 0x8e, (byte) 0xe4, - (byte) 0x58, (byte) 0x92, (byte) 0xa5, (byte) 0x59, - (byte) 0x9e, (byte) 0x07, (byte) 0x84, (byte) 0x86, - (byte) 0xac, (byte) 0x02, (byte) 0x00, (byte) 0x80, - (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x00, - (byte) 0x00, (byte) 0x50, (byte) 0x0c, (byte) 0x47, - (byte) 0xb1, (byte) 0x14, (byte) 0x4d, (byte) 0xf1, - (byte) 0x24, (byte) 0xcf, (byte) 0xf2, (byte) 0x3c, - (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, - (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, (byte) 0xf3, - (byte) 0x3c, (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, - (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, - (byte) 0xf3, (byte) 0x3c, (byte) 0x0d, (byte) 0x08, - (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00, - (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x82, (byte) 0x28, (byte) 0x64, (byte) 0x18, - (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56, - (byte) 0x01, (byte) 0x00, (byte) 0x40, (byte) 0x00, - (byte) 0x00, (byte) 0x08, (byte) 0x21, (byte) 0x1a, - (byte) 0x19, (byte) 0x43, (byte) 0x9d, (byte) 0x52, - (byte) 0x12, (byte) 0x5c, (byte) 0x0a, (byte) 0x16, - (byte) 0x42, (byte) 0x1c, (byte) 0x11, (byte) 0x43, - (byte) 0x1d, (byte) 0x42, (byte) 0xce, (byte) 0x43, - (byte) 0xa9, (byte) 0xa5, (byte) 0x83, (byte) 0xe0, - (byte) 0x29, (byte) 0x85, (byte) 0x25, (byte) 0x63, - (byte) 0xd2, (byte) 0x53, (byte) 0xac, (byte) 0x41, - (byte) 0x08, (byte) 0x21, (byte) 0x7c, (byte) 0xef, - (byte) 0x3d, (byte) 0xf7, (byte) 0xde, (byte) 0x7b, - (byte) 0xef, (byte) 0x81, (byte) 0xd0, (byte) 0x90, - (byte) 0x55, (byte) 0x00, (byte) 0x00, (byte) 0x10, - (byte) 0x00, (byte) 0x00, (byte) 0x61, (byte) 0x14, - (byte) 0x38, (byte) 0x88, (byte) 0x81, (byte) 0xc7, - (byte) 0x24, (byte) 0x08, (byte) 0x21, (byte) 0x84, - (byte) 0x62, (byte) 0x14, (byte) 0x27, (byte) 0x44, - (byte) 0x71, (byte) 0xa6, (byte) 0x20, (byte) 0x08, - (byte) 0x21, (byte) 0x84, (byte) 0xe5, (byte) 0x24, - (byte) 0x58, (byte) 0xca, (byte) 0x79, (byte) 0xe8, - (byte) 0x24, (byte) 0x08, (byte) 0xdd, (byte) 0x83, - (byte) 0x10, (byte) 0x42, (byte) 0xb8, (byte) 0x9c, - (byte) 0x7b, (byte) 0xcb, (byte) 0xb9, (byte) 0xf7, - (byte) 0xde, (byte) 0x7b, (byte) 0x20, (byte) 0x34, - (byte) 0x64, (byte) 0x15, (byte) 0x00, (byte) 0x00, - (byte) 0x08, (byte) 0x00, (byte) 0xc0, (byte) 0x20, - (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, - (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, - (byte) 0x08, (byte) 0x29, (byte) 0xa4, (byte) 0x94, - (byte) 0x52, (byte) 0x48, (byte) 0x29, (byte) 0xa6, - (byte) 0x98, (byte) 0x62, (byte) 0x8a, (byte) 0x29, - (byte) 0xc7, (byte) 0x1c, (byte) 0x73, (byte) 0xcc, - (byte) 0x31, (byte) 0xc7, (byte) 0x20, (byte) 0x83, - (byte) 0x0c, (byte) 0x32, (byte) 0xe8, (byte) 0xa0, - (byte) 0x93, (byte) 0x4e, (byte) 0x3a, (byte) 0xc9, - (byte) 0xa4, (byte) 0x92, (byte) 0x4e, (byte) 0x3a, - (byte) 0xca, (byte) 0x24, (byte) 0xa3, (byte) 0x8e, - (byte) 0x52, (byte) 0x6b, (byte) 0x29, (byte) 0xb5, - (byte) 0x14, (byte) 0x53, (byte) 0x4c, (byte) 0xb1, - (byte) 0xe5, (byte) 0x16, (byte) 0x63, (byte) 0xad, - (byte) 0xb5, (byte) 0xd6, (byte) 0x9c, (byte) 0x73, - (byte) 0xaf, (byte) 0x41, (byte) 0x29, (byte) 0x63, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, - (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x23, (byte) 0x08, - (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00, - (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x10, - (byte) 0x06, (byte) 0x19, (byte) 0x64, (byte) 0x90, - (byte) 0x41, (byte) 0x08, (byte) 0x21, (byte) 0x84, - (byte) 0x14, (byte) 0x52, (byte) 0x48, (byte) 0x29, - (byte) 0xa6, (byte) 0x98, (byte) 0x72, (byte) 0xcc, - (byte) 0x31, (byte) 0xc7, (byte) 0x1c, (byte) 0x03, - (byte) 0x42, (byte) 0x43, (byte) 0x56, (byte) 0x01, - (byte) 0x00, (byte) 0x80, (byte) 0x00, (byte) 0x00, - (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x1c, (byte) 0x45, (byte) 0x52, (byte) 0x24, - (byte) 0x47, (byte) 0x72, (byte) 0x24, (byte) 0x47, - (byte) 0x92, (byte) 0x24, (byte) 0xc9, (byte) 0x92, - (byte) 0x2c, (byte) 0x49, (byte) 0x93, (byte) 0x3c, - (byte) 0xcb, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb, - (byte) 0xb3, (byte) 0x3c, (byte) 0x4d, (byte) 0xd4, - (byte) 0x44, (byte) 0x4d, (byte) 0x15, (byte) 0x55, - (byte) 0xd5, (byte) 0x55, (byte) 0x6d, (byte) 0xd7, - (byte) 0xf6, (byte) 0x6d, (byte) 0x5f, (byte) 0xf6, - (byte) 0x6d, (byte) 0xdf, (byte) 0xd5, (byte) 0x65, - (byte) 0xdf, (byte) 0xf6, (byte) 0x65, (byte) 0xdb, - (byte) 0xd5, (byte) 0x65, (byte) 0x5d, (byte) 0x96, - (byte) 0x65, (byte) 0xdd, (byte) 0xb5, (byte) 0x6d, - (byte) 0x5d, (byte) 0xd6, (byte) 0x5d, (byte) 0x5d, - (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7, - (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x75, - (byte) 0x5d, (byte) 0xd7, (byte) 0x75, (byte) 0x5d, - (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7, - (byte) 0x81, (byte) 0xd0, (byte) 0x90, (byte) 0x55, - (byte) 0x00, (byte) 0x80, (byte) 0x04, (byte) 0x00, - (byte) 0x80, (byte) 0x8e, (byte) 0xe4, (byte) 0x38, - (byte) 0x8e, (byte) 0xe4, (byte) 0x38, (byte) 0x8e, - (byte) 0xe4, (byte) 0x48, (byte) 0x8e, (byte) 0xa4, - (byte) 0x48, (byte) 0x0a, (byte) 0x10, (byte) 0x1a, - (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0x90, - (byte) 0x01, (byte) 0x00, (byte) 0x10, (byte) 0x00, - (byte) 0x80, (byte) 0xa3, (byte) 0x38, (byte) 0x8a, - (byte) 0xe3, (byte) 0x48, (byte) 0x8e, (byte) 0xe4, - (byte) 0x58, (byte) 0x8e, (byte) 0x25, (byte) 0x59, - (byte) 0x92, (byte) 0x26, (byte) 0x69, (byte) 0x96, - (byte) 0x67, (byte) 0x79, (byte) 0x96, (byte) 0xa7, - (byte) 0x79, (byte) 0x9a, (byte) 0xa8, (byte) 0x89, - (byte) 0x1e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2, - (byte) 0x0a, (byte) 0x00, (byte) 0x00, (byte) 0x04, - (byte) 0x00, (byte) 0x10, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, - (byte) 0xa2, (byte) 0x28, (byte) 0x8a, (byte) 0xa3, - (byte) 0x38, (byte) 0x8e, (byte) 0x24, (byte) 0x59, - (byte) 0x96, (byte) 0xa6, (byte) 0x69, (byte) 0x9e, - (byte) 0xa7, (byte) 0x7a, (byte) 0xa2, (byte) 0x28, - (byte) 0x9a, (byte) 0xaa, (byte) 0xaa, (byte) 0x8a, - (byte) 0xa6, (byte) 0xa9, (byte) 0xaa, (byte) 0xaa, - (byte) 0x6a, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, - (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, - (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6, - (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, - (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, - (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6, - (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, - (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, - (byte) 0xa6, (byte) 0x69, (byte) 0x02, (byte) 0xa1, - (byte) 0x21, (byte) 0xab, (byte) 0x00, (byte) 0x00, - (byte) 0x09, (byte) 0x00, (byte) 0x00, (byte) 0x1d, - (byte) 0xc7, (byte) 0x71, (byte) 0x1c, (byte) 0x47, - (byte) 0x71, (byte) 0x1c, (byte) 0xc7, (byte) 0x71, - (byte) 0x24, (byte) 0x47, (byte) 0x92, (byte) 0x24, - (byte) 0x20, (byte) 0x34, (byte) 0x64, (byte) 0x15, - (byte) 0x00, (byte) 0x20, (byte) 0x03, (byte) 0x00, - (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x43, - (byte) 0x51, (byte) 0x1c, (byte) 0x45, (byte) 0x72, - (byte) 0x2c, (byte) 0xc7, (byte) 0x92, (byte) 0x34, - (byte) 0x4b, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb, - (byte) 0xd3, (byte) 0x44, (byte) 0xcf, (byte) 0xf4, - (byte) 0x5c, (byte) 0x51, (byte) 0x36, (byte) 0x75, - (byte) 0x53, (byte) 0x57, (byte) 0x6d, (byte) 0x20, - (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00, - (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x20, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0xc7, (byte) 0x73, - (byte) 0x3c, (byte) 0xc7, (byte) 0x73, (byte) 0x3c, - (byte) 0xc9, (byte) 0x93, (byte) 0x3c, (byte) 0xcb, - (byte) 0x73, (byte) 0x3c, (byte) 0xc7, (byte) 0x93, - (byte) 0x3c, (byte) 0x49, (byte) 0xd3, (byte) 0x34, - (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, - (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, - (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, - (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, - (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, - (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, - (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56, - (byte) 0x02, (byte) 0x00, (byte) 0x64, (byte) 0x00, - (byte) 0x00, (byte) 0x90, (byte) 0x02, (byte) 0xcf, - (byte) 0x42, (byte) 0x29, (byte) 0x2d, (byte) 0x46, - (byte) 0x02, (byte) 0x1c, (byte) 0x88, (byte) 0x98, - (byte) 0xa3, (byte) 0xd8, (byte) 0x7b, (byte) 0xef, - (byte) 0xbd, (byte) 0xf7, (byte) 0xde, (byte) 0x7b, - (byte) 0x65, (byte) 0x3c, (byte) 0x92, (byte) 0x88, - (byte) 0x49, (byte) 0xed, (byte) 0x31, (byte) 0xf4, - (byte) 0xd4, (byte) 0x31, (byte) 0x07, (byte) 0xb1, - (byte) 0x67, (byte) 0xc6, (byte) 0x23, (byte) 0x66, - (byte) 0x94, (byte) 0xa3, (byte) 0xd8, (byte) 0x29, - (byte) 0xcf, (byte) 0x1c, (byte) 0x42, (byte) 0x0c, - (byte) 0x62, (byte) 0xe8, (byte) 0x3c, (byte) 0x74, - (byte) 0x4a, (byte) 0x31, (byte) 0x88, (byte) 0x29, - (byte) 0xf5, (byte) 0x52, (byte) 0x32, (byte) 0xc6, - (byte) 0x20, (byte) 0xc6, (byte) 0xd8, (byte) 0x63, - (byte) 0x0c, (byte) 0x21, (byte) 0x94, (byte) 0x18, - (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x21, - (byte) 0x00, (byte) 0x84, (byte) 0x66, (byte) 0x00, - (byte) 0x18, (byte) 0x24, (byte) 0x09, (byte) 0x90, - (byte) 0x34, (byte) 0x0d, (byte) 0x90, (byte) 0x34, - (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x24, (byte) 0x4f, (byte) 0x03, (byte) 0x34, - (byte) 0x51, (byte) 0x04, (byte) 0x34, (byte) 0x4f, - (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x49, (byte) 0xf3, (byte) 0x00, (byte) 0x4d, - (byte) 0xf4, (byte) 0x00, (byte) 0x4d, (byte) 0x14, - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x90, (byte) 0x3c, (byte) 0x0d, (byte) 0xf0, - (byte) 0x44, (byte) 0x11, (byte) 0xd0, (byte) 0x44, - (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44, - (byte) 0x51, (byte) 0x05, (byte) 0x44, (byte) 0xd5, - (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0x4f, - (byte) 0x15, (byte) 0x01, (byte) 0xd1, (byte) 0x54, - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x90, (byte) 0x34, (byte) 0x0f, (byte) 0xd0, - (byte) 0x44, (byte) 0x11, (byte) 0xf0, (byte) 0x44, - (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44, - (byte) 0xd5, (byte) 0x04, (byte) 0x3c, (byte) 0x51, - (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0xd1, - (byte) 0x54, (byte) 0x01, (byte) 0x51, (byte) 0x15, - (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04, - (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x38, - (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x58, - (byte) 0x08, (byte) 0x85, (byte) 0x86, (byte) 0xac, - (byte) 0x08, (byte) 0x00, (byte) 0xe2, (byte) 0x04, - (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10, - (byte) 0x00, (byte) 0x00, (byte) 0x30, (byte) 0xe0, - (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x60, - (byte) 0x42, (byte) 0x19, (byte) 0x28, (byte) 0x34, - (byte) 0x64, (byte) 0x45, (byte) 0x00, (byte) 0x10, - (byte) 0x27, (byte) 0x00, (byte) 0x60, (byte) 0x70, - (byte) 0x1c, (byte) 0xcb, (byte) 0x02, (byte) 0x00, - (byte) 0x00, (byte) 0x47, (byte) 0x92, (byte) 0x34, - (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x1c, - (byte) 0x49, (byte) 0xd2, (byte) 0x34, (byte) 0x00, - (byte) 0x00, (byte) 0xd0, (byte) 0x34, (byte) 0x4d, - (byte) 0x14, (byte) 0x01, (byte) 0x00, (byte) 0xc0, - (byte) 0xd2, (byte) 0x34, (byte) 0x51, (byte) 0x04, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x30, - (byte) 0xe0, (byte) 0x00, (byte) 0x00, (byte) 0x10, - (byte) 0x60, (byte) 0x42, (byte) 0x19, (byte) 0x28, - (byte) 0x34, (byte) 0x64, (byte) 0x25, (byte) 0x00, - (byte) 0x10, (byte) 0x05, (byte) 0x00, (byte) 0x60, - (byte) 0x30, (byte) 0x14, (byte) 0x4d, (byte) 0x03, - (byte) 0x58, (byte) 0x16, (byte) 0xc0, (byte) 0xb2, - (byte) 0x00, (byte) 0x9a, (byte) 0x06, (byte) 0xd0, - (byte) 0x34, (byte) 0x80, (byte) 0xe7, (byte) 0x01, - (byte) 0x3c, (byte) 0x11, (byte) 0x60, (byte) 0x9a, - (byte) 0x00, (byte) 0x40, (byte) 0x00, (byte) 0x00, - (byte) 0x40, (byte) 0x81, (byte) 0x03, (byte) 0x00, - (byte) 0x40, (byte) 0x80, (byte) 0x0d, (byte) 0x9a, - (byte) 0x12, (byte) 0x8b, (byte) 0x03, (byte) 0x14, - (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, - (byte) 0x88, (byte) 0x02, (byte) 0x00, (byte) 0x30, - (byte) 0x28, (byte) 0x8a, (byte) 0x24, (byte) 0x59, - (byte) 0x96, (byte) 0xe7, (byte) 0x41, (byte) 0xd3, - (byte) 0x34, (byte) 0x4d, (byte) 0x14, (byte) 0xa1, - (byte) 0x69, (byte) 0x9a, (byte) 0x26, (byte) 0x8a, - (byte) 0xf0, (byte) 0x3c, (byte) 0xcf, (byte) 0x13, - (byte) 0x45, (byte) 0x78, (byte) 0x9e, (byte) 0xe7, - (byte) 0x99, (byte) 0x26, (byte) 0x44, (byte) 0xd1, - (byte) 0xf3, (byte) 0x4c, (byte) 0x13, (byte) 0xa2, - (byte) 0xe8, (byte) 0x79, (byte) 0xa6, (byte) 0x09, - (byte) 0xd3, (byte) 0x14, (byte) 0x45, (byte) 0xd3, - (byte) 0x04, (byte) 0xa2, (byte) 0x68, (byte) 0x9a, - (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x0a, - (byte) 0x1c, (byte) 0x00, (byte) 0x00, (byte) 0x02, - (byte) 0x6c, (byte) 0xd0, (byte) 0x94, (byte) 0x58, - (byte) 0x1c, (byte) 0xa0, (byte) 0xd0, (byte) 0x90, - (byte) 0x95, (byte) 0x00, (byte) 0x40, (byte) 0x48, - (byte) 0x00, (byte) 0x80, (byte) 0x41, (byte) 0x51, - (byte) 0x2c, (byte) 0xcb, (byte) 0xf3, (byte) 0x44, - (byte) 0x51, (byte) 0x14, (byte) 0x4d, (byte) 0x53, - (byte) 0x55, (byte) 0x5d, (byte) 0x17, (byte) 0x9a, - (byte) 0xe6, (byte) 0x79, (byte) 0xa2, (byte) 0x28, - (byte) 0x8a, (byte) 0xa6, (byte) 0xa9, (byte) 0xaa, - (byte) 0xae, (byte) 0x0b, (byte) 0x4d, (byte) 0xf3, - (byte) 0x3c, (byte) 0x51, (byte) 0x14, (byte) 0x45, - (byte) 0xd3, (byte) 0x54, (byte) 0x55, (byte) 0xd7, - (byte) 0x85, (byte) 0xe7, (byte) 0x79, (byte) 0xa2, - (byte) 0x29, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, - (byte) 0xaa, (byte) 0xaa, (byte) 0xeb, (byte) 0xc2, - (byte) 0xf3, (byte) 0x44, (byte) 0xd1, (byte) 0x34, - (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x55, - (byte) 0xd7, (byte) 0x75, (byte) 0xe1, (byte) 0x79, - (byte) 0xa2, (byte) 0x68, (byte) 0x9a, (byte) 0xa6, - (byte) 0xa9, (byte) 0xaa, (byte) 0xae, (byte) 0xeb, - (byte) 0xba, (byte) 0xf0, (byte) 0x3c, (byte) 0x51, - (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x54, - (byte) 0x55, (byte) 0xd7, (byte) 0x95, (byte) 0x65, - (byte) 0x88, (byte) 0xa2, (byte) 0x28, (byte) 0x9a, - (byte) 0xa6, (byte) 0x69, (byte) 0xaa, (byte) 0xaa, - (byte) 0xeb, (byte) 0xca, (byte) 0x32, (byte) 0x10, - (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x59, - (byte) 0x06, (byte) 0xa2, (byte) 0x68, (byte) 0x9a, - (byte) 0xaa, (byte) 0xea, (byte) 0xba, (byte) 0xae, - (byte) 0x2b, (byte) 0xcb, (byte) 0x40, (byte) 0x14, - (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x5d, - (byte) 0xd7, (byte) 0x75, (byte) 0x65, (byte) 0x19, - (byte) 0x98, (byte) 0xa6, (byte) 0x6a, (byte) 0xaa, - (byte) 0xaa, (byte) 0xeb, (byte) 0xca, (byte) 0xb2, - (byte) 0x2c, (byte) 0x03, (byte) 0x4c, (byte) 0x53, - (byte) 0x55, (byte) 0x5d, (byte) 0x57, (byte) 0x96, - (byte) 0x65, (byte) 0x19, (byte) 0xa0, (byte) 0xaa, - (byte) 0xae, (byte) 0xeb, (byte) 0xba, (byte) 0xb2, - (byte) 0x6c, (byte) 0xdb, (byte) 0x00, (byte) 0x55, - (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x95, - (byte) 0x65, (byte) 0xdb, (byte) 0x06, (byte) 0xb8, - (byte) 0xae, (byte) 0xeb, (byte) 0xca, (byte) 0xb2, - (byte) 0x2c, (byte) 0xdb, (byte) 0x36, (byte) 0x00, - (byte) 0xd7, (byte) 0x95, (byte) 0x65, (byte) 0x59, - (byte) 0xb6, (byte) 0x6d, (byte) 0x01, (byte) 0x00, - (byte) 0x00, (byte) 0x07, (byte) 0x0e, (byte) 0x00, - (byte) 0x00, (byte) 0x01, (byte) 0x46, (byte) 0xd0, - (byte) 0x49, (byte) 0x46, (byte) 0x95, (byte) 0x45, - (byte) 0xd8, (byte) 0x68, (byte) 0xc2, (byte) 0x85, - (byte) 0x07, (byte) 0xa0, (byte) 0xd0, (byte) 0x90, - (byte) 0x15, (byte) 0x01, (byte) 0x40, (byte) 0x14, - (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x8c, - (byte) 0x52, (byte) 0x8a, (byte) 0x29, (byte) 0x65, - (byte) 0x18, (byte) 0x93, (byte) 0x50, (byte) 0x4a, - (byte) 0x09, (byte) 0x0d, (byte) 0x63, (byte) 0x52, - (byte) 0x4a, (byte) 0x2a, (byte) 0xa5, (byte) 0x92, - (byte) 0x92, (byte) 0x52, (byte) 0x4a, (byte) 0xa5, - (byte) 0x54, (byte) 0x12, (byte) 0x52, (byte) 0x4a, - (byte) 0xa9, (byte) 0x94, (byte) 0x4a, (byte) 0x4a, - (byte) 0x4a, (byte) 0x29, (byte) 0x95, (byte) 0x92, - (byte) 0x51, (byte) 0x4a, (byte) 0x29, (byte) 0xb5, - (byte) 0x96, (byte) 0x2a, (byte) 0x29, (byte) 0xa9, - (byte) 0x94, (byte) 0x94, (byte) 0x52, (byte) 0x25, - (byte) 0xa5, (byte) 0xa4, (byte) 0x92, (byte) 0x52, - (byte) 0x2a, (byte) 0x00, (byte) 0x00, (byte) 0xec, - (byte) 0xc0, (byte) 0x01, (byte) 0x00, (byte) 0xec, - (byte) 0xc0, (byte) 0x42, (byte) 0x28, (byte) 0x34, - (byte) 0x64, (byte) 0x25, (byte) 0x00, (byte) 0x90, - (byte) 0x07, (byte) 0x00, (byte) 0x40, (byte) 0x10, - (byte) 0x82, (byte) 0x14, (byte) 0x63, (byte) 0x8c, - (byte) 0x39, (byte) 0x27, (byte) 0xa5, (byte) 0x54, - (byte) 0x8a, (byte) 0x31, (byte) 0xe7, (byte) 0x9c, - (byte) 0x93, (byte) 0x52, (byte) 0x2a, (byte) 0xc5, - (byte) 0x98, (byte) 0x73, (byte) 0xce, (byte) 0x49, - (byte) 0x29, (byte) 0x19, (byte) 0x63, (byte) 0xcc, - (byte) 0x39, (byte) 0xe7, (byte) 0xa4, (byte) 0x94, - (byte) 0x8c, (byte) 0x31, (byte) 0xe6, (byte) 0x9c, - (byte) 0x73, (byte) 0x52, (byte) 0x4a, (byte) 0xc6, - (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0x29, (byte) 0x25, (byte) 0x63, (byte) 0xce, - (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x94, - (byte) 0xd2, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, - (byte) 0x83, (byte) 0x50, (byte) 0x4a, (byte) 0x29, - (byte) 0xa5, (byte) 0x73, (byte) 0xce, (byte) 0x41, - (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x12, - (byte) 0x42, (byte) 0xe7, (byte) 0x20, (byte) 0x94, - (byte) 0x52, (byte) 0x4a, (byte) 0xe9, (byte) 0x9c, - (byte) 0x73, (byte) 0x10, (byte) 0x0a, (byte) 0x00, - (byte) 0x00, (byte) 0x2a, (byte) 0x70, (byte) 0x00, - (byte) 0x00, (byte) 0x08, (byte) 0xb0, (byte) 0x51, - (byte) 0x64, (byte) 0x73, (byte) 0x82, (byte) 0x91, - (byte) 0xa0, (byte) 0x42, (byte) 0x43, (byte) 0x56, - (byte) 0x02, (byte) 0x00, (byte) 0xa9, (byte) 0x00, - (byte) 0x00, (byte) 0x06, (byte) 0xc7, (byte) 0xb1, - (byte) 0x2c, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, - (byte) 0xcf, (byte) 0x13, (byte) 0x45, (byte) 0x4b, - (byte) 0x92, (byte) 0x34, (byte) 0xcf, (byte) 0x13, - (byte) 0x3d, (byte) 0x4f, (byte) 0x14, (byte) 0x4d, - (byte) 0xd5, (byte) 0x92, (byte) 0x24, (byte) 0xcf, - (byte) 0x13, (byte) 0x45, (byte) 0xcf, (byte) 0x13, - (byte) 0x4d, (byte) 0x53, (byte) 0xe5, (byte) 0x79, - (byte) 0x9e, (byte) 0x28, (byte) 0x8a, (byte) 0xa2, - (byte) 0x68, (byte) 0x9a, (byte) 0xaa, (byte) 0x4a, - (byte) 0x14, (byte) 0x45, (byte) 0x4f, (byte) 0x14, - (byte) 0x45, (byte) 0xd1, (byte) 0x34, (byte) 0x55, - (byte) 0x95, (byte) 0x2c, (byte) 0x8b, (byte) 0xa2, - (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0xaa, - (byte) 0xba, (byte) 0x2e, (byte) 0x5b, (byte) 0x16, - (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, - (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x98, - (byte) 0xa6, (byte) 0x28, (byte) 0xaa, (byte) 0xaa, - (byte) 0xeb, (byte) 0xca, (byte) 0x2e, (byte) 0x4c, - (byte) 0x53, (byte) 0x14, (byte) 0x4d, (byte) 0xd3, - (byte) 0x75, (byte) 0x65, (byte) 0x19, (byte) 0xb2, - (byte) 0xad, (byte) 0x9a, (byte) 0xaa, (byte) 0xea, - (byte) 0xba, (byte) 0xb2, (byte) 0x0d, (byte) 0xdb, - (byte) 0x36, (byte) 0x4d, (byte) 0x55, (byte) 0x75, - (byte) 0x5d, (byte) 0x59, (byte) 0x06, (byte) 0xae, - (byte) 0xeb, (byte) 0xba, (byte) 0xb2, (byte) 0x6c, - (byte) 0xeb, (byte) 0xc0, (byte) 0x75, (byte) 0x5d, - (byte) 0x57, (byte) 0x96, (byte) 0x6d, (byte) 0x5d, - (byte) 0x00, (byte) 0x00, (byte) 0x78, (byte) 0x82, - (byte) 0x03, (byte) 0x00, (byte) 0x50, (byte) 0x81, - (byte) 0x0d, (byte) 0xab, (byte) 0x23, (byte) 0x9c, - (byte) 0x14, (byte) 0x8d, (byte) 0x05, (byte) 0x16, - (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, - (byte) 0xc8, (byte) 0x00, (byte) 0x00, (byte) 0x20, - (byte) 0x08, (byte) 0x41, (byte) 0x48, (byte) 0x29, - (byte) 0x85, (byte) 0x90, (byte) 0x52, (byte) 0x0a, - (byte) 0x21, (byte) 0xa5, (byte) 0x14, (byte) 0x42, - (byte) 0x4a, (byte) 0x29, (byte) 0x84, (byte) 0x04, - (byte) 0x00, (byte) 0x00, (byte) 0x0c, (byte) 0x38, - (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x98, - (byte) 0x50, (byte) 0x06, (byte) 0x0a, (byte) 0x0d, - (byte) 0x59, (byte) 0x09, (byte) 0x00, (byte) 0xa4, - (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x10, - (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, - (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, - (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, - (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, - (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10, - (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, - (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, - (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, - (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, - (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10, - (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, - (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, - (byte) 0x84, (byte) 0xce, (byte) 0x39, (byte) 0xe7, - (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, - (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, - (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, - (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, - (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, - (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, - (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, - (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, - (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, - (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, - (byte) 0x02, (byte) 0x00, (byte) 0xb1, (byte) 0x2b, - (byte) 0x1c, (byte) 0x00, (byte) 0x76, (byte) 0x22, - (byte) 0x6c, (byte) 0x58, (byte) 0x1d, (byte) 0xe1, - (byte) 0xa4, (byte) 0x68, (byte) 0x2c, (byte) 0xb0, - (byte) 0xd0, (byte) 0x90, (byte) 0x95, (byte) 0x00, - (byte) 0x40, (byte) 0x38, (byte) 0x00, (byte) 0x00, - (byte) 0x60, (byte) 0x8c, (byte) 0x31, (byte) 0xce, - (byte) 0x59, (byte) 0xac, (byte) 0xb5, (byte) 0xd6, - (byte) 0x5a, (byte) 0x2b, (byte) 0xa5, (byte) 0x94, - (byte) 0x92, (byte) 0x50, (byte) 0x6b, (byte) 0xad, - (byte) 0xb5, (byte) 0xd6, (byte) 0x9a, (byte) 0x29, - (byte) 0xa4, (byte) 0x94, (byte) 0x84, (byte) 0x16, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, - (byte) 0x98, (byte) 0x31, (byte) 0x08, (byte) 0x29, - (byte) 0xb5, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0x31, (byte) 0xc6, (byte) 0x8c, (byte) 0x39, - (byte) 0x47, (byte) 0x2d, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xb6, - (byte) 0x56, (byte) 0x4a, (byte) 0x6c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0xb1, (byte) 0xb5, (byte) 0x52, (byte) 0x62, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, - (byte) 0x16, (byte) 0x5b, (byte) 0x8c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, - (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, - (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, - (byte) 0x63, (byte) 0x6c, (byte) 0x31, (byte) 0xc6, - (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, - (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, - (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63, - (byte) 0x2c, (byte) 0x00, (byte) 0xc0, (byte) 0xe4, - (byte) 0xc1, (byte) 0x01, (byte) 0x00, (byte) 0x2a, - (byte) 0xc1, (byte) 0xc6, (byte) 0x19, (byte) 0x56, - (byte) 0x92, (byte) 0xce, (byte) 0x0a, (byte) 0x47, - (byte) 0x83, (byte) 0x0b, (byte) 0x0d, (byte) 0x59, - (byte) 0x09, (byte) 0x00, (byte) 0xe4, (byte) 0x06, - (byte) 0x00, (byte) 0x00, (byte) 0xc6, (byte) 0x28, - (byte) 0xc5, (byte) 0x98, (byte) 0x63, (byte) 0xce, - (byte) 0x41, (byte) 0x08, (byte) 0xa1, (byte) 0x94, - (byte) 0x12, (byte) 0x4a, (byte) 0x49, (byte) 0xad, - (byte) 0x75, (byte) 0xce, (byte) 0x39, (byte) 0x08, - (byte) 0x21, (byte) 0x94, (byte) 0x52, (byte) 0x4a, - (byte) 0x49, (byte) 0xa9, (byte) 0xb4, (byte) 0x94, - (byte) 0x62, (byte) 0xca, (byte) 0x98, (byte) 0x73, - (byte) 0xce, (byte) 0x41, (byte) 0x08, (byte) 0xa5, - (byte) 0x94, (byte) 0x12, (byte) 0x4a, (byte) 0x49, - (byte) 0xa9, (byte) 0xa5, (byte) 0xd4, (byte) 0x39, - (byte) 0xe7, (byte) 0x20, (byte) 0x94, (byte) 0x52, - (byte) 0x4a, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, - (byte) 0x94, (byte) 0x5a, (byte) 0x6a, (byte) 0xad, - (byte) 0x73, (byte) 0x10, (byte) 0x42, (byte) 0x08, - (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4a, - (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0xd4, - (byte) 0x52, (byte) 0x08, (byte) 0x21, (byte) 0x94, - (byte) 0x52, (byte) 0x4a, (byte) 0x2a, (byte) 0x29, - (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, - (byte) 0xad, (byte) 0xa5, (byte) 0x10, (byte) 0x42, - (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x94, - (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, - (byte) 0xd4, (byte) 0x5a, (byte) 0x8b, (byte) 0xa1, - (byte) 0x94, (byte) 0x90, (byte) 0x4a, (byte) 0x29, - (byte) 0x25, (byte) 0xa5, (byte) 0x94, (byte) 0x52, - (byte) 0x49, (byte) 0x2d, (byte) 0xb5, (byte) 0x96, - (byte) 0x5a, (byte) 0x2a, (byte) 0xa1, (byte) 0x94, - (byte) 0x54, (byte) 0x52, (byte) 0x4a, (byte) 0x29, - (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, - (byte) 0xa9, (byte) 0xb5, (byte) 0x56, (byte) 0x4a, - (byte) 0x49, (byte) 0x25, (byte) 0xa5, (byte) 0x94, - (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, - (byte) 0xd4, (byte) 0x62, (byte) 0x6b, (byte) 0x29, - (byte) 0x94, (byte) 0x92, (byte) 0x52, (byte) 0x49, - (byte) 0x29, (byte) 0xb5, (byte) 0x94, (byte) 0x52, - (byte) 0x4a, (byte) 0xad, (byte) 0xc5, (byte) 0xd8, - (byte) 0x62, (byte) 0x29, (byte) 0xad, (byte) 0xa4, - (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0x29, - (byte) 0xa5, (byte) 0xd6, (byte) 0x52, (byte) 0x6c, - (byte) 0xad, (byte) 0xb5, (byte) 0xd8, (byte) 0x52, - (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0x96, - (byte) 0x5a, (byte) 0x4a, (byte) 0x29, (byte) 0xb5, - (byte) 0x16, (byte) 0x5b, (byte) 0x6a, (byte) 0x2d, - (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4b, - (byte) 0x29, (byte) 0xa5, (byte) 0x96, (byte) 0x52, - (byte) 0x4b, (byte) 0x2d, (byte) 0xc6, (byte) 0xd6, - (byte) 0x5a, (byte) 0x4b, (byte) 0x29, (byte) 0xa5, - (byte) 0xd4, (byte) 0x52, (byte) 0x6a, (byte) 0xa9, - (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6c, - (byte) 0xad, (byte) 0xb5, (byte) 0x98, (byte) 0x52, - (byte) 0x6a, (byte) 0x2d, (byte) 0xa5, (byte) 0xd4, - (byte) 0x52, (byte) 0x6b, (byte) 0x2d, (byte) 0xb5, - (byte) 0xd8, (byte) 0x52, (byte) 0x6a, (byte) 0x2d, - (byte) 0xb5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, - (byte) 0xa9, (byte) 0xa5, (byte) 0x94, (byte) 0x5a, - (byte) 0x6b, (byte) 0x2d, (byte) 0xb6, (byte) 0xd8, - (byte) 0x5a, (byte) 0x6b, (byte) 0x29, (byte) 0xb5, - (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0xa9, - (byte) 0xb5, (byte) 0x16, (byte) 0x5b, (byte) 0x8a, - (byte) 0xb1, (byte) 0xb5, (byte) 0xd4, (byte) 0x4a, - (byte) 0x4a, (byte) 0x29, (byte) 0xb5, (byte) 0xd4, - (byte) 0x5a, (byte) 0x6a, (byte) 0x2d, (byte) 0xb6, - (byte) 0x16, (byte) 0x5b, (byte) 0x6b, (byte) 0xad, - (byte) 0xa5, (byte) 0xd6, (byte) 0x5a, (byte) 0x6a, - (byte) 0x29, (byte) 0xa5, (byte) 0x16, (byte) 0x5b, - (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x16, - (byte) 0x63, (byte) 0x6b, (byte) 0x31, (byte) 0xa5, - (byte) 0x94, (byte) 0x52, (byte) 0x4b, (byte) 0xa9, - (byte) 0xa5, (byte) 0x02, (byte) 0x00, (byte) 0x80, - (byte) 0x0e, (byte) 0x1c, (byte) 0x00, (byte) 0x00, - (byte) 0x02, (byte) 0x8c, (byte) 0xa8, (byte) 0xb4, - (byte) 0x10, (byte) 0x3b, (byte) 0xcd, (byte) 0xb8, - (byte) 0xf2, (byte) 0x08, (byte) 0x1c, (byte) 0x51, - (byte) 0xc8, (byte) 0x30, (byte) 0x01, (byte) 0x15, - (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, - (byte) 0x20, (byte) 0x03, (byte) 0x00, (byte) 0x20, - (byte) 0x90, (byte) 0x69, (byte) 0x92, (byte) 0x39, - (byte) 0x49, (byte) 0xa9, (byte) 0x11, (byte) 0x26, - (byte) 0x39, (byte) 0xc5, (byte) 0xa0, (byte) 0x94, - (byte) 0xe6, (byte) 0x9c, (byte) 0x53, (byte) 0x4a, - (byte) 0x29, (byte) 0xa5, (byte) 0x34, (byte) 0x44, - (byte) 0x96, (byte) 0x64, (byte) 0x90, (byte) 0x62, - (byte) 0x50, (byte) 0x1d, (byte) 0x99, (byte) 0x8c, - (byte) 0x39, (byte) 0x49, (byte) 0x39, (byte) 0x43, - (byte) 0xa4, (byte) 0x31, (byte) 0xa4, (byte) 0x20, - (byte) 0xf5, (byte) 0x4c, (byte) 0x91, (byte) 0xc7, - (byte) 0x94, (byte) 0x62, (byte) 0x10, (byte) 0x43, - (byte) 0x48, (byte) 0x2a, (byte) 0x74, (byte) 0x8a, - (byte) 0x39, (byte) 0x6c, (byte) 0x35, (byte) 0xf9, - (byte) 0x58, (byte) 0x42, (byte) 0x07, (byte) 0xb1, - (byte) 0x06, (byte) 0x65, (byte) 0x8c, (byte) 0x70, - (byte) 0x29, (byte) 0xc5, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x08, (byte) 0x02, (byte) 0x00, - (byte) 0x04, (byte) 0x84, (byte) 0x04, (byte) 0x00, - (byte) 0x18, (byte) 0x20, (byte) 0x28, (byte) 0x98, - (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x07, - (byte) 0x08, (byte) 0x23, (byte) 0x07, (byte) 0x02, - (byte) 0x1d, (byte) 0x01, (byte) 0x04, (byte) 0x0e, - (byte) 0x6d, (byte) 0x00, (byte) 0x80, (byte) 0x81, - (byte) 0x08, (byte) 0x99, (byte) 0x09, (byte) 0x0c, - (byte) 0x0a, (byte) 0xa1, (byte) 0xc1, (byte) 0x41, - (byte) 0x26, (byte) 0x00, (byte) 0x3c, (byte) 0x40, - (byte) 0x44, (byte) 0x48, (byte) 0x05, (byte) 0x00, - (byte) 0x89, (byte) 0x09, (byte) 0x8a, (byte) 0xd2, - (byte) 0x85, (byte) 0x2e, (byte) 0x08, (byte) 0x21, - (byte) 0x82, (byte) 0x74, (byte) 0x11, (byte) 0x64, - (byte) 0xf1, (byte) 0xc0, (byte) 0x85, (byte) 0x13, - (byte) 0x37, (byte) 0x9e, (byte) 0xb8, (byte) 0xe1, - (byte) 0x84, (byte) 0x0e, (byte) 0x6d, (byte) 0x20, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0xf0, - (byte) 0x01, (byte) 0x00, (byte) 0x90, (byte) 0x50, - (byte) 0x00, (byte) 0x11, (byte) 0x11, (byte) 0xd1, - (byte) 0xcc, (byte) 0x55, (byte) 0x58, (byte) 0x5c, - (byte) 0x60, (byte) 0x64, (byte) 0x68, (byte) 0x6c, - (byte) 0x70, (byte) 0x74, (byte) 0x78, (byte) 0x7c, - (byte) 0x80, (byte) 0x84, (byte) 0x08, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x10, (byte) 0x00, (byte) 0x7c, (byte) 0x00, - (byte) 0x00, (byte) 0x24, (byte) 0x22, (byte) 0x40, - (byte) 0x44, (byte) 0x44, (byte) 0x34, (byte) 0x73, - (byte) 0x15, (byte) 0x16, (byte) 0x17, (byte) 0x18, - (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c, - (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20, - (byte) 0x21, (byte) 0x01, (byte) 0x00, (byte) 0x80, - (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x80, - (byte) 0x00, (byte) 0x04, (byte) 0x04, (byte) 0x04, - (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x04, (byte) 0x04 - }; - -} 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 fb50ef131b..ee17068242 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 @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.testutil; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; -import android.app.Instrumentation; import android.content.Context; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; @@ -132,20 +131,10 @@ public class TestUtil { return joined; } - public static byte[] getByteArray(Instrumentation instrumentation, String fileName) - throws IOException { - return getByteArray(instrumentation.getContext(), fileName); - } - public static byte[] getByteArray(Context context, String fileName) throws IOException { return Util.toByteArray(getInputStream(context, fileName)); } - public static InputStream getInputStream(Instrumentation instrumentation, String fileName) - throws IOException { - return getInputStream(instrumentation.getContext(), fileName); - } - public static InputStream getInputStream(Context context, String fileName) throws IOException { return context.getResources().getAssets().open(fileName); } diff --git a/testutils/src/test/AndroidManifest.xml b/testutils/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..9602d01633 --- /dev/null +++ b/testutils/src/test/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java new file mode 100644 index 0000000000..7fd84f6287 --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSetTest.java @@ -0,0 +1,147 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.List; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link FakeAdaptiveDataSet}. */ +@RunWith(RobolectricTestRunner.class) +public final class FakeAdaptiveDataSetTest { + + private static final Format[] TEST_FORMATS = { + Format.createVideoSampleFormat( + null, + MimeTypes.VIDEO_H264, + null, + 1000000, + Format.NO_VALUE, + 1280, + 720, + Format.NO_VALUE, + null, + null), + Format.createVideoSampleFormat( + null, + MimeTypes.VIDEO_H264, + null, + 300000, + Format.NO_VALUE, + 640, + 360, + Format.NO_VALUE, + null, + null) + }; + private static final TrackGroup TRACK_GROUP = new TrackGroup(TEST_FORMATS); + + @Test + public void testAdaptiveDataSet() { + long chunkDuration = 2 * C.MICROS_PER_SECOND; + FakeAdaptiveDataSet dataSet = + new FakeAdaptiveDataSet( + TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0)); + assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length); + assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse(); + assertThat(dataSet.getChunkCount()).isEqualTo(5); + assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(2); + assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(4); + for (int i = 0; i < dataSet.getChunkCount(); i++) { + assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration); + } + assertChunkData(dataSet, chunkDuration); + } + + @Test + public void testAdaptiveDataSetTrailingSmallChunk() { + long chunkDuration = 3 * C.MICROS_PER_SECOND; + FakeAdaptiveDataSet dataSet = + new FakeAdaptiveDataSet( + TRACK_GROUP, 10 * C.MICROS_PER_SECOND, chunkDuration, 0.0, new Random(0)); + assertThat(dataSet.getAllData().size()).isEqualTo(TEST_FORMATS.length); + assertThat(dataSet.getUri(0).equals(dataSet.getUri(1))).isFalse(); + assertThat(dataSet.getChunkCount()).isEqualTo(4); + assertThat(dataSet.getChunkIndexByPosition(4 * C.MICROS_PER_SECOND)).isEqualTo(1); + assertThat(dataSet.getChunkIndexByPosition(9 * C.MICROS_PER_SECOND)).isEqualTo(3); + for (int i = 0; i < dataSet.getChunkCount() - 1; i++) { + assertThat(dataSet.getChunkDuration(i)).isEqualTo(chunkDuration); + } + assertThat(dataSet.getChunkDuration(3)).isEqualTo(1 * C.MICROS_PER_SECOND); + assertChunkData(dataSet, chunkDuration); + } + + @Test + public void testAdaptiveDataSetChunkSizeDistribution() { + double expectedStdDev = 4.0; + FakeAdaptiveDataSet dataSet = + new FakeAdaptiveDataSet( + TRACK_GROUP, + 100000 * C.MICROS_PER_SECOND, + 1 * C.MICROS_PER_SECOND, + expectedStdDev, + new Random(0)); + for (int i = 0; i < TEST_FORMATS.length; i++) { + FakeData data = dataSet.getData(dataSet.getUri(i)); + double mean = computeSegmentSizeMean(data.getSegments()); + double stddev = computeSegmentSizeStdDev(data.getSegments(), mean); + double relativePercentStdDev = stddev / mean * 100.0; + assertThat(relativePercentStdDev).isWithin(0.02).of(expectedStdDev); + assertThat(mean * 8 / TEST_FORMATS[i].bitrate).isWithin(0.01).of(1.0); + } + } + + private void assertChunkData(FakeAdaptiveDataSet dataSet, long chunkDuration) { + for (int i = 0; i < dataSet.getChunkCount(); i++) { + assertThat(dataSet.getStartTime(i)).isEqualTo(chunkDuration * i); + } + for (int s = 0; s < TEST_FORMATS.length; s++) { + FakeData data = dataSet.getData(dataSet.getUri(s)); + assertThat(data.getSegments().size()).isEqualTo(dataSet.getChunkCount()); + for (int i = 0; i < data.getSegments().size(); i++) { + long expectedLength = + TEST_FORMATS[s].bitrate * dataSet.getChunkDuration(i) / (8 * C.MICROS_PER_SECOND); + assertThat(data.getSegments().get(i).length).isEqualTo(expectedLength); + } + } + } + + private static double computeSegmentSizeMean(List segments) { + double totalSize = 0.0; + for (Segment segment : segments) { + totalSize += segment.length; + } + return totalSize / segments.size(); + } + + private static double computeSegmentSizeStdDev(List segments, double mean) { + double totalSquaredSize = 0.0; + for (Segment segment : segments) { + totalSquaredSize += (double) segment.length * segment.length; + } + return Math.sqrt(totalSquaredSize / segments.size() - mean * mean); + } +} diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java new file mode 100644 index 0000000000..725753ce46 --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -0,0 +1,201 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.ConditionVariable; +import android.os.HandlerThread; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link FakeClock}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class FakeClockTest { + + private static final long TIMEOUT_MS = 10000; + + @Test + public void testAdvanceTime() { + FakeClock fakeClock = new FakeClock(2000); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000); + fakeClock.advanceTime(500); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500); + fakeClock.advanceTime(0); + assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500); + } + + @Test + public void testSleep() throws InterruptedException { + FakeClock fakeClock = new FakeClock(0); + SleeperThread sleeperThread = new SleeperThread(fakeClock, 1000); + sleeperThread.start(); + assertThat(sleeperThread.waitUntilAsleep(TIMEOUT_MS)).isTrue(); + assertThat(sleeperThread.isSleeping()).isTrue(); + fakeClock.advanceTime(1000); + sleeperThread.join(TIMEOUT_MS); + assertThat(sleeperThread.isSleeping()).isFalse(); + + sleeperThread = new SleeperThread(fakeClock, 0); + sleeperThread.start(); + sleeperThread.join(); + assertThat(sleeperThread.isSleeping()).isFalse(); + + SleeperThread[] sleeperThreads = new SleeperThread[5]; + sleeperThreads[0] = new SleeperThread(fakeClock, 1000); + sleeperThreads[1] = new SleeperThread(fakeClock, 1000); + sleeperThreads[2] = new SleeperThread(fakeClock, 2000); + sleeperThreads[3] = new SleeperThread(fakeClock, 3000); + sleeperThreads[4] = new SleeperThread(fakeClock, 4000); + for (SleeperThread thread : sleeperThreads) { + thread.start(); + assertThat(thread.waitUntilAsleep(TIMEOUT_MS)).isTrue(); + } + assertSleepingStates(new boolean[] {true, true, true, true, true}, sleeperThreads); + fakeClock.advanceTime(1500); + assertThat(sleeperThreads[0].waitUntilAwake(TIMEOUT_MS)).isTrue(); + assertThat(sleeperThreads[1].waitUntilAwake(TIMEOUT_MS)).isTrue(); + assertSleepingStates(new boolean[] {false, false, true, true, true}, sleeperThreads); + fakeClock.advanceTime(2000); + assertThat(sleeperThreads[2].waitUntilAwake(TIMEOUT_MS)).isTrue(); + assertThat(sleeperThreads[3].waitUntilAwake(TIMEOUT_MS)).isTrue(); + assertSleepingStates(new boolean[] {false, false, false, false, true}, sleeperThreads); + fakeClock.advanceTime(2000); + for (SleeperThread thread : sleeperThreads) { + thread.join(TIMEOUT_MS); + } + assertSleepingStates(new boolean[] {false, false, false, false, false}, sleeperThreads); + } + + @Test + public void testPostDelayed() { + HandlerThread handlerThread = new HandlerThread("FakeClockTest thread"); + handlerThread.start(); + FakeClock fakeClock = new FakeClock(0); + HandlerWrapper handler = + fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null); + + TestRunnable[] testRunnables = { + new TestRunnable(), + new TestRunnable(), + new TestRunnable(), + new TestRunnable(), + new TestRunnable() + }; + handler.postDelayed(testRunnables[0], 0); + handler.postDelayed(testRunnables[1], 100); + handler.postDelayed(testRunnables[2], 200); + waitForHandler(handler); + assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables); + + fakeClock.advanceTime(150); + handler.postDelayed(testRunnables[3], 50); + handler.postDelayed(testRunnables[4], 100); + waitForHandler(handler); + assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables); + + fakeClock.advanceTime(50); + waitForHandler(handler); + assertTestRunnableStates(new boolean[] {true, true, true, true, false}, testRunnables); + + fakeClock.advanceTime(1000); + waitForHandler(handler); + assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables); + } + + private static void assertSleepingStates(boolean[] states, SleeperThread[] sleeperThreads) { + for (int i = 0; i < sleeperThreads.length; i++) { + assertThat(sleeperThreads[i].isSleeping()).isEqualTo(states[i]); + } + } + + private static void waitForHandler(HandlerWrapper handler) { + final ConditionVariable handlerFinished = new ConditionVariable(); + handler.post( + new Runnable() { + @Override + public void run() { + handlerFinished.open(); + } + }); + handlerFinished.block(); + } + + private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) { + for (int i = 0; i < testRunnables.length; i++) { + assertThat(testRunnables[i].hasRun).isEqualTo(states[i]); + } + } + + private static final class SleeperThread extends Thread { + + private final Clock clock; + private final long sleepDurationMs; + private final CountDownLatch fallAsleepCountDownLatch; + private final CountDownLatch wakeUpCountDownLatch; + + private volatile boolean isSleeping; + + public SleeperThread(Clock clock, long sleepDurationMs) { + this.clock = clock; + this.sleepDurationMs = sleepDurationMs; + this.fallAsleepCountDownLatch = new CountDownLatch(1); + this.wakeUpCountDownLatch = new CountDownLatch(1); + } + + public boolean waitUntilAsleep(long timeoutMs) throws InterruptedException { + return fallAsleepCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS); + } + + public boolean waitUntilAwake(long timeoutMs) throws InterruptedException { + return wakeUpCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS); + } + + public boolean isSleeping() { + return isSleeping; + } + + @Override + public void run() { + // This relies on the FakeClock's methods synchronizing on its own monitor to ensure that + // any interactions with it occur only after sleep() has called wait() or returned. + synchronized (clock) { + isSleeping = true; + fallAsleepCountDownLatch.countDown(); + clock.sleep(sleepDurationMs); + isSleeping = false; + wakeUpCountDownLatch.countDown(); + } + } + } + + private static final class TestRunnable implements Runnable { + + public boolean hasRun; + + @Override + public void run() { + hasRun = true; + } + } +} diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java new file mode 100644 index 0000000000..75c6f886c2 --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSetTest.java @@ -0,0 +1,119 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData.Segment; +import java.io.IOException; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link FakeDataSet} */ +@RunWith(RobolectricTestRunner.class) +public final class FakeDataSetTest { + + @Test + public void testMultipleDataSets() { + byte[][] testData = new byte[4][]; + Uri[] uris = new Uri[3]; + for (int i = 0; i < 4; i++) { + testData[i] = TestUtil.buildTestData(10, i); + if (i > 0) { + uris[i - 1] = Uri.parse("test_uri_" + i); + } + } + FakeDataSet fakeDataSet = + new FakeDataSet() + .newDefaultData() + .appendReadData(testData[0]) + .endData() + .setData(uris[0], testData[1]) + .newData(uris[1]) + .appendReadData(testData[2]) + .endData() + .setData(uris[2], testData[3]); + + assertThat(fakeDataSet.getAllData().size()).isEqualTo(4); + assertThat(fakeDataSet.getData("unseen_uri")).isEqualTo(fakeDataSet.getData((Uri) null)); + for (int i = 0; i < 3; i++) { + assertThat(fakeDataSet.getData(uris[i]).uri).isEqualTo(uris[i]); + } + assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(testData[0]); + for (int i = 1; i < 4; i++) { + assertThat(fakeDataSet.getData(uris[i - 1]).getData()).isEqualTo(testData[i]); + } + } + + @Test + public void testSegmentTypes() { + byte[] testData = TestUtil.buildTestData(3); + Runnable runnable = + new Runnable() { + @Override + public void run() { + // Do nothing. + } + }; + IOException exception = new IOException(); + FakeDataSet fakeDataSet = + new FakeDataSet() + .newDefaultData() + .appendReadData(testData) + .appendReadData(testData) + .appendReadData(50) + .appendReadAction(runnable) + .appendReadError(exception) + .endData(); + + List segments = fakeDataSet.getData((Uri) null).getSegments(); + assertThat(segments.size()).isEqualTo(5); + assertSegment(segments.get(0), testData, 3, 0, null, null); + assertSegment(segments.get(1), testData, 3, 3, null, null); + assertSegment(segments.get(2), null, 50, 6, null, null); + assertSegment(segments.get(3), null, 0, 56, runnable, null); + assertSegment(segments.get(4), null, 0, 56, null, exception); + + byte[] allData = new byte[6]; + System.arraycopy(testData, 0, allData, 0, 3); + System.arraycopy(testData, 0, allData, 3, 3); + assertThat(fakeDataSet.getData((Uri) null).getData()).isEqualTo(allData); + } + + private static void assertSegment( + Segment segment, + byte[] data, + int length, + long byteOffset, + Runnable runnable, + IOException exception) { + if (data != null) { + assertThat(segment.data).isEqualTo(data); + assertThat(data).hasLength(length); + } else { + assertThat(segment.data).isNull(); + } + assertThat(segment.length).isEqualTo(length); + assertThat(segment.byteOffset).isEqualTo(byteOffset); + assertThat(segment.action).isEqualTo(runnable); + assertThat(segment.isActionSegment()).isEqualTo(runnable != null); + assertThat(segment.exception).isEqualTo(exception); + assertThat(segment.isErrorSegment()).isEqualTo(exception != null); + } +} diff --git a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSourceTest.java b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSourceTest.java new file mode 100644 index 0000000000..c88aba4e08 --- /dev/null +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeDataSourceTest.java @@ -0,0 +1,251 @@ +/* + * 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.testutil; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import java.util.Arrays; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test for {@link FakeDataSource}. */ +@RunWith(RobolectricTestRunner.class) +public final class FakeDataSourceTest { + + private static final String URI_STRING = "test://test.test"; + private static final byte[] BUFFER = new byte[500]; + private static final byte[] TEST_DATA = TestUtil.buildTestData(15); + private static final byte[] TEST_DATA_PART_1 = Arrays.copyOf(TEST_DATA, 10); + private static final byte[] TEST_DATA_PART_2 = Arrays.copyOfRange(TEST_DATA, 10, 15); + + private static Uri uri; + private static FakeDataSet fakeDataSet; + + @Before + public void setUp() { + uri = Uri.parse(URI_STRING); + fakeDataSet = + new FakeDataSet() + .newData(uri.toString()) + .appendReadData(TEST_DATA_PART_1) + .appendReadData(TEST_DATA_PART_2) + .endData(); + } + + @Test + public void testReadFull() throws IOException { + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(15); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(10); + assertBuffer(TEST_DATA_PART_1); + assertThat(dataSource.read(BUFFER, 10, BUFFER.length)).isEqualTo(5); + assertBuffer(TEST_DATA); + assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + assertBuffer(TEST_DATA); + assertThat(dataSource.read(BUFFER, 20, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testReadPartialOpenEnded() throws IOException { + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + assertThat(dataSource.open(new DataSpec(uri, 7, C.LENGTH_UNSET, null))).isEqualTo(8); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(3); + assertBuffer(TEST_DATA_PART_1, 7, 3); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(5); + assertBuffer(TEST_DATA_PART_2); + assertThat(dataSource.read(BUFFER, 15, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testReadPartialBounded() throws IOException { + FakeDataSource dataSource = new FakeDataSource(fakeDataSet); + assertThat(dataSource.open(new DataSpec(uri, 9, 3, null))).isEqualTo(3); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(1); + assertBuffer(TEST_DATA_PART_1, 9, 1); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(2); + assertBuffer(TEST_DATA_PART_2, 0, 2); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + + assertThat(dataSource.open(new DataSpec(uri, 11, 4, null))).isEqualTo(4); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(4); + assertBuffer(TEST_DATA_PART_2, 1, 4); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testDummyData() throws IOException { + FakeDataSource dataSource = + new FakeDataSource( + new FakeDataSet() + .newData(uri.toString()) + .appendReadData(100) + .appendReadData(TEST_DATA) + .appendReadData(200) + .endData()); + assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(315); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(100); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(200); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testException() throws IOException { + String errorMessage = "error, error, error"; + IOException exception = new IOException(errorMessage); + FakeDataSource dataSource = + new FakeDataSource( + new FakeDataSet() + .newData(uri.toString()) + .appendReadData(TEST_DATA) + .appendReadError(exception) + .appendReadData(TEST_DATA) + .endData()); + assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(30); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + try { + dataSource.read(BUFFER, 0, BUFFER.length); + fail("IOException expected."); + } catch (IOException e) { + assertThat(e).hasMessageThat().isEqualTo(errorMessage); + } + try { + dataSource.read(BUFFER, 0, BUFFER.length); + fail("IOException expected."); + } catch (IOException e) { + assertThat(e).hasMessageThat().isEqualTo(errorMessage); + } + dataSource.close(); + assertThat(dataSource.open(new DataSpec(uri, 15, 15, null))).isEqualTo(15); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testRunnable() throws IOException { + TestRunnable[] runnables = new TestRunnable[3]; + for (int i = 0; i < 3; i++) { + runnables[i] = new TestRunnable(); + } + FakeDataSource dataSource = + new FakeDataSource( + new FakeDataSet() + .newData(uri.toString()) + .appendReadData(TEST_DATA) + .appendReadAction(runnables[0]) + .appendReadData(TEST_DATA) + .appendReadAction(runnables[1]) + .appendReadAction(runnables[2]) + .appendReadData(TEST_DATA) + .endData()); + assertThat(dataSource.open(new DataSpec(uri))).isEqualTo(45); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + for (int i = 0; i < 3; i++) { + assertThat(runnables[i].ran).isFalse(); + } + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + assertThat(runnables[0].ran).isTrue(); + assertThat(runnables[1].ran).isFalse(); + assertThat(runnables[2].ran).isFalse(); + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(15); + assertBuffer(TEST_DATA); + for (int i = 0; i < 3; i++) { + assertThat(runnables[i].ran).isTrue(); + } + assertThat(dataSource.read(BUFFER, 0, BUFFER.length)).isEqualTo(C.RESULT_END_OF_INPUT); + dataSource.close(); + } + + @Test + public void testOpenSourceFailures() throws IOException { + // Empty data. + FakeDataSource dataSource = + new FakeDataSource(new FakeDataSet().newData(uri.toString()).endData()); + try { + dataSource.open(new DataSpec(uri)); + fail("IOException expected."); + } catch (IOException e) { + // Expected. + } finally { + dataSource.close(); + } + + // Non-existent data + dataSource = new FakeDataSource(new FakeDataSet()); + try { + dataSource.open(new DataSpec(uri)); + fail("IOException expected."); + } catch (IOException e) { + // Expected. + } finally { + dataSource.close(); + } + + // DataSpec out of bounds. + dataSource = + new FakeDataSource( + new FakeDataSet() + .newDefaultData() + .appendReadData(TestUtil.buildTestData(10)) + .endData()); + try { + dataSource.open(new DataSpec(uri, 5, 10, null)); + fail("IOException expected."); + } catch (IOException e) { + // Expected. + } finally { + dataSource.close(); + } + } + + private static void assertBuffer(byte[] expected) { + assertBuffer(expected, 0, expected.length); + } + + private static void assertBuffer(byte[] expected, int expectedStart, int expectedLength) { + for (int i = 0; i < expectedLength; i++) { + assertThat(BUFFER[i]).isEqualTo(expected[i + expectedStart]); + } + } + + private static final class TestRunnable implements Runnable { + + public boolean ran; + + @Override + public void run() { + ran = true; + } + } +} diff --git a/testutils/src/test/resources/robolectric.properties b/testutils/src/test/resources/robolectric.properties new file mode 100644 index 0000000000..2f3210368e --- /dev/null +++ b/testutils/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +manifest=src/test/AndroidManifest.xml diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle new file mode 100644 index 0000000000..f5a427a4b3 --- /dev/null +++ b/testutils_robolectric/build.gradle @@ -0,0 +1,37 @@ +// 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 + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion project.ext.minSdkVersion + targetSdkVersion project.ext.targetSdkVersion + } + + lintOptions { + // Truth depends on JUnit, which depends on java.lang.management, which + // is not part of Android. Remove this when JUnit 4.13 or later is used. + // See: https://github.com/junit-team/junit4/pull/1187. + disable 'InvalidPackage' + } +} + +dependencies { + compile project(modulePrefix + 'testutils') + compile 'org.robolectric:robolectric:' + robolectricVersion +} diff --git a/testutils_robolectric/src/main/AndroidManifest.xml b/testutils_robolectric/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..057caad867 --- /dev/null +++ b/testutils_robolectric/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java similarity index 99% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index f9ababe389..6d3b15ac7a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -29,9 +29,7 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; import java.io.IOException; import java.util.ArrayList; -/** - * Assertion methods for {@link Cache}. - */ +/** Assertion methods for {@link Cache}. */ public final class CacheAsserts { /** @@ -135,5 +133,4 @@ public final class CacheAsserts { } private CacheAsserts() {} - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java similarity index 81% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java index 6ef3dd0b0d..8f65dc876a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/DummyMainThread.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; +import android.os.Looper; /** Helper class to simulate main/UI thread in tests. */ public final class DummyMainThread { @@ -54,16 +55,20 @@ public final class DummyMainThread { * @param runnable The {@link Runnable} to run. */ public void runOnMainThread(int timeoutMs, final Runnable runnable) { - final ConditionVariable finishedCondition = new ConditionVariable(); - handler.post( - new Runnable() { - @Override - public void run() { - runnable.run(); - finishedCondition.open(); - } - }); - assertThat(finishedCondition.block(timeoutMs)).isTrue(); + if (Looper.myLooper() == handler.getLooper()) { + runnable.run(); + } else { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post( + new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertThat(finishedCondition.block(timeoutMs)).isTrue(); + } } public void release() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java similarity index 93% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java index 4d118f9288..009afd1ff7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaClockRenderer.java @@ -19,9 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.util.MediaClock; -/** - * Fake abstract {@link Renderer} which is also a {@link MediaClock}. - */ +/** Fake abstract {@link Renderer} which is also a {@link MediaClock}. */ public abstract class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { public FakeMediaClockRenderer(Format... expectedFormats) { @@ -32,5 +30,4 @@ public abstract class FakeMediaClockRenderer extends FakeRenderer implements Med public MediaClock getMediaClock() { return this; } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java similarity index 100% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java similarity index 94% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java index f5f1987f31..8ceb5338a6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -25,8 +25,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import java.util.List; /** - * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number - * of calls to its methods. + * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number of + * calls to its methods. */ public final class FakeTrackSelection implements TrackSelection { @@ -118,8 +118,8 @@ public final class FakeTrackSelection implements TrackSelection { } @Override - public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, - long availableDurationUs) { + public void updateSelectedTrack( + long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { assertThat(isEnabled).isTrue(); } @@ -134,5 +134,4 @@ public final class FakeTrackSelection implements TrackSelection { assertThat(isEnabled).isTrue(); return false; } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java similarity index 83% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java index da9a1a18ad..2daafbbb0b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java @@ -25,9 +25,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import java.util.ArrayList; import java.util.List; -/** - * A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. - */ +/** A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. */ public class FakeTrackSelector extends MappingTrackSelector { private final List selectedTrackSelections = new ArrayList<>(); @@ -38,17 +36,19 @@ public class FakeTrackSelector extends MappingTrackSelector { } /** - * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse - * {@link TrackSelection}s during track selection, when it finds previously-selected track - * selection using the same {@link TrackGroup}. + * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse {@link + * TrackSelection}s during track selection, when it finds previously-selected track selection + * using the same {@link TrackGroup}. */ public FakeTrackSelector(boolean mayReuseTrackSelection) { this.mayReuseTrackSelection = mayReuseTrackSelection; } @Override - protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, - TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + protected TrackSelection[] selectTracks( + RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, + int[][][] rendererFormatSupports) throws ExoPlaybackException { List resultList = new ArrayList<>(); for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { @@ -76,11 +76,8 @@ public class FakeTrackSelector extends MappingTrackSelector { return trackSelectionForRenderer; } - /** - * Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. - */ + /** Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. */ public List getSelectedTrackSelections() { return selectedTrackSelections; } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java similarity index 85% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 16389112ca..fbb48c9529 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -37,9 +37,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; -/** - * A runner for {@link MediaSource} tests. - */ +/** A runner for {@link MediaSource} tests. */ public class MediaSourceTestRunner { public static final int TIMEOUT_MS = 10000; @@ -78,18 +76,19 @@ public class MediaSourceTestRunner { public void runOnPlaybackThread(final Runnable runnable) { final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); - playbackHandler.post(new Runnable() { - @Override - public void run() { - try { - runnable.run(); - } catch (Throwable e) { - throwable[0] = e; - } finally { - finishedCondition.open(); - } - } - }); + playbackHandler.post( + new Runnable() { + @Override + public void run() { + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); + } + } + }); assertThat(finishedCondition.block(TIMEOUT_MS)).isTrue(); if (throwable[0] != null) { Util.sneakyThrow(throwable[0]); @@ -103,20 +102,21 @@ public class MediaSourceTestRunner { */ public Timeline prepareSource() throws IOException { final IOException[] prepareError = new IOException[1]; - runOnPlaybackThread(new Runnable() { - @Override - public void run() { - mediaSource.prepareSource(player, true, mediaSourceListener); - try { - // TODO: This only catches errors that are set synchronously in prepareSource. To capture - // async errors we'll need to poll maybeThrowSourceInfoRefreshError until the first call - // to onSourceInfoRefreshed. - mediaSource.maybeThrowSourceInfoRefreshError(); - } catch (IOException e) { - prepareError[0] = e; - } - } - }); + runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(player, true, mediaSourceListener); + try { + // TODO: This only catches errors that are set synchronously in prepareSource. To + // capture async errors we'll need to poll maybeThrowSourceInfoRefreshError until the + // first call to onSourceInfoRefreshed. + mediaSource.maybeThrowSourceInfoRefreshError(); + } catch (IOException e) { + prepareError[0] = e; + } + } + }); if (prepareError[0] != null) { throw prepareError[0]; } @@ -132,12 +132,13 @@ public class MediaSourceTestRunner { */ public MediaPeriod createPeriod(final MediaPeriodId periodId) { final MediaPeriod[] holder = new MediaPeriod[1]; - runOnPlaybackThread(new Runnable() { - @Override - public void run() { - holder[0] = mediaSource.createPeriod(periodId, allocator); - } - }); + runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + holder[0] = mediaSource.createPeriod(periodId, allocator); + } + }); assertThat(holder[0]).isNotNull(); return holder[0]; } @@ -183,24 +184,24 @@ public class MediaSourceTestRunner { * @param mediaPeriod The {@link MediaPeriod} to release. */ public void releasePeriod(final MediaPeriod mediaPeriod) { - runOnPlaybackThread(new Runnable() { - @Override - public void run() { - mediaSource.releasePeriod(mediaPeriod); - } - }); + runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + mediaSource.releasePeriod(mediaPeriod); + } + }); } - /** - * Calls {@link MediaSource#releaseSource()} on the playback thread. - */ + /** Calls {@link MediaSource#releaseSource()} on the playback thread. */ public void releaseSource() { - runOnPlaybackThread(new Runnable() { - @Override - public void run() { - mediaSource.releaseSource(); - } - }); + runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + mediaSource.releaseSource(); + } + }); } /** @@ -276,9 +277,7 @@ public class MediaSourceTestRunner { releasePeriod(secondMediaPeriod); } - /** - * Releases the runner. Should be called when the runner is no longer required. - */ + /** Releases the runner. Should be called when the runner is no longer required. */ public void release() { playbackThread.quit(); } @@ -290,7 +289,6 @@ public class MediaSourceTestRunner { Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); timelines.addLast(timeline); } - } private static class EventHandlingExoPlayer extends StubExoPlayer @@ -326,5 +324,4 @@ public class MediaSourceTestRunner { return true; } } - } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java new file mode 100644 index 0000000000..8dd0cd16b1 --- /dev/null +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -0,0 +1,1070 @@ +/* + * 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.testutil; + +/** Provides ogg/vorbis test data in bytes for unit tests. */ +public final class OggTestData { + + public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { + return new FakeExtractorInput.Builder() + .setData(data) + .setSimulateIOErrors(true) + .setSimulateUnknownLength(simulateUnknownLength) + .setSimulatePartialReads(true) + .build(); + } + + public static byte[] buildOggHeader( + int headerType, long granule, int pageSequenceCounter, int pageSegmentCount) { + return TestUtil.createByteArray( + 0x4F, + 0x67, + 0x67, + 0x53, // Oggs. + 0x00, // Stream revision. + headerType, + (int) (granule) & 0xFF, + (int) (granule >> 8) & 0xFF, + (int) (granule >> 16) & 0xFF, + (int) (granule >> 24) & 0xFF, + (int) (granule >> 32) & 0xFF, + (int) (granule >> 40) & 0xFF, + (int) (granule >> 48) & 0xFF, + (int) (granule >> 56) & 0xFF, + 0x00, // LSB of data serial number. + 0x10, + 0x00, + 0x00, // MSB of data serial number. + (pageSequenceCounter) & 0xFF, + (pageSequenceCounter >> 8) & 0xFF, + (pageSequenceCounter >> 16) & 0xFF, + (pageSequenceCounter >> 24) & 0xFF, + 0x00, // LSB of page checksum. + 0x00, + 0x10, + 0x00, // MSB of page checksum. + pageSegmentCount); + } + + /** + * Returns the initial two pages of bytes which by spec contain the three vorbis header packets: + * identification, comment and setup header. + */ + public static byte[] getVorbisHeaderPages() { + byte[] data = new byte[VORBIS_HEADER_PAGES.length]; + System.arraycopy(VORBIS_HEADER_PAGES, 0, data, 0, VORBIS_HEADER_PAGES.length); + return data; + } + + /** Returns a valid vorbis identification header in bytes. */ + public static byte[] getIdentificationHeaderData() { + int idHeaderStart = 28; + int idHeaderLength = 30; + byte[] idHeaderData = new byte[idHeaderLength]; + System.arraycopy(VORBIS_HEADER_PAGES, idHeaderStart, idHeaderData, 0, idHeaderLength); + return idHeaderData; + } + + /** Returns a valid vorbis comment header with 3 comments including utf8 chars in bytes. */ + public static byte[] getCommentHeaderDataUTF8() { + byte[] commentHeaderData = new byte[COMMENT_HEADER_WITH_UTF8.length]; + System.arraycopy( + COMMENT_HEADER_WITH_UTF8, 0, commentHeaderData, 0, COMMENT_HEADER_WITH_UTF8.length); + return commentHeaderData; + } + + /** Returns a valid vorbis setup header in bytes. */ + public static byte[] getSetupHeaderData() { + int setupHeaderStart = 146; + int setupHeaderLength = VORBIS_HEADER_PAGES.length - setupHeaderStart; + byte[] setupHeaderData = new byte[setupHeaderLength]; + System.arraycopy(VORBIS_HEADER_PAGES, setupHeaderStart, setupHeaderData, 0, setupHeaderLength); + return setupHeaderData; + } + + private static final byte[] COMMENT_HEADER_WITH_UTF8 = { + (byte) 0x03, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 3, v, o, r, + (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x2b, // b, i, s, . + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x58, + (byte) 0x69, (byte) 0x70, (byte) 0x68, (byte) 0x2e, + (byte) 0x4f, (byte) 0x72, (byte) 0x67, (byte) 0x20, + (byte) 0x6c, (byte) 0x69, (byte) 0x62, (byte) 0x56, + (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69, + (byte) 0x73, (byte) 0x20, (byte) 0x49, (byte) 0x20, + (byte) 0x32, (byte) 0x30, (byte) 0x31, (byte) 0x32, + (byte) 0x30, (byte) 0x32, (byte) 0x30, (byte) 0x33, + (byte) 0x20, (byte) 0x28, (byte) 0x4f, (byte) 0x6d, + (byte) 0x6e, (byte) 0x69, (byte) 0x70, (byte) 0x72, + (byte) 0x65, (byte) 0x73, (byte) 0x65, (byte) 0x6e, + (byte) 0x74, (byte) 0x29, (byte) 0x03, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x4c, + (byte) 0x42, (byte) 0x55, (byte) 0x4d, (byte) 0x3d, + (byte) 0xc3, (byte) 0xa4, (byte) 0xc3, (byte) 0xb6, + (byte) 0x13, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x54, (byte) 0x49, (byte) 0x54, (byte) 0x4c, + (byte) 0x45, (byte) 0x3d, (byte) 0x41, (byte) 0x20, + (byte) 0x73, (byte) 0x61, (byte) 0x6d, (byte) 0x70, + (byte) 0x6c, (byte) 0x65, (byte) 0x20, (byte) 0x73, + (byte) 0x6f, (byte) 0x6e, (byte) 0x67, (byte) 0x0d, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, + (byte) 0x52, (byte) 0x54, (byte) 0x49, (byte) 0x53, + (byte) 0x54, (byte) 0x3d, (byte) 0x47, (byte) 0x6f, + (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, + (byte) 0x01 + }; + + // two OGG pages with 3 packets (id, comment and setup header) + // length: 3743 bytes + private static final byte[] VORBIS_HEADER_PAGES = { /* capture pattern ogg header 1 */ + (byte) 0x4f, (byte) 0x67, (byte) 0x67, (byte) 0x53, // O,g,g,S : start pos 0 + (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x5e, (byte) 0x5f, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x83, (byte) 0x36, + (byte) 0xe3, (byte) 0x49, (byte) 0x01, (byte) 0x1e, /* capture pattern vorbis id header */ + (byte) 0x01, (byte) 0x76, (byte) 0x6f, (byte) 0x72, // 1,v,o,r : start pos 28 + (byte) 0x62, (byte) 0x69, (byte) 0x73, (byte) 0x00, // b,i,s,. + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x02, + (byte) 0x22, (byte) 0x56, (byte) 0x00, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0x6a, (byte) 0x04, (byte) 0x01, (byte) 0x00, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern ogg header 2 */ + (byte) 0xa9, (byte) 0x01, (byte) 0x4f, (byte) 0x67, // .,.,O,g : start pos 86 + (byte) 0x67, (byte) 0x53, (byte) 0x00, (byte) 0x00, // g,S,.,. + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x5e, (byte) 0x5f, (byte) 0x00, (byte) 0x00, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x69, (byte) 0xf8, (byte) 0xeb, (byte) 0xe1, + (byte) 0x10, (byte) 0x2d, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, /* capture pattern vorbis comment header*/ + (byte) 0x1b, (byte) 0x03, (byte) 0x76, (byte) 0x6f, // .,3,v,o : start pos 101 + (byte) 0x72, (byte) 0x62, (byte) 0x69, (byte) 0x73, // r,b,i,s + (byte) 0x1d, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x58, (byte) 0x69, (byte) 0x70, (byte) 0x68, + (byte) 0x2e, (byte) 0x4f, (byte) 0x72, (byte) 0x67, + (byte) 0x20, (byte) 0x6c, (byte) 0x69, (byte) 0x62, + (byte) 0x56, (byte) 0x6f, (byte) 0x72, (byte) 0x62, + (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x49, + (byte) 0x20, (byte) 0x32, (byte) 0x30, (byte) 0x30, + (byte) 0x33, (byte) 0x30, (byte) 0x39, (byte) 0x30, + (byte) 0x39, (byte) 0x00, (byte) 0x00, (byte) 0x00, /* capture pattern vorbis setup header */ + (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x76, // .,.,5,v : start pos 146 + (byte) 0x6f, (byte) 0x72, (byte) 0x62, (byte) 0x69, // o,r,b,i + (byte) 0x73, (byte) 0x22, (byte) 0x42, (byte) 0x43, // s,. + (byte) 0x56, (byte) 0x01, (byte) 0x00, (byte) 0x40, + (byte) 0x00, (byte) 0x00, (byte) 0x18, (byte) 0x42, + (byte) 0x10, (byte) 0x2a, (byte) 0x05, (byte) 0xad, + (byte) 0x63, (byte) 0x8e, (byte) 0x3a, (byte) 0xc8, + (byte) 0x15, (byte) 0x21, (byte) 0x8c, (byte) 0x19, + (byte) 0xa2, (byte) 0xa0, (byte) 0x42, (byte) 0xca, + (byte) 0x29, (byte) 0xc7, (byte) 0x1d, (byte) 0x42, + (byte) 0xd0, (byte) 0x21, (byte) 0xa3, (byte) 0x24, + (byte) 0x43, (byte) 0x88, (byte) 0x3a, (byte) 0xc6, + (byte) 0x35, (byte) 0xc7, (byte) 0x18, (byte) 0x63, + (byte) 0x47, (byte) 0xb9, (byte) 0x64, (byte) 0x8a, + (byte) 0x42, (byte) 0xc9, (byte) 0x81, (byte) 0xd0, + (byte) 0x90, (byte) 0x55, (byte) 0x00, (byte) 0x00, + (byte) 0x40, (byte) 0x00, (byte) 0x00, (byte) 0xa4, + (byte) 0x1c, (byte) 0x57, (byte) 0x50, (byte) 0x72, + (byte) 0x49, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0xa3, (byte) 0x18, (byte) 0x57, + (byte) 0xcc, (byte) 0x71, (byte) 0xe8, (byte) 0x20, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe5, + (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71, + (byte) 0x09, (byte) 0x25, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0x8e, (byte) 0x39, (byte) 0xe7, + (byte) 0x92, (byte) 0x72, (byte) 0x8e, (byte) 0x31, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa3, + (byte) 0x18, (byte) 0x57, (byte) 0x0e, (byte) 0x72, + (byte) 0x29, (byte) 0x2d, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0x81, (byte) 0x14, (byte) 0x47, + (byte) 0x8a, (byte) 0x71, (byte) 0xa7, (byte) 0x18, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xa4, + (byte) 0x1c, (byte) 0x47, (byte) 0x8a, (byte) 0x71, + (byte) 0xa8, (byte) 0x18, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0x6d, (byte) 0x31, (byte) 0xb7, + (byte) 0x92, (byte) 0x72, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xe6, + (byte) 0x20, (byte) 0x87, (byte) 0x52, (byte) 0x72, + (byte) 0xae, (byte) 0x35, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0xa4, (byte) 0x18, (byte) 0x67, + (byte) 0x0e, (byte) 0x72, (byte) 0x0b, (byte) 0x25, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xc6, + (byte) 0x20, (byte) 0x67, (byte) 0xcc, (byte) 0x71, + (byte) 0xeb, (byte) 0x20, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0x8c, (byte) 0x35, (byte) 0xb7, + (byte) 0xd4, (byte) 0x72, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, + (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, + (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0x8c, (byte) 0x31, (byte) 0xe7, + (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x6e, + (byte) 0x31, (byte) 0xe7, (byte) 0x16, (byte) 0x73, + (byte) 0xae, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, + (byte) 0x1c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0x20, + (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00, + (byte) 0x90, (byte) 0x00, (byte) 0x00, (byte) 0xa0, + (byte) 0xa1, (byte) 0x28, (byte) 0x8a, (byte) 0xe2, + (byte) 0x28, (byte) 0x0e, (byte) 0x10, (byte) 0x1a, + (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0xc8, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x40, + (byte) 0x71, (byte) 0x14, (byte) 0x47, (byte) 0x91, + (byte) 0x14, (byte) 0x4b, (byte) 0xb1, (byte) 0x1c, + (byte) 0xcb, (byte) 0xd1, (byte) 0x24, (byte) 0x0d, + (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05, + (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x00, + (byte) 0x08, (byte) 0x00, (byte) 0x00, (byte) 0xa0, + (byte) 0x48, (byte) 0x86, (byte) 0xa4, (byte) 0x48, + (byte) 0x8a, (byte) 0xa5, (byte) 0x58, (byte) 0x8e, + (byte) 0x66, (byte) 0x69, (byte) 0x9e, (byte) 0x26, + (byte) 0x7a, (byte) 0xa2, (byte) 0x28, (byte) 0x9a, + (byte) 0xa2, (byte) 0x2a, (byte) 0xab, (byte) 0xb2, + (byte) 0x69, (byte) 0xca, (byte) 0xb2, (byte) 0x2c, + (byte) 0xcb, (byte) 0xb2, (byte) 0xeb, (byte) 0xba, + (byte) 0x2e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2, + (byte) 0x0a, (byte) 0x00, (byte) 0x48, (byte) 0x00, + (byte) 0x00, (byte) 0x50, (byte) 0x51, (byte) 0x14, + (byte) 0xc5, (byte) 0x70, (byte) 0x14, (byte) 0x07, + (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x05, + (byte) 0x00, (byte) 0x64, (byte) 0x00, (byte) 0x00, + (byte) 0x08, (byte) 0x60, (byte) 0x28, (byte) 0x8a, + (byte) 0xa3, (byte) 0x38, (byte) 0x8e, (byte) 0xe4, + (byte) 0x58, (byte) 0x92, (byte) 0xa5, (byte) 0x59, + (byte) 0x9e, (byte) 0x07, (byte) 0x84, (byte) 0x86, + (byte) 0xac, (byte) 0x02, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x00, + (byte) 0x00, (byte) 0x50, (byte) 0x0c, (byte) 0x47, + (byte) 0xb1, (byte) 0x14, (byte) 0x4d, (byte) 0xf1, + (byte) 0x24, (byte) 0xcf, (byte) 0xf2, (byte) 0x3c, + (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, + (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, (byte) 0xf3, + (byte) 0x3c, (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, + (byte) 0xcf, (byte) 0xf3, (byte) 0x3c, (byte) 0xcf, + (byte) 0xf3, (byte) 0x3c, (byte) 0x0d, (byte) 0x08, + (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00, + (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x82, (byte) 0x28, (byte) 0x64, (byte) 0x18, + (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56, + (byte) 0x01, (byte) 0x00, (byte) 0x40, (byte) 0x00, + (byte) 0x00, (byte) 0x08, (byte) 0x21, (byte) 0x1a, + (byte) 0x19, (byte) 0x43, (byte) 0x9d, (byte) 0x52, + (byte) 0x12, (byte) 0x5c, (byte) 0x0a, (byte) 0x16, + (byte) 0x42, (byte) 0x1c, (byte) 0x11, (byte) 0x43, + (byte) 0x1d, (byte) 0x42, (byte) 0xce, (byte) 0x43, + (byte) 0xa9, (byte) 0xa5, (byte) 0x83, (byte) 0xe0, + (byte) 0x29, (byte) 0x85, (byte) 0x25, (byte) 0x63, + (byte) 0xd2, (byte) 0x53, (byte) 0xac, (byte) 0x41, + (byte) 0x08, (byte) 0x21, (byte) 0x7c, (byte) 0xef, + (byte) 0x3d, (byte) 0xf7, (byte) 0xde, (byte) 0x7b, + (byte) 0xef, (byte) 0x81, (byte) 0xd0, (byte) 0x90, + (byte) 0x55, (byte) 0x00, (byte) 0x00, (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x61, (byte) 0x14, + (byte) 0x38, (byte) 0x88, (byte) 0x81, (byte) 0xc7, + (byte) 0x24, (byte) 0x08, (byte) 0x21, (byte) 0x84, + (byte) 0x62, (byte) 0x14, (byte) 0x27, (byte) 0x44, + (byte) 0x71, (byte) 0xa6, (byte) 0x20, (byte) 0x08, + (byte) 0x21, (byte) 0x84, (byte) 0xe5, (byte) 0x24, + (byte) 0x58, (byte) 0xca, (byte) 0x79, (byte) 0xe8, + (byte) 0x24, (byte) 0x08, (byte) 0xdd, (byte) 0x83, + (byte) 0x10, (byte) 0x42, (byte) 0xb8, (byte) 0x9c, + (byte) 0x7b, (byte) 0xcb, (byte) 0xb9, (byte) 0xf7, + (byte) 0xde, (byte) 0x7b, (byte) 0x20, (byte) 0x34, + (byte) 0x64, (byte) 0x15, (byte) 0x00, (byte) 0x00, + (byte) 0x08, (byte) 0x00, (byte) 0xc0, (byte) 0x20, + (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, + (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, + (byte) 0x08, (byte) 0x29, (byte) 0xa4, (byte) 0x94, + (byte) 0x52, (byte) 0x48, (byte) 0x29, (byte) 0xa6, + (byte) 0x98, (byte) 0x62, (byte) 0x8a, (byte) 0x29, + (byte) 0xc7, (byte) 0x1c, (byte) 0x73, (byte) 0xcc, + (byte) 0x31, (byte) 0xc7, (byte) 0x20, (byte) 0x83, + (byte) 0x0c, (byte) 0x32, (byte) 0xe8, (byte) 0xa0, + (byte) 0x93, (byte) 0x4e, (byte) 0x3a, (byte) 0xc9, + (byte) 0xa4, (byte) 0x92, (byte) 0x4e, (byte) 0x3a, + (byte) 0xca, (byte) 0x24, (byte) 0xa3, (byte) 0x8e, + (byte) 0x52, (byte) 0x6b, (byte) 0x29, (byte) 0xb5, + (byte) 0x14, (byte) 0x53, (byte) 0x4c, (byte) 0xb1, + (byte) 0xe5, (byte) 0x16, (byte) 0x63, (byte) 0xad, + (byte) 0xb5, (byte) 0xd6, (byte) 0x9c, (byte) 0x73, + (byte) 0xaf, (byte) 0x41, (byte) 0x29, (byte) 0x63, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, + (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x23, (byte) 0x08, + (byte) 0x0d, (byte) 0x59, (byte) 0x05, (byte) 0x00, + (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x10, + (byte) 0x06, (byte) 0x19, (byte) 0x64, (byte) 0x90, + (byte) 0x41, (byte) 0x08, (byte) 0x21, (byte) 0x84, + (byte) 0x14, (byte) 0x52, (byte) 0x48, (byte) 0x29, + (byte) 0xa6, (byte) 0x98, (byte) 0x72, (byte) 0xcc, + (byte) 0x31, (byte) 0xc7, (byte) 0x1c, (byte) 0x03, + (byte) 0x42, (byte) 0x43, (byte) 0x56, (byte) 0x01, + (byte) 0x00, (byte) 0x80, (byte) 0x00, (byte) 0x00, + (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x1c, (byte) 0x45, (byte) 0x52, (byte) 0x24, + (byte) 0x47, (byte) 0x72, (byte) 0x24, (byte) 0x47, + (byte) 0x92, (byte) 0x24, (byte) 0xc9, (byte) 0x92, + (byte) 0x2c, (byte) 0x49, (byte) 0x93, (byte) 0x3c, + (byte) 0xcb, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb, + (byte) 0xb3, (byte) 0x3c, (byte) 0x4d, (byte) 0xd4, + (byte) 0x44, (byte) 0x4d, (byte) 0x15, (byte) 0x55, + (byte) 0xd5, (byte) 0x55, (byte) 0x6d, (byte) 0xd7, + (byte) 0xf6, (byte) 0x6d, (byte) 0x5f, (byte) 0xf6, + (byte) 0x6d, (byte) 0xdf, (byte) 0xd5, (byte) 0x65, + (byte) 0xdf, (byte) 0xf6, (byte) 0x65, (byte) 0xdb, + (byte) 0xd5, (byte) 0x65, (byte) 0x5d, (byte) 0x96, + (byte) 0x65, (byte) 0xdd, (byte) 0xb5, (byte) 0x6d, + (byte) 0x5d, (byte) 0xd6, (byte) 0x5d, (byte) 0x5d, + (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7, + (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x75, + (byte) 0x5d, (byte) 0xd7, (byte) 0x75, (byte) 0x5d, + (byte) 0xd7, (byte) 0x75, (byte) 0x5d, (byte) 0xd7, + (byte) 0x81, (byte) 0xd0, (byte) 0x90, (byte) 0x55, + (byte) 0x00, (byte) 0x80, (byte) 0x04, (byte) 0x00, + (byte) 0x80, (byte) 0x8e, (byte) 0xe4, (byte) 0x38, + (byte) 0x8e, (byte) 0xe4, (byte) 0x38, (byte) 0x8e, + (byte) 0xe4, (byte) 0x48, (byte) 0x8e, (byte) 0xa4, + (byte) 0x48, (byte) 0x0a, (byte) 0x10, (byte) 0x1a, + (byte) 0xb2, (byte) 0x0a, (byte) 0x00, (byte) 0x90, + (byte) 0x01, (byte) 0x00, (byte) 0x10, (byte) 0x00, + (byte) 0x80, (byte) 0xa3, (byte) 0x38, (byte) 0x8a, + (byte) 0xe3, (byte) 0x48, (byte) 0x8e, (byte) 0xe4, + (byte) 0x58, (byte) 0x8e, (byte) 0x25, (byte) 0x59, + (byte) 0x92, (byte) 0x26, (byte) 0x69, (byte) 0x96, + (byte) 0x67, (byte) 0x79, (byte) 0x96, (byte) 0xa7, + (byte) 0x79, (byte) 0x9a, (byte) 0xa8, (byte) 0x89, + (byte) 0x1e, (byte) 0x10, (byte) 0x1a, (byte) 0xb2, + (byte) 0x0a, (byte) 0x00, (byte) 0x00, (byte) 0x04, + (byte) 0x00, (byte) 0x10, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80, + (byte) 0xa2, (byte) 0x28, (byte) 0x8a, (byte) 0xa3, + (byte) 0x38, (byte) 0x8e, (byte) 0x24, (byte) 0x59, + (byte) 0x96, (byte) 0xa6, (byte) 0x69, (byte) 0x9e, + (byte) 0xa7, (byte) 0x7a, (byte) 0xa2, (byte) 0x28, + (byte) 0x9a, (byte) 0xaa, (byte) 0xaa, (byte) 0x8a, + (byte) 0xa6, (byte) 0xa9, (byte) 0xaa, (byte) 0xaa, + (byte) 0x6a, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, + (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, + (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6, + (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, + (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, + (byte) 0xa6, (byte) 0x69, (byte) 0x9a, (byte) 0xa6, + (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, + (byte) 0x9a, (byte) 0xa6, (byte) 0x69, (byte) 0x9a, + (byte) 0xa6, (byte) 0x69, (byte) 0x02, (byte) 0xa1, + (byte) 0x21, (byte) 0xab, (byte) 0x00, (byte) 0x00, + (byte) 0x09, (byte) 0x00, (byte) 0x00, (byte) 0x1d, + (byte) 0xc7, (byte) 0x71, (byte) 0x1c, (byte) 0x47, + (byte) 0x71, (byte) 0x1c, (byte) 0xc7, (byte) 0x71, + (byte) 0x24, (byte) 0x47, (byte) 0x92, (byte) 0x24, + (byte) 0x20, (byte) 0x34, (byte) 0x64, (byte) 0x15, + (byte) 0x00, (byte) 0x20, (byte) 0x03, (byte) 0x00, + (byte) 0x20, (byte) 0x00, (byte) 0x00, (byte) 0x43, + (byte) 0x51, (byte) 0x1c, (byte) 0x45, (byte) 0x72, + (byte) 0x2c, (byte) 0xc7, (byte) 0x92, (byte) 0x34, + (byte) 0x4b, (byte) 0xb3, (byte) 0x3c, (byte) 0xcb, + (byte) 0xd3, (byte) 0x44, (byte) 0xcf, (byte) 0xf4, + (byte) 0x5c, (byte) 0x51, (byte) 0x36, (byte) 0x75, + (byte) 0x53, (byte) 0x57, (byte) 0x6d, (byte) 0x20, + (byte) 0x34, (byte) 0x64, (byte) 0x15, (byte) 0x00, + (byte) 0x00, (byte) 0x08, (byte) 0x00, (byte) 0x20, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0xc7, (byte) 0x73, + (byte) 0x3c, (byte) 0xc7, (byte) 0x73, (byte) 0x3c, + (byte) 0xc9, (byte) 0x93, (byte) 0x3c, (byte) 0xcb, + (byte) 0x73, (byte) 0x3c, (byte) 0xc7, (byte) 0x93, + (byte) 0x3c, (byte) 0x49, (byte) 0xd3, (byte) 0x34, + (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, + (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, + (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, + (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, + (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0xd3, (byte) 0x34, (byte) 0x4d, (byte) 0xd3, + (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, + (byte) 0x4d, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0x03, (byte) 0x42, (byte) 0x43, (byte) 0x56, + (byte) 0x02, (byte) 0x00, (byte) 0x64, (byte) 0x00, + (byte) 0x00, (byte) 0x90, (byte) 0x02, (byte) 0xcf, + (byte) 0x42, (byte) 0x29, (byte) 0x2d, (byte) 0x46, + (byte) 0x02, (byte) 0x1c, (byte) 0x88, (byte) 0x98, + (byte) 0xa3, (byte) 0xd8, (byte) 0x7b, (byte) 0xef, + (byte) 0xbd, (byte) 0xf7, (byte) 0xde, (byte) 0x7b, + (byte) 0x65, (byte) 0x3c, (byte) 0x92, (byte) 0x88, + (byte) 0x49, (byte) 0xed, (byte) 0x31, (byte) 0xf4, + (byte) 0xd4, (byte) 0x31, (byte) 0x07, (byte) 0xb1, + (byte) 0x67, (byte) 0xc6, (byte) 0x23, (byte) 0x66, + (byte) 0x94, (byte) 0xa3, (byte) 0xd8, (byte) 0x29, + (byte) 0xcf, (byte) 0x1c, (byte) 0x42, (byte) 0x0c, + (byte) 0x62, (byte) 0xe8, (byte) 0x3c, (byte) 0x74, + (byte) 0x4a, (byte) 0x31, (byte) 0x88, (byte) 0x29, + (byte) 0xf5, (byte) 0x52, (byte) 0x32, (byte) 0xc6, + (byte) 0x20, (byte) 0xc6, (byte) 0xd8, (byte) 0x63, + (byte) 0x0c, (byte) 0x21, (byte) 0x94, (byte) 0x18, + (byte) 0x08, (byte) 0x0d, (byte) 0x59, (byte) 0x21, + (byte) 0x00, (byte) 0x84, (byte) 0x66, (byte) 0x00, + (byte) 0x18, (byte) 0x24, (byte) 0x09, (byte) 0x90, + (byte) 0x34, (byte) 0x0d, (byte) 0x90, (byte) 0x34, + (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x24, (byte) 0x4f, (byte) 0x03, (byte) 0x34, + (byte) 0x51, (byte) 0x04, (byte) 0x34, (byte) 0x4f, + (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x49, (byte) 0xf3, (byte) 0x00, (byte) 0x4d, + (byte) 0xf4, (byte) 0x00, (byte) 0x4d, (byte) 0x14, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x90, (byte) 0x3c, (byte) 0x0d, (byte) 0xf0, + (byte) 0x44, (byte) 0x11, (byte) 0xd0, (byte) 0x44, + (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44, + (byte) 0x51, (byte) 0x05, (byte) 0x44, (byte) 0xd5, + (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0x4f, + (byte) 0x15, (byte) 0x01, (byte) 0xd1, (byte) 0x54, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x90, (byte) 0x34, (byte) 0x0f, (byte) 0xd0, + (byte) 0x44, (byte) 0x11, (byte) 0xf0, (byte) 0x44, + (byte) 0x11, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x34, (byte) 0x51, (byte) 0x04, (byte) 0x44, + (byte) 0xd5, (byte) 0x04, (byte) 0x3c, (byte) 0x51, + (byte) 0x05, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x4d, (byte) 0x14, (byte) 0x01, (byte) 0xd1, + (byte) 0x54, (byte) 0x01, (byte) 0x51, (byte) 0x15, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x04, + (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x38, + (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x58, + (byte) 0x08, (byte) 0x85, (byte) 0x86, (byte) 0xac, + (byte) 0x08, (byte) 0x00, (byte) 0xe2, (byte) 0x04, + (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10, + (byte) 0x00, (byte) 0x00, (byte) 0x30, (byte) 0xe0, + (byte) 0x00, (byte) 0x00, (byte) 0x10, (byte) 0x60, + (byte) 0x42, (byte) 0x19, (byte) 0x28, (byte) 0x34, + (byte) 0x64, (byte) 0x45, (byte) 0x00, (byte) 0x10, + (byte) 0x27, (byte) 0x00, (byte) 0x60, (byte) 0x70, + (byte) 0x1c, (byte) 0xcb, (byte) 0x02, (byte) 0x00, + (byte) 0x00, (byte) 0x47, (byte) 0x92, (byte) 0x34, + (byte) 0x0d, (byte) 0x00, (byte) 0x00, (byte) 0x1c, + (byte) 0x49, (byte) 0xd2, (byte) 0x34, (byte) 0x00, + (byte) 0x00, (byte) 0xd0, (byte) 0x34, (byte) 0x4d, + (byte) 0x14, (byte) 0x01, (byte) 0x00, (byte) 0xc0, + (byte) 0xd2, (byte) 0x34, (byte) 0x51, (byte) 0x04, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x10, (byte) 0x00, (byte) 0x00, (byte) 0x30, + (byte) 0xe0, (byte) 0x00, (byte) 0x00, (byte) 0x10, + (byte) 0x60, (byte) 0x42, (byte) 0x19, (byte) 0x28, + (byte) 0x34, (byte) 0x64, (byte) 0x25, (byte) 0x00, + (byte) 0x10, (byte) 0x05, (byte) 0x00, (byte) 0x60, + (byte) 0x30, (byte) 0x14, (byte) 0x4d, (byte) 0x03, + (byte) 0x58, (byte) 0x16, (byte) 0xc0, (byte) 0xb2, + (byte) 0x00, (byte) 0x9a, (byte) 0x06, (byte) 0xd0, + (byte) 0x34, (byte) 0x80, (byte) 0xe7, (byte) 0x01, + (byte) 0x3c, (byte) 0x11, (byte) 0x60, (byte) 0x9a, + (byte) 0x00, (byte) 0x40, (byte) 0x00, (byte) 0x00, + (byte) 0x40, (byte) 0x81, (byte) 0x03, (byte) 0x00, + (byte) 0x40, (byte) 0x80, (byte) 0x0d, (byte) 0x9a, + (byte) 0x12, (byte) 0x8b, (byte) 0x03, (byte) 0x14, + (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, + (byte) 0x88, (byte) 0x02, (byte) 0x00, (byte) 0x30, + (byte) 0x28, (byte) 0x8a, (byte) 0x24, (byte) 0x59, + (byte) 0x96, (byte) 0xe7, (byte) 0x41, (byte) 0xd3, + (byte) 0x34, (byte) 0x4d, (byte) 0x14, (byte) 0xa1, + (byte) 0x69, (byte) 0x9a, (byte) 0x26, (byte) 0x8a, + (byte) 0xf0, (byte) 0x3c, (byte) 0xcf, (byte) 0x13, + (byte) 0x45, (byte) 0x78, (byte) 0x9e, (byte) 0xe7, + (byte) 0x99, (byte) 0x26, (byte) 0x44, (byte) 0xd1, + (byte) 0xf3, (byte) 0x4c, (byte) 0x13, (byte) 0xa2, + (byte) 0xe8, (byte) 0x79, (byte) 0xa6, (byte) 0x09, + (byte) 0xd3, (byte) 0x14, (byte) 0x45, (byte) 0xd3, + (byte) 0x04, (byte) 0xa2, (byte) 0x68, (byte) 0x9a, + (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x0a, + (byte) 0x1c, (byte) 0x00, (byte) 0x00, (byte) 0x02, + (byte) 0x6c, (byte) 0xd0, (byte) 0x94, (byte) 0x58, + (byte) 0x1c, (byte) 0xa0, (byte) 0xd0, (byte) 0x90, + (byte) 0x95, (byte) 0x00, (byte) 0x40, (byte) 0x48, + (byte) 0x00, (byte) 0x80, (byte) 0x41, (byte) 0x51, + (byte) 0x2c, (byte) 0xcb, (byte) 0xf3, (byte) 0x44, + (byte) 0x51, (byte) 0x14, (byte) 0x4d, (byte) 0x53, + (byte) 0x55, (byte) 0x5d, (byte) 0x17, (byte) 0x9a, + (byte) 0xe6, (byte) 0x79, (byte) 0xa2, (byte) 0x28, + (byte) 0x8a, (byte) 0xa6, (byte) 0xa9, (byte) 0xaa, + (byte) 0xae, (byte) 0x0b, (byte) 0x4d, (byte) 0xf3, + (byte) 0x3c, (byte) 0x51, (byte) 0x14, (byte) 0x45, + (byte) 0xd3, (byte) 0x54, (byte) 0x55, (byte) 0xd7, + (byte) 0x85, (byte) 0xe7, (byte) 0x79, (byte) 0xa2, + (byte) 0x29, (byte) 0x9a, (byte) 0xa6, (byte) 0x69, + (byte) 0xaa, (byte) 0xaa, (byte) 0xeb, (byte) 0xc2, + (byte) 0xf3, (byte) 0x44, (byte) 0xd1, (byte) 0x34, + (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x55, + (byte) 0xd7, (byte) 0x75, (byte) 0xe1, (byte) 0x79, + (byte) 0xa2, (byte) 0x68, (byte) 0x9a, (byte) 0xa6, + (byte) 0xa9, (byte) 0xaa, (byte) 0xae, (byte) 0xeb, + (byte) 0xba, (byte) 0xf0, (byte) 0x3c, (byte) 0x51, + (byte) 0x34, (byte) 0x4d, (byte) 0xd3, (byte) 0x54, + (byte) 0x55, (byte) 0xd7, (byte) 0x95, (byte) 0x65, + (byte) 0x88, (byte) 0xa2, (byte) 0x28, (byte) 0x9a, + (byte) 0xa6, (byte) 0x69, (byte) 0xaa, (byte) 0xaa, + (byte) 0xeb, (byte) 0xca, (byte) 0x32, (byte) 0x10, + (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x59, + (byte) 0x06, (byte) 0xa2, (byte) 0x68, (byte) 0x9a, + (byte) 0xaa, (byte) 0xea, (byte) 0xba, (byte) 0xae, + (byte) 0x2b, (byte) 0xcb, (byte) 0x40, (byte) 0x14, + (byte) 0x4d, (byte) 0x53, (byte) 0x55, (byte) 0x5d, + (byte) 0xd7, (byte) 0x75, (byte) 0x65, (byte) 0x19, + (byte) 0x98, (byte) 0xa6, (byte) 0x6a, (byte) 0xaa, + (byte) 0xaa, (byte) 0xeb, (byte) 0xca, (byte) 0xb2, + (byte) 0x2c, (byte) 0x03, (byte) 0x4c, (byte) 0x53, + (byte) 0x55, (byte) 0x5d, (byte) 0x57, (byte) 0x96, + (byte) 0x65, (byte) 0x19, (byte) 0xa0, (byte) 0xaa, + (byte) 0xae, (byte) 0xeb, (byte) 0xba, (byte) 0xb2, + (byte) 0x6c, (byte) 0xdb, (byte) 0x00, (byte) 0x55, + (byte) 0x75, (byte) 0x5d, (byte) 0xd7, (byte) 0x95, + (byte) 0x65, (byte) 0xdb, (byte) 0x06, (byte) 0xb8, + (byte) 0xae, (byte) 0xeb, (byte) 0xca, (byte) 0xb2, + (byte) 0x2c, (byte) 0xdb, (byte) 0x36, (byte) 0x00, + (byte) 0xd7, (byte) 0x95, (byte) 0x65, (byte) 0x59, + (byte) 0xb6, (byte) 0x6d, (byte) 0x01, (byte) 0x00, + (byte) 0x00, (byte) 0x07, (byte) 0x0e, (byte) 0x00, + (byte) 0x00, (byte) 0x01, (byte) 0x46, (byte) 0xd0, + (byte) 0x49, (byte) 0x46, (byte) 0x95, (byte) 0x45, + (byte) 0xd8, (byte) 0x68, (byte) 0xc2, (byte) 0x85, + (byte) 0x07, (byte) 0xa0, (byte) 0xd0, (byte) 0x90, + (byte) 0x15, (byte) 0x01, (byte) 0x40, (byte) 0x14, + (byte) 0x00, (byte) 0x00, (byte) 0x60, (byte) 0x8c, + (byte) 0x52, (byte) 0x8a, (byte) 0x29, (byte) 0x65, + (byte) 0x18, (byte) 0x93, (byte) 0x50, (byte) 0x4a, + (byte) 0x09, (byte) 0x0d, (byte) 0x63, (byte) 0x52, + (byte) 0x4a, (byte) 0x2a, (byte) 0xa5, (byte) 0x92, + (byte) 0x92, (byte) 0x52, (byte) 0x4a, (byte) 0xa5, + (byte) 0x54, (byte) 0x12, (byte) 0x52, (byte) 0x4a, + (byte) 0xa9, (byte) 0x94, (byte) 0x4a, (byte) 0x4a, + (byte) 0x4a, (byte) 0x29, (byte) 0x95, (byte) 0x92, + (byte) 0x51, (byte) 0x4a, (byte) 0x29, (byte) 0xb5, + (byte) 0x96, (byte) 0x2a, (byte) 0x29, (byte) 0xa9, + (byte) 0x94, (byte) 0x94, (byte) 0x52, (byte) 0x25, + (byte) 0xa5, (byte) 0xa4, (byte) 0x92, (byte) 0x52, + (byte) 0x2a, (byte) 0x00, (byte) 0x00, (byte) 0xec, + (byte) 0xc0, (byte) 0x01, (byte) 0x00, (byte) 0xec, + (byte) 0xc0, (byte) 0x42, (byte) 0x28, (byte) 0x34, + (byte) 0x64, (byte) 0x25, (byte) 0x00, (byte) 0x90, + (byte) 0x07, (byte) 0x00, (byte) 0x40, (byte) 0x10, + (byte) 0x82, (byte) 0x14, (byte) 0x63, (byte) 0x8c, + (byte) 0x39, (byte) 0x27, (byte) 0xa5, (byte) 0x54, + (byte) 0x8a, (byte) 0x31, (byte) 0xe7, (byte) 0x9c, + (byte) 0x93, (byte) 0x52, (byte) 0x2a, (byte) 0xc5, + (byte) 0x98, (byte) 0x73, (byte) 0xce, (byte) 0x49, + (byte) 0x29, (byte) 0x19, (byte) 0x63, (byte) 0xcc, + (byte) 0x39, (byte) 0xe7, (byte) 0xa4, (byte) 0x94, + (byte) 0x8c, (byte) 0x31, (byte) 0xe6, (byte) 0x9c, + (byte) 0x73, (byte) 0x52, (byte) 0x4a, (byte) 0xc6, + (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0x29, (byte) 0x25, (byte) 0x63, (byte) 0xce, + (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x94, + (byte) 0xd2, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, + (byte) 0x83, (byte) 0x50, (byte) 0x4a, (byte) 0x29, + (byte) 0xa5, (byte) 0x73, (byte) 0xce, (byte) 0x41, + (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x12, + (byte) 0x42, (byte) 0xe7, (byte) 0x20, (byte) 0x94, + (byte) 0x52, (byte) 0x4a, (byte) 0xe9, (byte) 0x9c, + (byte) 0x73, (byte) 0x10, (byte) 0x0a, (byte) 0x00, + (byte) 0x00, (byte) 0x2a, (byte) 0x70, (byte) 0x00, + (byte) 0x00, (byte) 0x08, (byte) 0xb0, (byte) 0x51, + (byte) 0x64, (byte) 0x73, (byte) 0x82, (byte) 0x91, + (byte) 0xa0, (byte) 0x42, (byte) 0x43, (byte) 0x56, + (byte) 0x02, (byte) 0x00, (byte) 0xa9, (byte) 0x00, + (byte) 0x00, (byte) 0x06, (byte) 0xc7, (byte) 0xb1, + (byte) 0x2c, (byte) 0x4d, (byte) 0xd3, (byte) 0x34, + (byte) 0xcf, (byte) 0x13, (byte) 0x45, (byte) 0x4b, + (byte) 0x92, (byte) 0x34, (byte) 0xcf, (byte) 0x13, + (byte) 0x3d, (byte) 0x4f, (byte) 0x14, (byte) 0x4d, + (byte) 0xd5, (byte) 0x92, (byte) 0x24, (byte) 0xcf, + (byte) 0x13, (byte) 0x45, (byte) 0xcf, (byte) 0x13, + (byte) 0x4d, (byte) 0x53, (byte) 0xe5, (byte) 0x79, + (byte) 0x9e, (byte) 0x28, (byte) 0x8a, (byte) 0xa2, + (byte) 0x68, (byte) 0x9a, (byte) 0xaa, (byte) 0x4a, + (byte) 0x14, (byte) 0x45, (byte) 0x4f, (byte) 0x14, + (byte) 0x45, (byte) 0xd1, (byte) 0x34, (byte) 0x55, + (byte) 0x95, (byte) 0x2c, (byte) 0x8b, (byte) 0xa2, + (byte) 0x69, (byte) 0x9a, (byte) 0xa6, (byte) 0xaa, + (byte) 0xba, (byte) 0x2e, (byte) 0x5b, (byte) 0x16, + (byte) 0x45, (byte) 0xd3, (byte) 0x34, (byte) 0x4d, + (byte) 0x55, (byte) 0x75, (byte) 0x5d, (byte) 0x98, + (byte) 0xa6, (byte) 0x28, (byte) 0xaa, (byte) 0xaa, + (byte) 0xeb, (byte) 0xca, (byte) 0x2e, (byte) 0x4c, + (byte) 0x53, (byte) 0x14, (byte) 0x4d, (byte) 0xd3, + (byte) 0x75, (byte) 0x65, (byte) 0x19, (byte) 0xb2, + (byte) 0xad, (byte) 0x9a, (byte) 0xaa, (byte) 0xea, + (byte) 0xba, (byte) 0xb2, (byte) 0x0d, (byte) 0xdb, + (byte) 0x36, (byte) 0x4d, (byte) 0x55, (byte) 0x75, + (byte) 0x5d, (byte) 0x59, (byte) 0x06, (byte) 0xae, + (byte) 0xeb, (byte) 0xba, (byte) 0xb2, (byte) 0x6c, + (byte) 0xeb, (byte) 0xc0, (byte) 0x75, (byte) 0x5d, + (byte) 0x57, (byte) 0x96, (byte) 0x6d, (byte) 0x5d, + (byte) 0x00, (byte) 0x00, (byte) 0x78, (byte) 0x82, + (byte) 0x03, (byte) 0x00, (byte) 0x50, (byte) 0x81, + (byte) 0x0d, (byte) 0xab, (byte) 0x23, (byte) 0x9c, + (byte) 0x14, (byte) 0x8d, (byte) 0x05, (byte) 0x16, + (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, + (byte) 0xc8, (byte) 0x00, (byte) 0x00, (byte) 0x20, + (byte) 0x08, (byte) 0x41, (byte) 0x48, (byte) 0x29, + (byte) 0x85, (byte) 0x90, (byte) 0x52, (byte) 0x0a, + (byte) 0x21, (byte) 0xa5, (byte) 0x14, (byte) 0x42, + (byte) 0x4a, (byte) 0x29, (byte) 0x84, (byte) 0x04, + (byte) 0x00, (byte) 0x00, (byte) 0x0c, (byte) 0x38, + (byte) 0x00, (byte) 0x00, (byte) 0x04, (byte) 0x98, + (byte) 0x50, (byte) 0x06, (byte) 0x0a, (byte) 0x0d, + (byte) 0x59, (byte) 0x09, (byte) 0x00, (byte) 0xa4, + (byte) 0x02, (byte) 0x00, (byte) 0x00, (byte) 0x10, + (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, + (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, + (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, + (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, + (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10, + (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, + (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, + (byte) 0x84, (byte) 0x10, (byte) 0x42, (byte) 0x08, + (byte) 0x21, (byte) 0x84, (byte) 0x10, (byte) 0x42, + (byte) 0x08, (byte) 0x21, (byte) 0x84, (byte) 0x10, + (byte) 0x42, (byte) 0x08, (byte) 0x21, (byte) 0x84, + (byte) 0x10, (byte) 0x42, (byte) 0x08, (byte) 0x21, + (byte) 0x84, (byte) 0xce, (byte) 0x39, (byte) 0xe7, + (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, + (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, + (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, + (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, + (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, + (byte) 0xce, (byte) 0x39, (byte) 0xe7, (byte) 0x9c, + (byte) 0x73, (byte) 0xce, (byte) 0x39, (byte) 0xe7, + (byte) 0x9c, (byte) 0x73, (byte) 0xce, (byte) 0x39, + (byte) 0xe7, (byte) 0x9c, (byte) 0x73, (byte) 0xce, + (byte) 0x39, (byte) 0xe7, (byte) 0x9c, (byte) 0x73, + (byte) 0x02, (byte) 0x00, (byte) 0xb1, (byte) 0x2b, + (byte) 0x1c, (byte) 0x00, (byte) 0x76, (byte) 0x22, + (byte) 0x6c, (byte) 0x58, (byte) 0x1d, (byte) 0xe1, + (byte) 0xa4, (byte) 0x68, (byte) 0x2c, (byte) 0xb0, + (byte) 0xd0, (byte) 0x90, (byte) 0x95, (byte) 0x00, + (byte) 0x40, (byte) 0x38, (byte) 0x00, (byte) 0x00, + (byte) 0x60, (byte) 0x8c, (byte) 0x31, (byte) 0xce, + (byte) 0x59, (byte) 0xac, (byte) 0xb5, (byte) 0xd6, + (byte) 0x5a, (byte) 0x2b, (byte) 0xa5, (byte) 0x94, + (byte) 0x92, (byte) 0x50, (byte) 0x6b, (byte) 0xad, + (byte) 0xb5, (byte) 0xd6, (byte) 0x9a, (byte) 0x29, + (byte) 0xa4, (byte) 0x94, (byte) 0x84, (byte) 0x16, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, + (byte) 0x98, (byte) 0x31, (byte) 0x08, (byte) 0x29, + (byte) 0xb5, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0x31, (byte) 0xc6, (byte) 0x8c, (byte) 0x39, + (byte) 0x47, (byte) 0x2d, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xb6, + (byte) 0x56, (byte) 0x4a, (byte) 0x6c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0xb1, (byte) 0xb5, (byte) 0x52, (byte) 0x62, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, + (byte) 0x16, (byte) 0x5b, (byte) 0x8c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, + (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0x31, (byte) 0xb6, (byte) 0x18, (byte) 0x63, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x8c, (byte) 0x31, (byte) 0xc6, + (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x18, + (byte) 0x63, (byte) 0x6c, (byte) 0x31, (byte) 0xc6, + (byte) 0x18, (byte) 0x63, (byte) 0x8c, (byte) 0x31, + (byte) 0xc6, (byte) 0x18, (byte) 0x63, (byte) 0x8c, + (byte) 0x31, (byte) 0xc6, (byte) 0x18, (byte) 0x63, + (byte) 0x2c, (byte) 0x00, (byte) 0xc0, (byte) 0xe4, + (byte) 0xc1, (byte) 0x01, (byte) 0x00, (byte) 0x2a, + (byte) 0xc1, (byte) 0xc6, (byte) 0x19, (byte) 0x56, + (byte) 0x92, (byte) 0xce, (byte) 0x0a, (byte) 0x47, + (byte) 0x83, (byte) 0x0b, (byte) 0x0d, (byte) 0x59, + (byte) 0x09, (byte) 0x00, (byte) 0xe4, (byte) 0x06, + (byte) 0x00, (byte) 0x00, (byte) 0xc6, (byte) 0x28, + (byte) 0xc5, (byte) 0x98, (byte) 0x63, (byte) 0xce, + (byte) 0x41, (byte) 0x08, (byte) 0xa1, (byte) 0x94, + (byte) 0x12, (byte) 0x4a, (byte) 0x49, (byte) 0xad, + (byte) 0x75, (byte) 0xce, (byte) 0x39, (byte) 0x08, + (byte) 0x21, (byte) 0x94, (byte) 0x52, (byte) 0x4a, + (byte) 0x49, (byte) 0xa9, (byte) 0xb4, (byte) 0x94, + (byte) 0x62, (byte) 0xca, (byte) 0x98, (byte) 0x73, + (byte) 0xce, (byte) 0x41, (byte) 0x08, (byte) 0xa5, + (byte) 0x94, (byte) 0x12, (byte) 0x4a, (byte) 0x49, + (byte) 0xa9, (byte) 0xa5, (byte) 0xd4, (byte) 0x39, + (byte) 0xe7, (byte) 0x20, (byte) 0x94, (byte) 0x52, + (byte) 0x4a, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, + (byte) 0x94, (byte) 0x5a, (byte) 0x6a, (byte) 0xad, + (byte) 0x73, (byte) 0x10, (byte) 0x42, (byte) 0x08, + (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4a, + (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0xd4, + (byte) 0x52, (byte) 0x08, (byte) 0x21, (byte) 0x94, + (byte) 0x52, (byte) 0x4a, (byte) 0x2a, (byte) 0x29, + (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, + (byte) 0xad, (byte) 0xa5, (byte) 0x10, (byte) 0x42, + (byte) 0x28, (byte) 0xa5, (byte) 0x94, (byte) 0x94, + (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, + (byte) 0xd4, (byte) 0x5a, (byte) 0x8b, (byte) 0xa1, + (byte) 0x94, (byte) 0x90, (byte) 0x4a, (byte) 0x29, + (byte) 0x25, (byte) 0xa5, (byte) 0x94, (byte) 0x52, + (byte) 0x49, (byte) 0x2d, (byte) 0xb5, (byte) 0x96, + (byte) 0x5a, (byte) 0x2a, (byte) 0xa1, (byte) 0x94, + (byte) 0x54, (byte) 0x52, (byte) 0x4a, (byte) 0x29, + (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, + (byte) 0xa9, (byte) 0xb5, (byte) 0x56, (byte) 0x4a, + (byte) 0x49, (byte) 0x25, (byte) 0xa5, (byte) 0x94, + (byte) 0x52, (byte) 0x4a, (byte) 0x29, (byte) 0xa5, + (byte) 0xd4, (byte) 0x62, (byte) 0x6b, (byte) 0x29, + (byte) 0x94, (byte) 0x92, (byte) 0x52, (byte) 0x49, + (byte) 0x29, (byte) 0xb5, (byte) 0x94, (byte) 0x52, + (byte) 0x4a, (byte) 0xad, (byte) 0xc5, (byte) 0xd8, + (byte) 0x62, (byte) 0x29, (byte) 0xad, (byte) 0xa4, + (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0x29, + (byte) 0xa5, (byte) 0xd6, (byte) 0x52, (byte) 0x6c, + (byte) 0xad, (byte) 0xb5, (byte) 0xd8, (byte) 0x52, + (byte) 0x4a, (byte) 0x29, (byte) 0xa5, (byte) 0x96, + (byte) 0x5a, (byte) 0x4a, (byte) 0x29, (byte) 0xb5, + (byte) 0x16, (byte) 0x5b, (byte) 0x6a, (byte) 0x2d, + (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x4b, + (byte) 0x29, (byte) 0xa5, (byte) 0x96, (byte) 0x52, + (byte) 0x4b, (byte) 0x2d, (byte) 0xc6, (byte) 0xd6, + (byte) 0x5a, (byte) 0x4b, (byte) 0x29, (byte) 0xa5, + (byte) 0xd4, (byte) 0x52, (byte) 0x6a, (byte) 0xa9, + (byte) 0xa5, (byte) 0x94, (byte) 0x52, (byte) 0x6c, + (byte) 0xad, (byte) 0xb5, (byte) 0x98, (byte) 0x52, + (byte) 0x6a, (byte) 0x2d, (byte) 0xa5, (byte) 0xd4, + (byte) 0x52, (byte) 0x6b, (byte) 0x2d, (byte) 0xb5, + (byte) 0xd8, (byte) 0x52, (byte) 0x6a, (byte) 0x2d, + (byte) 0xb5, (byte) 0x94, (byte) 0x52, (byte) 0x6b, + (byte) 0xa9, (byte) 0xa5, (byte) 0x94, (byte) 0x5a, + (byte) 0x6b, (byte) 0x2d, (byte) 0xb6, (byte) 0xd8, + (byte) 0x5a, (byte) 0x6b, (byte) 0x29, (byte) 0xb5, + (byte) 0x94, (byte) 0x52, (byte) 0x4a, (byte) 0xa9, + (byte) 0xb5, (byte) 0x16, (byte) 0x5b, (byte) 0x8a, + (byte) 0xb1, (byte) 0xb5, (byte) 0xd4, (byte) 0x4a, + (byte) 0x4a, (byte) 0x29, (byte) 0xb5, (byte) 0xd4, + (byte) 0x5a, (byte) 0x6a, (byte) 0x2d, (byte) 0xb6, + (byte) 0x16, (byte) 0x5b, (byte) 0x6b, (byte) 0xad, + (byte) 0xa5, (byte) 0xd6, (byte) 0x5a, (byte) 0x6a, + (byte) 0x29, (byte) 0xa5, (byte) 0x16, (byte) 0x5b, + (byte) 0x8c, (byte) 0x31, (byte) 0xc6, (byte) 0x16, + (byte) 0x63, (byte) 0x6b, (byte) 0x31, (byte) 0xa5, + (byte) 0x94, (byte) 0x52, (byte) 0x4b, (byte) 0xa9, + (byte) 0xa5, (byte) 0x02, (byte) 0x00, (byte) 0x80, + (byte) 0x0e, (byte) 0x1c, (byte) 0x00, (byte) 0x00, + (byte) 0x02, (byte) 0x8c, (byte) 0xa8, (byte) 0xb4, + (byte) 0x10, (byte) 0x3b, (byte) 0xcd, (byte) 0xb8, + (byte) 0xf2, (byte) 0x08, (byte) 0x1c, (byte) 0x51, + (byte) 0xc8, (byte) 0x30, (byte) 0x01, (byte) 0x15, + (byte) 0x1a, (byte) 0xb2, (byte) 0x12, (byte) 0x00, + (byte) 0x20, (byte) 0x03, (byte) 0x00, (byte) 0x20, + (byte) 0x90, (byte) 0x69, (byte) 0x92, (byte) 0x39, + (byte) 0x49, (byte) 0xa9, (byte) 0x11, (byte) 0x26, + (byte) 0x39, (byte) 0xc5, (byte) 0xa0, (byte) 0x94, + (byte) 0xe6, (byte) 0x9c, (byte) 0x53, (byte) 0x4a, + (byte) 0x29, (byte) 0xa5, (byte) 0x34, (byte) 0x44, + (byte) 0x96, (byte) 0x64, (byte) 0x90, (byte) 0x62, + (byte) 0x50, (byte) 0x1d, (byte) 0x99, (byte) 0x8c, + (byte) 0x39, (byte) 0x49, (byte) 0x39, (byte) 0x43, + (byte) 0xa4, (byte) 0x31, (byte) 0xa4, (byte) 0x20, + (byte) 0xf5, (byte) 0x4c, (byte) 0x91, (byte) 0xc7, + (byte) 0x94, (byte) 0x62, (byte) 0x10, (byte) 0x43, + (byte) 0x48, (byte) 0x2a, (byte) 0x74, (byte) 0x8a, + (byte) 0x39, (byte) 0x6c, (byte) 0x35, (byte) 0xf9, + (byte) 0x58, (byte) 0x42, (byte) 0x07, (byte) 0xb1, + (byte) 0x06, (byte) 0x65, (byte) 0x8c, (byte) 0x70, + (byte) 0x29, (byte) 0xc5, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x08, (byte) 0x02, (byte) 0x00, + (byte) 0x04, (byte) 0x84, (byte) 0x04, (byte) 0x00, + (byte) 0x18, (byte) 0x20, (byte) 0x28, (byte) 0x98, + (byte) 0x01, (byte) 0x00, (byte) 0x06, (byte) 0x07, + (byte) 0x08, (byte) 0x23, (byte) 0x07, (byte) 0x02, + (byte) 0x1d, (byte) 0x01, (byte) 0x04, (byte) 0x0e, + (byte) 0x6d, (byte) 0x00, (byte) 0x80, (byte) 0x81, + (byte) 0x08, (byte) 0x99, (byte) 0x09, (byte) 0x0c, + (byte) 0x0a, (byte) 0xa1, (byte) 0xc1, (byte) 0x41, + (byte) 0x26, (byte) 0x00, (byte) 0x3c, (byte) 0x40, + (byte) 0x44, (byte) 0x48, (byte) 0x05, (byte) 0x00, + (byte) 0x89, (byte) 0x09, (byte) 0x8a, (byte) 0xd2, + (byte) 0x85, (byte) 0x2e, (byte) 0x08, (byte) 0x21, + (byte) 0x82, (byte) 0x74, (byte) 0x11, (byte) 0x64, + (byte) 0xf1, (byte) 0xc0, (byte) 0x85, (byte) 0x13, + (byte) 0x37, (byte) 0x9e, (byte) 0xb8, (byte) 0xe1, + (byte) 0x84, (byte) 0x0e, (byte) 0x6d, (byte) 0x20, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x20, (byte) 0x00, (byte) 0xf0, + (byte) 0x01, (byte) 0x00, (byte) 0x90, (byte) 0x50, + (byte) 0x00, (byte) 0x11, (byte) 0x11, (byte) 0xd1, + (byte) 0xcc, (byte) 0x55, (byte) 0x58, (byte) 0x5c, + (byte) 0x60, (byte) 0x64, (byte) 0x68, (byte) 0x6c, + (byte) 0x70, (byte) 0x74, (byte) 0x78, (byte) 0x7c, + (byte) 0x80, (byte) 0x84, (byte) 0x08, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x10, (byte) 0x00, (byte) 0x7c, (byte) 0x00, + (byte) 0x00, (byte) 0x24, (byte) 0x22, (byte) 0x40, + (byte) 0x44, (byte) 0x44, (byte) 0x34, (byte) 0x73, + (byte) 0x15, (byte) 0x16, (byte) 0x17, (byte) 0x18, + (byte) 0x19, (byte) 0x1a, (byte) 0x1b, (byte) 0x1c, + (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, (byte) 0x20, + (byte) 0x21, (byte) 0x01, (byte) 0x00, (byte) 0x80, + (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x20, (byte) 0x80, + (byte) 0x00, (byte) 0x04, (byte) 0x04, (byte) 0x04, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x02, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x04, (byte) 0x04 + }; +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java similarity index 99% rename from library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java index 0b5740b72c..93611b9b0a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/RobolectricUtil.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2; +package com.google.android.exoplayer2.testutil; import static org.robolectric.Shadows.shadowOf; import static org.robolectric.util.ReflectionHelpers.callInstanceMethod; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java similarity index 99% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 40d5b6c3f9..af8b10e6d3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -271,5 +271,4 @@ public abstract class StubExoPlayer implements ExoPlayer { public long getContentPosition() { throw new UnsupportedOperationException(); } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java similarity index 84% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java rename to testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 42136bfe4d..abef8e06be 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -23,9 +23,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -/** - * Unit test for {@link Timeline}. - */ +/** Unit test for {@link Timeline}. */ public final class TimelineAsserts { private static final int[] REPEAT_MODES = { @@ -34,9 +32,7 @@ public final class TimelineAsserts { private TimelineAsserts() {} - /** - * Assert that timeline is empty (i.e. has no windows or periods). - */ + /** Assert that timeline is empty (i.e. has no windows or periods). */ public static void assertEmpty(Timeline timeline) { assertWindowIds(timeline); assertPeriodCounts(timeline); @@ -63,9 +59,7 @@ public final class TimelineAsserts { } } - /** - * Asserts that window properties {@link Window}.isDynamic are set correctly. - */ + /** Asserts that window properties {@link Window}.isDynamic are set correctly. */ public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) { Window window = new Window(); for (int i = 0; i < timeline.getWindowCount(); i++) { @@ -78,8 +72,10 @@ public final class TimelineAsserts { * Asserts that previous window indices for each window depending on the repeat mode and the * shuffle mode are equal to the given sequence. */ - public static void assertPreviousWindowIndices(Timeline timeline, - @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, + public static void assertPreviousWindowIndices( + Timeline timeline, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertThat(timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled)) @@ -88,11 +84,14 @@ public final class TimelineAsserts { } /** - * Asserts that next window indices for each window depending on the repeat mode and the - * shuffle mode are equal to the given sequence. + * Asserts that next window indices for each window depending on the repeat mode and the shuffle + * mode are equal to the given sequence. */ - public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled, int... expectedNextWindowIndices) { + public static void assertNextWindowIndices( + Timeline timeline, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertThat(timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled)) .isEqualTo(expectedNextWindowIndices[i]); @@ -113,9 +112,9 @@ public final class TimelineAsserts { } /** - * Asserts that period counts for each window are set correctly. Also asserts that - * {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it - * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}. + * Asserts that period counts for each window are set correctly. Also asserts that {@link + * Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it asserts + * the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}. */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); @@ -147,8 +146,8 @@ public final class TimelineAsserts { .isEqualTo(i + 1); } else { int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false); - int nextPeriod = nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindow]; + int nextPeriod = + nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindow]; assertThat(timeline.getNextPeriodIndex(i, period, window, repeatMode, false)) .isEqualTo(nextPeriod); } @@ -156,9 +155,7 @@ public final class TimelineAsserts { } } - /** - * Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. - */ + /** Asserts that periods' {@link Period#getAdGroupCount()} are set correctly. */ public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) { Period period = new Period(); for (int i = 0; i < timeline.getPeriodCount(); i++) { @@ -166,5 +163,4 @@ public final class TimelineAsserts { assertThat(period.getAdGroupCount()).isEqualTo(expectedAdGroupCounts[i]); } } - } From 803f15dec746be68f845d7a1e179a27edd789cdd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Feb 2018 08:40:02 -0800 Subject: [PATCH 071/376] Sync issue template ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187026802 --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From aa570621991904c18291369d9e804c94351cad70 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 27 Feb 2018 13:57:18 +0000 Subject: [PATCH 072/376] Restore equivalence --- README.md | 4 +- .../extractor/mkv/MatroskaExtractor.java | 18 +- .../exoplayer2/offline/DownloadService.java | 1 + .../upstream/DefaultHttpDataSource.java | 4 +- .../android/exoplayer2/util/EventLogger.java | 243 +++++++++++------- 5 files changed, 166 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index 8755ac588d..13dfaddab3 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ repositories { } ``` -Next add a gradle compile dependency to the `build.gradle` file of your app -module. The following will add a dependency to the full library: +Next add a dependency in the `build.gradle` file of your app module. The +following will add a dependency to the full library: ```gradle implementation 'com.google.android.exoplayer:exoplayer:2.X.X' 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 c2b3735265..79537e1c24 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 @@ -1929,12 +1929,12 @@ public final class MatroskaExtractor implements Extractor { /** * Builds initialization data for a {@link Format} from FourCC codec private data. - *

- * VC1 and H263 are the only supported compression types. * - * @return A pair object with the first object being the codec mime type - * and the second object the initialization data for the {@link Format}, - * or null if the compression type is not a currently supported type (VC1 or H263). + *

VC1 and H263 are the only supported compression types. + * + * @return The codec mime type and initialization data. If the compression type is not supported + * then the mime type is set to {@link MimeTypes#VIDEO_UNKNOWN} and the initialization data + * is {@code null}. * @throws ParserException If the initialization data could not be built. */ private static Pair> parseFourCcPrivate(ParsableByteArray buffer) @@ -1944,14 +1944,16 @@ public final class MatroskaExtractor implements Extractor { long compression = buffer.readLittleEndianUnsignedInt(); if (compression == FOURCC_COMPRESSION_DIVX) { return new Pair<>(MimeTypes.VIDEO_H263, null); - } else if (compression == FOURCC_COMPRESSION_VC1) { + } else if (compression == FOURCC_COMPRESSION_VC1) { // Search for the initialization data from the end of the BITMAPINFOHEADER. The last 20 // bytes of which are: sizeImage(4), xPel/m (4), yPel/m (4), clrUsed(4), clrImportant(4). int startOffset = buffer.getPosition() + 20; byte[] bufferData = buffer.data; for (int offset = startOffset; offset < bufferData.length - 4; offset++) { - if (bufferData[offset] == 0x00 && bufferData[offset + 1] == 0x00 - && bufferData[offset + 2] == 0x01 && bufferData[offset + 3] == 0x0F) { + if (bufferData[offset] == 0x00 + && bufferData[offset + 1] == 0x00 + && bufferData[offset + 2] == 0x01 + && bufferData[offset + 3] == 0x0F) { // We've found the initialization data. byte[] initializationData = Arrays.copyOfRange(bufferData, offset, bufferData.length); return new Pair<>(MimeTypes.VIDEO_VC1, Collections.singletonList(initializationData)); 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 0a6bc062f1..a5656ec109 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 @@ -289,6 +289,7 @@ public abstract class DownloadService extends Service implements DownloadManager @Override public void onIdle(DownloadManager downloadManager) { // Make sure startForeground is called before stopping. + // Workaround for https://buganizer.corp.google.com/issues/69424260 if (Util.SDK_INT >= 26) { Builder notificationBuilder = new Builder(this, getNotificationChannelId()); Notification foregroundNotification = notificationBuilder.build(); 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 e8b7f00a56..d45b7182c4 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 @@ -614,8 +614,8 @@ public class DefaultHttpDataSource implements HttpDataSource { } String className = inputStream.getClass().getName(); if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream".equals(className) - || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream".equals( - className)) { + || "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream" + .equals(className)) { Class superclass = inputStream.getClass().getSuperclass(); Method unexpectedEndOfInput = superclass.getDeclaredMethod("unexpectedEndOfInput"); unexpectedEndOfInput.setAccessible(true); 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 1cac8958c4..df555b824a 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 @@ -89,173 +89,207 @@ public class EventLogger @Override public void onLoadingChanged(boolean isLoading) { - logd(TAG, "loading [" + isLoading + "]"); + logd("loading [" + isLoading + "]"); } @Override public void onPlayerStateChanged(boolean playWhenReady, int state) { - logd(TAG, "state [" + getSessionTimeString() + ", " + playWhenReady + ", " - + getStateString(state) + "]"); + logd( + "state [" + + getSessionTimeString() + + ", " + + playWhenReady + + ", " + + getStateString(state) + + "]"); } @Override public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - logd(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); + logd("repeatMode [" + getRepeatModeString(repeatMode) + "]"); } @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - logd(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); + logd("shuffleModeEnabled [" + shuffleModeEnabled + "]"); } @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - logd(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); + logd("positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - logd(TAG, "playbackParameters " + String.format( - "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); + logd( + "playbackParameters " + + String.format( + "[speed=%.2f, pitch=%.2f]", playbackParameters.speed, playbackParameters.pitch)); } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); - logd(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount - + ", reason=" + getTimelineChangeReasonString(reason)); + logd( + "timelineChanged [periodCount=" + + periodCount + + ", windowCount=" + + windowCount + + ", reason=" + + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getPeriod(i, period); - logd(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); + logd(" " + "period [" + getTimeString(period.getDurationMs()) + "]"); } if (periodCount > MAX_TIMELINE_ITEM_LINES) { - logd(TAG, " ..."); + logd(" ..."); } for (int i = 0; i < Math.min(windowCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getWindow(i, window); - logd(TAG, " " + "window [" + getTimeString(window.getDurationMs()) + ", " - + window.isSeekable + ", " + window.isDynamic + "]"); + logd( + " " + + "window [" + + getTimeString(window.getDurationMs()) + + ", " + + window.isSeekable + + ", " + + window.isDynamic + + "]"); } if (windowCount > MAX_TIMELINE_ITEM_LINES) { - logd(TAG, " ..."); + logd(" ..."); } - logd(TAG, "]"); + logd("]"); } @Override public void onPlayerError(ExoPlaybackException e) { - loge(TAG, "playerFailed [" + getSessionTimeString() + "]", e); + loge("playerFailed [" + getSessionTimeString() + "]", e); } @Override public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo == null) { - logd(TAG, "Tracks []"); + logd("Tracks []"); return; } - logd(TAG, "Tracks ["); + logd("Tracks ["); // Log tracks associated to renderers. for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) { TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); TrackSelection trackSelection = trackSelections.get(rendererIndex); if (rendererTrackGroups.length > 0) { - logd(TAG, " Renderer:" + rendererIndex + " ["); + logd(" Renderer:" + rendererIndex + " ["); for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) { TrackGroup trackGroup = rendererTrackGroups.get(groupIndex); - String adaptiveSupport = getAdaptiveSupportString(trackGroup.length, - mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); - logd(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); + String adaptiveSupport = + getAdaptiveSupportString( + trackGroup.length, + mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false)); + logd(" Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " ["); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(trackSelection, trackGroup, trackIndex); - String formatSupport = getFormatSupportString( - mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); - logd(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); + String formatSupport = + getFormatSupportString( + mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); } - logd(TAG, " ]"); + logd(" ]"); } // Log metadata for at most one of the tracks selected for the renderer. if (trackSelection != null) { for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) { Metadata metadata = trackSelection.getFormat(selectionIndex).metadata; if (metadata != null) { - logd(TAG, " Metadata ["); + logd(" Metadata ["); printMetadata(metadata, " "); - logd(TAG, " ]"); + logd(" ]"); break; } } } - logd(TAG, " ]"); + logd(" ]"); } } // Log tracks not associated with a renderer. TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups(); if (unassociatedTrackGroups.length > 0) { - logd(TAG, " Renderer:None ["); + logd(" Renderer:None ["); for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) { - logd(TAG, " Group:" + groupIndex + " ["); + logd(" Group:" + groupIndex + " ["); TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex); for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { String status = getTrackStatusString(false); - String formatSupport = getFormatSupportString( - RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); - logd(TAG, " " + status + " Track:" + trackIndex + ", " - + Format.toLogString(trackGroup.getFormat(trackIndex)) - + ", supported=" + formatSupport); + String formatSupport = + getFormatSupportString(RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); + logd( + " " + + status + + " Track:" + + trackIndex + + ", " + + Format.toLogString(trackGroup.getFormat(trackIndex)) + + ", supported=" + + formatSupport); } - logd(TAG, " ]"); + logd(" ]"); } - logd(TAG, " ]"); + logd(" ]"); } - logd(TAG, "]"); + logd("]"); } @Override public void onSeekProcessed() { - logd(TAG, "seekProcessed"); + logd("seekProcessed"); } // MetadataOutput @Override public void onMetadata(Metadata metadata) { - logd(TAG, "onMetadata ["); + logd("onMetadata ["); printMetadata(metadata, " "); - logd(TAG, "]"); + logd("]"); } // AudioRendererEventListener @Override public void onAudioEnabled(DecoderCounters counters) { - logd(TAG, "audioEnabled [" + getSessionTimeString() + "]"); + logd("audioEnabled [" + getSessionTimeString() + "]"); } @Override public void onAudioSessionId(int audioSessionId) { - logd(TAG, "audioSessionId [" + audioSessionId + "]"); + logd("audioSessionId [" + audioSessionId + "]"); } @Override - public void onAudioDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - logd(TAG, "audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + public void onAudioDecoderInitialized( + String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { + logd("audioDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); } @Override public void onAudioInputFormatChanged(Format format) { - logd(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); + logd("audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); } @Override public void onAudioDisabled(DecoderCounters counters) { - logd(TAG, "audioDisabled [" + getSessionTimeString() + "]"); + logd("audioDisabled [" + getSessionTimeString() + "]"); } @Override @@ -268,40 +302,39 @@ public class EventLogger @Override public void onVideoEnabled(DecoderCounters counters) { - logd(TAG, "videoEnabled [" + getSessionTimeString() + "]"); + logd("videoEnabled [" + getSessionTimeString() + "]"); } @Override - public void onVideoDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - logd(TAG, "videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); + public void onVideoDecoderInitialized( + String decoderName, long elapsedRealtimeMs, long initializationDurationMs) { + logd("videoDecoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]"); } @Override public void onVideoInputFormatChanged(Format format) { - logd(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) - + "]"); + logd("videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format) + "]"); } @Override public void onVideoDisabled(DecoderCounters counters) { - logd(TAG, "videoDisabled [" + getSessionTimeString() + "]"); + logd("videoDisabled [" + getSessionTimeString() + "]"); } @Override public void onDroppedFrames(int count, long elapsed) { - logd(TAG, "droppedFrames [" + getSessionTimeString() + ", " + count + "]"); + logd("droppedFrames [" + getSessionTimeString() + ", " + count + "]"); } @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - logd(TAG, "videoSizeChanged [" + width + ", " + height + "]"); + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + logd("videoSizeChanged [" + width + ", " + height + "]"); } @Override public void onRenderedFirstFrame(Surface surface) { - logd(TAG, "renderedFirstFrame [" + surface + "]"); + logd("renderedFirstFrame [" + surface + "]"); } // DefaultDrmSessionManager.EventListener @@ -313,17 +346,17 @@ public class EventLogger @Override public void onDrmKeysRestored() { - logd(TAG, "drmKeysRestored [" + getSessionTimeString() + "]"); + logd("drmKeysRestored [" + getSessionTimeString() + "]"); } @Override public void onDrmKeysRemoved() { - logd(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]"); + logd("drmKeysRemoved [" + getSessionTimeString() + "]"); } @Override public void onDrmKeysLoaded() { - logd(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); + logd("drmKeysLoaded [" + getSessionTimeString() + "]"); } // MediaSourceEventListener @@ -397,10 +430,29 @@ public class EventLogger // Do nothing. } + /** + * Logs a debug message. + * + * @param msg The message to log. + */ + protected void logd(String msg) { + Log.d(TAG, msg); + } + + /** + * Logs an error message and exception. + * + * @param msg The message to log. + * @param tr The exception to log. + */ + protected void loge(String msg, Throwable tr) { + Log.e(TAG, msg, tr); + } + // Internal methods private void printInternalError(String type, Exception e) { - loge(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e); + loge("internalError [" + getSessionTimeString() + ", " + type + "]", e); } private void printMetadata(Metadata metadata, String prefix) { @@ -408,37 +460,51 @@ public class EventLogger Metadata.Entry entry = metadata.get(i); if (entry instanceof TextInformationFrame) { TextInformationFrame textInformationFrame = (TextInformationFrame) entry; - logd(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id, - textInformationFrame.value)); + logd( + prefix + + String.format( + "%s: value=%s", textInformationFrame.id, textInformationFrame.value)); } else if (entry instanceof UrlLinkFrame) { UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry; - logd(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); + logd(prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url)); } else if (entry instanceof PrivFrame) { PrivFrame privFrame = (PrivFrame) entry; - logd(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); + logd(prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); } else if (entry instanceof GeobFrame) { GeobFrame geobFrame = (GeobFrame) entry; - logd(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s", - geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); + logd( + prefix + + String.format( + "%s: mimeType=%s, filename=%s, description=%s", + geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description)); } else if (entry instanceof ApicFrame) { ApicFrame apicFrame = (ApicFrame) entry; - logd(TAG, prefix + String.format("%s: mimeType=%s, description=%s", - apicFrame.id, apicFrame.mimeType, apicFrame.description)); + logd( + prefix + + String.format( + "%s: mimeType=%s, description=%s", + apicFrame.id, apicFrame.mimeType, apicFrame.description)); } else if (entry instanceof CommentFrame) { CommentFrame commentFrame = (CommentFrame) entry; - logd(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id, - commentFrame.language, commentFrame.description)); + logd( + prefix + + String.format( + "%s: language=%s, description=%s", + commentFrame.id, commentFrame.language, commentFrame.description)); } else if (entry instanceof Id3Frame) { Id3Frame id3Frame = (Id3Frame) entry; - logd(TAG, prefix + String.format("%s", id3Frame.id)); + logd(prefix + id3Frame.id); } else if (entry instanceof EventMessage) { EventMessage eventMessage = (EventMessage) entry; - logd(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", - eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); + logd( + prefix + + String.format( + "EMSG: scheme=%s, id=%d, value=%s", + eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); } else if (entry instanceof SpliceCommand) { String description = String.format("SCTE-35 splice command: type=%s.", entry.getClass().getSimpleName()); - logd(TAG, prefix + description); + logd(prefix + description); } } } @@ -555,11 +621,4 @@ public class EventLogger } } - protected void logd(String tag, String msg) { - Log.d(tag, msg); - } - - protected void loge(String tag, String msg, Throwable tr) { - Log.e(tag, msg, tr); - } } From 5d1c84cebca32a629d90f6ac1d6a798f43be064f Mon Sep 17 00:00:00 2001 From: Sven Wischnowsky Date: Wed, 28 Feb 2018 18:08:03 +0100 Subject: [PATCH 073/376] support zlib compressed PGS subtitles --- .../exoplayer2/text/pgs/PgsDecoder.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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 6d60da7d81..e1fef19bfa 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 @@ -25,6 +25,10 @@ import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.zip.InflaterInputStream; /** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ public final class PgsDecoder extends SimpleSubtitleDecoder { @@ -34,18 +38,29 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { private static final int SECTION_TYPE_IDENTIFIER = 0x16; private static final int SECTION_TYPE_END = 0x80; + private static final int INFLATE_HEADER = 0x78; + private static final int INFLATE_BUFFER_SIZE = 5; + private final ParsableByteArray buffer; private final CueBuilder cueBuilder; + private final ByteArrayOutputStream inflateBuffer; + private final byte[] inflateReadBuffer; public PgsDecoder() { super("PgsDecoder"); buffer = new ParsableByteArray(); cueBuilder = new CueBuilder(); + inflateBuffer = new ByteArrayOutputStream(); + inflateReadBuffer = new byte[INFLATE_BUFFER_SIZE]; } @Override protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { - buffer.reset(data, size); + byte[] inflated = tryInflateBuffer(data, size); + if (inflated == null) + buffer.reset(data, size); + else + buffer.reset(inflated, inflated.length); cueBuilder.reset(); ArrayList cues = new ArrayList<>(); while (buffer.bytesLeft() >= 3) { @@ -57,6 +72,25 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { return new PgsSubtitle(Collections.unmodifiableList(cues)); } + private byte[] tryInflateBuffer(byte[] data, int size) { + if (size > 0 && (((int) data[0]) & 0xff) != INFLATE_HEADER) return null; + + inflateBuffer.reset(); + + try { + InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, 0, size)); + int len = -1; + + while ((len = iis.read(inflateReadBuffer)) != -1) { + inflateBuffer.write(inflateReadBuffer, 0, len); + } + return inflateBuffer.toByteArray(); + } + catch (IOException e) { } + + return null; + } + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { int limit = buffer.limit(); int sectionType = buffer.readUnsignedByte(); From 322e15f63b358bc5f16c9d0abdeaa26a86d40cb0 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 27 Feb 2018 06:01:13 -0800 Subject: [PATCH 074/376] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187165173 --- .../com/google/android/exoplayer2/offline/DownloadService.java | 1 - 1 file changed, 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 a5656ec109..0a6bc062f1 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 @@ -289,7 +289,6 @@ public abstract class DownloadService extends Service implements DownloadManager @Override public void onIdle(DownloadManager downloadManager) { // Make sure startForeground is called before stopping. - // Workaround for https://buganizer.corp.google.com/issues/69424260 if (Util.SDK_INT >= 26) { Builder notificationBuilder = new Builder(this, getNotificationChannelId()); Notification foregroundNotification = notificationBuilder.build(); From b489f58cdbf3581ef7091e3d1af5bb671869ca6d Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 27 Feb 2018 08:37:07 -0800 Subject: [PATCH 075/376] Change listener notification in DynamicConcatenatingMediaSource. Up to now we use a boolean "preventListenerNotification" to suppress updates while other operations are still in progress. This ensures, for example, that only one initial timeline is issued even for multiple child sources. As soon as we allow to reuse the same instance, it becomes increasingly difficult to manage this manual listener notification suppression. Therefore, this change schedules an update as a new message on the playback thread immediately after the current message. This way, we also ensure that all simultaneous updates within one looper message iteration are reported together. Issue:#3498 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187180342 --- .../DynamicConcatenatingMediaSource.java | 116 ++++++++++-------- 1 file changed, 64 insertions(+), 52 deletions(-) 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 index f52c1bfd0f..6d57cca440 100644 --- 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 @@ -48,7 +48,8 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private static final int MSG_ADD_MULTIPLE = 1; private static final int MSG_REMOVE = 2; private static final int MSG_MOVE = 3; - private static final int MSG_ON_COMPLETION = 4; + private static final int MSG_NOTIFY_LISTENER = 4; + private static final int MSG_ON_COMPLETION = 5; // Accessed on the app thread. private final List mediaSourcesPublic; @@ -58,12 +59,13 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private final MediaSourceHolder query; private final Map mediaSourceByMediaPeriod; private final List deferredMediaPeriods; + private final List pendingOnCompletionActions; private final boolean isAtomic; private ExoPlayer player; private Listener listener; + private boolean listenerNotificationScheduled; private ShuffleOrder shuffleOrder; - private boolean preventListenerNotification; private int windowCount; private int periodCount; @@ -98,6 +100,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); this.deferredMediaPeriods = new ArrayList<>(1); + this.pendingOnCompletionActions = new ArrayList<>(); this.query = new MediaSourceHolder(null, null, -1, -1, -1); this.isAtomic = isAtomic; } @@ -349,11 +352,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< super.prepareSource(player, isTopLevelSource, listener); this.player = player; this.listener = listener; - preventListenerNotification = true; shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); - preventListenerNotification = false; - maybeNotifyListener(null); + scheduleListenerNotification(/* actionOnCompletion= */ null); } @Override @@ -412,62 +413,73 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Override @SuppressWarnings("unchecked") public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_ON_COMPLETION) { - ((EventDispatcher) message).dispatchEvent(); - return; - } - preventListenerNotification = true; - EventDispatcher actionOnCompletion; switch (messageType) { - case MSG_ADD: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, 1); - addMediaSourceInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; + case MSG_ADD: + MessageData addMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); + addMediaSourceInternal(addMessage.index, addMessage.customData); + scheduleListenerNotification(addMessage.actionOnCompletion); break; - } - case MSG_ADD_MULTIPLE: { - MessageData> messageData = + case MSG_ADD_MULTIPLE: + MessageData> addMultipleMessage = (MessageData>) message; - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.index, - messageData.customData.size()); - addMediaSourcesInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; + shuffleOrder = + shuffleOrder.cloneAndInsert( + addMultipleMessage.index, addMultipleMessage.customData.size()); + addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); + scheduleListenerNotification(addMultipleMessage.actionOnCompletion); break; - } - case MSG_REMOVE: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); - removeMediaSourceInternal(messageData.index); - actionOnCompletion = messageData.actionOnCompletion; + case MSG_REMOVE: + MessageData removeMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); + removeMediaSourceInternal(removeMessage.index); + scheduleListenerNotification(removeMessage.actionOnCompletion); break; - } - case MSG_MOVE: { - MessageData messageData = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(messageData.index); - shuffleOrder = shuffleOrder.cloneAndInsert(messageData.customData, 1); - moveMediaSourceInternal(messageData.index, messageData.customData); - actionOnCompletion = messageData.actionOnCompletion; + case MSG_MOVE: + MessageData moveMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); + shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); + moveMediaSourceInternal(moveMessage.index, moveMessage.customData); + scheduleListenerNotification(moveMessage.actionOnCompletion); break; - } - default: { + case MSG_NOTIFY_LISTENER: + notifyListener(); + break; + case MSG_ON_COMPLETION: + List actionsOnCompletion = ((List) message); + for (int i = 0; i < actionsOnCompletion.size(); i++) { + actionsOnCompletion.get(i).dispatchEvent(); + } + break; + default: throw new IllegalStateException(); - } } - preventListenerNotification = false; - maybeNotifyListener(actionOnCompletion); } - private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) { - if (!preventListenerNotification) { - listener.onSourceInfoRefreshed( - this, - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - null); - if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); - } + private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) { + if (!listenerNotificationScheduled) { + player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); + listenerNotificationScheduled = true; + } + if (actionOnCompletion != null) { + pendingOnCompletionActions.add(actionOnCompletion); + } + } + + private void notifyListener() { + listenerNotificationScheduled = false; + List actionsOnCompletion = + pendingOnCompletionActions.isEmpty() + ? Collections.emptyList() + : new ArrayList<>(pendingOnCompletionActions); + pendingOnCompletionActions.clear(); + listener.onSourceInfoRefreshed( + this, + new ConcatenatedTimeline( + mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), + /* manifest= */ null); + if (!actionsOnCompletion.isEmpty()) { + player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send(); } } @@ -528,7 +540,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< } } mediaSourceHolder.isPrepared = true; - maybeNotifyListener(null); + scheduleListenerNotification(/* actionOnCompletion= */ null); } private void removeMediaSourceInternal(int index) { From b2c445776aaab420e7b971d2f2feb95846c41624 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 27 Feb 2018 09:29:49 -0800 Subject: [PATCH 076/376] Allow parallel reuse of media sources. This is achieved by adding a BaseMediaSource which keeps a reference count of the number of times the source has been prepared and forwards to the actual implementations only once, such that only minimal changes are needed for each media source. Issue:#3498 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187186691 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 21 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../exoplayer2/source/BaseMediaSource.java | 95 +++++++++++++++++++ .../source/ClippingMediaSource.java | 13 +-- .../source/CompositeMediaSource.java | 48 ++++++---- .../source/ConcatenatingMediaSource.java | 23 ++--- .../DynamicConcatenatingMediaSource.java | 58 +++-------- .../source/ExtractorMediaSource.java | 32 +++---- .../exoplayer2/source/LoopingMediaSource.java | 13 +-- .../exoplayer2/source/MediaSource.java | 54 ++++++----- .../exoplayer2/source/MergingMediaSource.java | 13 +-- .../source/SingleSampleMediaSource.java | 8 +- .../exoplayer2/source/ads/AdsMediaSource.java | 13 +-- .../android/exoplayer2/ExoPlayerTest.java | 6 +- .../source/ConcatenatingMediaSourceTest.java | 57 +++++++++++ .../DynamicConcatenatingMediaSourceTest.java | 77 +++++++++++---- .../source/dash/DashMediaSource.java | 11 +-- .../exoplayer2/source/hls/HlsMediaSource.java | 19 ++-- .../source/smoothstreaming/SsMediaSource.java | 18 ++-- .../exoplayer2/testutil/FakeMediaSource.java | 19 ++-- .../exoplayer2/testutil/FakeTimeline.java | 12 +-- .../testutil/MediaSourceTestRunner.java | 4 +- .../exoplayer2/testutil/TimelineAsserts.java | 1 + 24 files changed, 384 insertions(+), 236 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b71b54575b..b1ae541411 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Downloading: Add `DownloadService`, `DownloadManager` and related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). +* MediaSources: Allow reusing media sources after they have been released and + also in parallel to allow adding them multiple times to a concatenation. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). ### 2.7.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 1899c815da..1010a27178 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import android.view.ViewGroup; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -33,10 +34,12 @@ import java.io.IOException; * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ @Deprecated -public final class ImaAdsMediaSource implements MediaSource { +public final class ImaAdsMediaSource extends BaseMediaSource { private final AdsMediaSource adsMediaSource; + private Listener adsMediaSourceListener; + /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. @@ -74,18 +77,16 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public void prepareSource( - final ExoPlayer player, boolean isTopLevelSource, final Listener listener) { - adsMediaSource.prepareSource( - player, - isTopLevelSource, + public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { + adsMediaSourceListener = new Listener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { - listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } - }); + }; + adsMediaSource.prepareSource(player, isTopLevelSource, adsMediaSourceListener); } @Override @@ -104,7 +105,7 @@ public final class ImaAdsMediaSource implements MediaSource { } @Override - public void releaseSource() { - adsMediaSource.releaseSource(); + public void releaseSourceInternal() { + adsMediaSource.releaseSource(adsMediaSourceListener); } } 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 e05068a7b3..2272bef573 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 @@ -799,7 +799,7 @@ import java.util.Collections; resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); if (releaseMediaSource) { if (mediaSource != null) { - mediaSource.releaseSource(); + mediaSource.releaseSource(/* listener= */ this); mediaSource = null; } } 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 new file mode 100644 index 0000000000..a6924b5e05 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/BaseMediaSource.java @@ -0,0 +1,95 @@ +/* + * 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.source; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import java.util.ArrayList; + +/** + * Base {@link MediaSource} implementation to handle parallel reuse. + * + *

Whenever an implementing subclass needs to provide a new timeline and/or manifest, it must + * call {@link #refreshSourceInfo(Timeline, Object)} to notify all listeners. + */ +public abstract class BaseMediaSource implements MediaSource { + + private final ArrayList sourceInfoListeners; + + private ExoPlayer player; + private Timeline timeline; + private Object manifest; + + public BaseMediaSource() { + sourceInfoListeners = new ArrayList<>(/* initialCapacity= */ 1); + } + + /** + * Starts source preparation. This method is called at most once until the next call to {@link + * #releaseSourceInternal()}. + * + * @param player The player for which this source is being prepared. + * @param isTopLevelSource Whether this source has been passed directly to {@link + * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean, + * boolean)}. + */ + protected abstract void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource); + + /** + * Releases the source. This method is called exactly once after each call to {@link + * #prepareSourceInternal(ExoPlayer, boolean)}. + */ + protected abstract void releaseSourceInternal(); + + /** + * 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) { + this.timeline = timeline; + this.manifest = manifest; + for (Listener listener : sourceInfoListeners) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } + } + + @Override + public final void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkArgument(this.player == null || this.player == player); + sourceInfoListeners.add(listener); + if (this.player == null) { + this.player = player; + prepareSourceInternal(player, isTopLevelSource); + } else if (timeline != null) { + listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); + } + } + + @Override + public final void releaseSource(Listener listener) { + sourceInfoListeners.remove(listener); + if (sourceInfoListeners.isEmpty()) { + player = 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 9ff704e75a..e25795443e 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 @@ -84,7 +84,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final boolean enableInitialDiscontinuity; private final ArrayList mediaPeriods; - private MediaSource.Listener sourceListener; private IllegalClippingException clippingError; /** @@ -131,9 +130,8 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); prepareChildSource(/* id= */ null, mediaSource); } @@ -161,10 +159,9 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); + public void releaseSourceInternal() { + super.releaseSourceInternal(); clippingError = null; - sourceListener = null; } @Override @@ -180,7 +177,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { clippingError = e; return; } - sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest); + refreshSourceInfo(clippingTimeline, manifest); int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); 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 6472fe3c2f..4f5a3c7a4e 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 @@ -28,9 +28,9 @@ import java.util.HashMap; * * @param The type of the id used to identify prepared child sources. */ -public abstract class CompositeMediaSource implements MediaSource { +public abstract class CompositeMediaSource extends BaseMediaSource { - private final HashMap childSources; + private final HashMap childSources; private ExoPlayer player; /** Create composite media source without child sources. */ @@ -40,23 +40,23 @@ public abstract class CompositeMediaSource implements MediaSource { @Override @CallSuper - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { this.player = player; } @Override @CallSuper public void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSource childSource : childSources.values()) { - childSource.maybeThrowSourceInfoRefreshError(); + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.maybeThrowSourceInfoRefreshError(); } } @Override @CallSuper - public void releaseSource() { - for (MediaSource childSource : childSources.values()) { - childSource.releaseSource(); + public void releaseSourceInternal() { + for (MediaSourceAndListener childSource : childSources.values()) { + childSource.mediaSource.releaseSource(childSource.listener); } childSources.clear(); player = null; @@ -81,24 +81,23 @@ public abstract class CompositeMediaSource implements MediaSource { * this method. * *

Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)} - * will be released in {@link #releaseSource()}. + * will be released in {@link #releaseSourceInternal()}. * * @param id A unique id to identify the child source preparation. Null is allowed as an id. * @param mediaSource The child {@link MediaSource}. */ - protected void prepareChildSource(@Nullable final T id, final MediaSource mediaSource) { + protected final void prepareChildSource(@Nullable final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - childSources.put(id, mediaSource); - mediaSource.prepareSource( - player, - /* isTopLevelSource= */ false, + Listener sourceListener = new Listener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { - onChildSourceInfoRefreshed(id, mediaSource, timeline, manifest); + onChildSourceInfoRefreshed(id, source, timeline, manifest); } - }); + }; + childSources.put(id, new MediaSourceAndListener(mediaSource, sourceListener)); + mediaSource.prepareSource(player, /* isTopLevelSource= */ false, sourceListener); } /** @@ -106,8 +105,19 @@ public abstract class CompositeMediaSource implements MediaSource { * * @param id The unique id used to prepare the child source. */ - protected void releaseChildSource(@Nullable T id) { - MediaSource removedChild = childSources.remove(id); - removedChild.releaseSource(); + protected final void releaseChildSource(@Nullable T id) { + MediaSourceAndListener removedChild = childSources.remove(id); + removedChild.mediaSource.releaseSource(removedChild.listener); + } + + private static final class MediaSourceAndListener { + + public final MediaSource mediaSource; + public final Listener listener; + + public MediaSourceAndListener(MediaSource mediaSource, Listener listener) { + this.mediaSource = mediaSource; + this.listener = listener; + } } } 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 c29367e109..ee6600b098 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 @@ -40,7 +40,6 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource implements PlayerMessage.Target { @@ -63,7 +64,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< private final boolean isAtomic; private ExoPlayer player; - private Listener listener; private boolean listenerNotificationScheduled; private ShuffleOrder shuffleOrder; private int windowCount; @@ -107,9 +107,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Appends a {@link MediaSource} to the playlist. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param mediaSource The {@link MediaSource} to be added to the list. */ @@ -119,9 +116,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param mediaSource The {@link MediaSource} to be added to the list. * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media @@ -134,9 +128,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Adds a {@link MediaSource} to the playlist. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource} will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -148,9 +139,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource} will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -161,7 +149,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< public synchronized void addMediaSource(int index, MediaSource mediaSource, @Nullable Runnable actionOnCompletion) { Assertions.checkNotNull(mediaSource); - Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { player @@ -176,9 +163,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Appends multiple {@link MediaSource}s to the playlist. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @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. @@ -190,9 +174,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on * completion. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @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. @@ -206,9 +187,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Adds multiple {@link MediaSource}s to the playlist. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource}s will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -221,9 +199,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. - *

- * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same - * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource}s will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -236,7 +211,6 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< @Nullable Runnable actionOnCompletion) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); - Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { @@ -252,10 +226,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Removes a {@link MediaSource} from the playlist. - *

- * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being - * removed should not be re-added. If you want to move the instance use - * {@link #moveMediaSource(int, int)} instead. + * + *

Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. @@ -266,10 +239,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< /** * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. - *

- * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being - * removed should not be re-added. If you want to move the instance use - * {@link #moveMediaSource(int, int)} instead. + * + *

Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. @@ -347,11 +319,9 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< } @Override - public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, - Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); + public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); this.player = player; - this.listener = listener; shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); scheduleListenerNotification(/* actionOnCompletion= */ null); @@ -391,11 +361,10 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< } @Override - public void releaseSource() { - super.releaseSource(); + public void releaseSourceInternal() { + super.releaseSourceInternal(); mediaSourceHolders.clear(); player = null; - listener = null; shuffleOrder = shuffleOrder.cloneAndClear(); windowCount = 0; periodCount = 0; @@ -473,8 +442,7 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< ? Collections.emptyList() : new ArrayList<>(pendingOnCompletionActions); pendingOnCompletionActions.clear(); - listener.onSourceInfoRefreshed( - this, + refreshSourceInfo( new ConcatenatedTimeline( mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), /* manifest= */ null); 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 14453653af..20ef5ab147 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,16 +35,17 @@ import java.io.IOException; /** * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. - *

- * If the possible input stream container formats are known, pass a factory that instantiates - * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to - * use the default extractors. When reading a new stream, the first {@link Extractor} in the array - * of extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will - * be used to extract samples from the input stream. - *

- * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. + * + *

If the possible input stream container formats are known, pass a factory that instantiates + * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use + * the default extractors. When reading a new stream, the first {@link Extractor} in the array of + * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be + * used to extract samples from the input stream. + * + *

Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ -public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { +public final class ExtractorMediaSource extends BaseMediaSource + implements ExtractorMediaPeriod.Listener { /** * Listener of {@link ExtractorMediaSource} events. * @@ -100,7 +101,6 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; - private MediaSource.Listener sourceListener; private long timelineDurationUs; private boolean timelineIsSeekable; @@ -324,8 +324,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { notifySourceInfoRefreshed(C.TIME_UNSET, false); } @@ -355,8 +354,8 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe } @Override - public void releaseSource() { - sourceListener = null; + public void releaseSourceInternal() { + // Do nothing. } // ExtractorMediaPeriod.Listener implementation. @@ -378,8 +377,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. - sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); + refreshSourceInfo( + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false), + /* manifest= */ null); } /** 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 e2ef4eb5fa..774074b016 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 @@ -36,7 +36,6 @@ public final class LoopingMediaSource extends CompositeMediaSource { private final int loopCount; private int childPeriodCount; - private Listener listener; /** * Loops the provided source indefinitely. Note that it is usually better to use @@ -61,9 +60,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); prepareChildSource(/* id= */ null, childSource); } @@ -81,9 +79,8 @@ public final class LoopingMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); - listener = null; + public void releaseSourceInternal() { + super.releaseSourceInternal(); childPeriodCount = 0; } @@ -95,7 +92,7 @@ public final class LoopingMediaSource extends CompositeMediaSource { loopCount != Integer.MAX_VALUE ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline); - listener.onSourceInfoRefreshed(this, loopingTimeline, manifest); + refreshSourceInfo(loopingTimeline, manifest); } private static final class LoopingTimeline extends AbstractConcatenatedTimeline { 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 02bd0cdbc7..aec8ed47af 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 @@ -25,18 +25,20 @@ import java.io.IOException; /** * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main * responsibilities: + * *

    *
  • 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 Listener#onSourceInfoRefreshed} on the {@link Listener} - * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
  • + * provide a new timeline whenever the structure of the media changes. The MediaSource + * provides these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link + * Listener}s passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}. *
  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are - * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the - * player to load and read the media.
  • + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for + * the player to load and read the media. *
- * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances - * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. + * + * All methods are called on the player's internal playback thread, as described in the {@link + * ExoPlayer} Javadoc. They should not be called directly from application code. Instances can be + * re-used, but only for one {@link ExoPlayer} instance simultaneously. */ public interface MediaSource { @@ -170,19 +172,23 @@ public interface MediaSource { } - String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused."; - /** - * Starts preparation of the source. - *

- * Should not be called directly from application code. + * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest + * updates. + * + *

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

For each call to this method, a call to {@link #releaseSource(Listener)} is needed to remove + * the listener and to release the source if no longer required. * * @param player The player for which this source is being prepared. - * @param isTopLevelSource Whether this source has been passed directly to - * {@link ExoPlayer#prepare(MediaSource)} or - * {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. If {@code false}, this source is - * being prepared by another source (e.g. {@link ConcatenatingMediaSource}) for composition. - * @param listener The listener for source events. + * @param isTopLevelSource Whether this source has been passed directly to {@link + * ExoPlayer#prepare(MediaSource)} or {@link ExoPlayer#prepare(MediaSource, boolean, + * boolean)}. If {@code false}, this source is being prepared by another source (e.g. {@link + * ConcatenatingMediaSource}) for composition. + * @param listener The listener for source info updates to be added. */ void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener); @@ -216,10 +222,12 @@ public interface MediaSource { void releasePeriod(MediaPeriod mediaPeriod); /** - * Releases the source. - *

- * Should not be called directly from application code. + * Removes a listener for timeline and/or manifest updates and releases the source if no longer + * required. + * + *

Should not be called directly from application code. + * + * @param listener The listener for source info updates to be removed. */ - void releaseSource(); - + void releaseSource(Listener listener); } 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 a738cb1893..f9bf86081f 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 @@ -70,7 +70,6 @@ public final class MergingMediaSource extends CompositeMediaSource { private final ArrayList pendingTimelineSources; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private Listener listener; private Timeline primaryTimeline; private Object primaryManifest; private int periodCount; @@ -98,9 +97,8 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); - this.listener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); for (int i = 0; i < mediaSources.length; i++) { prepareChildSource(i, mediaSources[i]); } @@ -132,9 +130,8 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); - listener = null; + public void releaseSourceInternal() { + super.releaseSourceInternal(); primaryTimeline = null; primaryManifest = null; periodCount = PERIOD_COUNT_UNSET; @@ -158,7 +155,7 @@ public final class MergingMediaSource extends CompositeMediaSource { primaryManifest = manifest; } if (pendingTimelineSources.isEmpty()) { - listener.onSourceInfoRefreshed(this, primaryTimeline, primaryManifest); + refreshSourceInfo(primaryTimeline, primaryManifest); } } 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 b92085d15e..445f0b882e 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 @@ -31,7 +31,7 @@ import java.io.IOException; /** * Loads data at a given {@link Uri} as a single sample belonging to a single {@link MediaPeriod}. */ -public final class SingleSampleMediaSource implements MediaSource { +public final class SingleSampleMediaSource extends BaseMediaSource { /** * Listener of {@link SingleSampleMediaSource} events. @@ -250,8 +250,8 @@ public final class SingleSampleMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - listener.onSourceInfoRefreshed(this, timeline, null); + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + refreshSourceInfo(timeline, /* manifest= */ null); } @Override @@ -278,7 +278,7 @@ public final class SingleSampleMediaSource implements MediaSource { } @Override - public void releaseSource() { + public 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 64bab7ed96..5f73a57e4e 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 @@ -119,7 +119,6 @@ public final class AdsMediaSource extends CompositeMediaSource { private AdPlaybackState adPlaybackState; private MediaSource[][] adGroupMediaSources; private long[][] adDurationsUs; - private MediaSource.Listener listener; /** * Constructs a new source that inserts ads linearly with the content specified by {@code @@ -204,11 +203,10 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); + public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); Assertions.checkArgument(isTopLevelSource); final ComponentListener componentListener = new ComponentListener(); - this.listener = listener; this.componentListener = componentListener; prepareChildSource(new MediaPeriodId(/* periodIndex= */ 0), contentMediaSource); mainHandler.post(new Runnable() { @@ -276,8 +274,8 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public void releaseSource() { - super.releaseSource(); + public void releaseSourceInternal() { + super.releaseSourceInternal(); componentListener.release(); componentListener = null; deferredMediaPeriodByAdMediaSource.clear(); @@ -286,7 +284,6 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState = null; adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - listener = null; mainHandler.post(new Runnable() { @Override public void run() { @@ -350,7 +347,7 @@ public final class AdsMediaSource extends CompositeMediaSource { adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); - listener.onSourceInfoRefreshed(this, timeline, contentManifest); + refreshSourceInfo(timeline, contentManifest); } } 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 fd7577b5ec..1a4d7c07a4 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 @@ -227,9 +227,9 @@ public final class ExoPlayerTest { MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { @Override - public synchronized void prepareSource( - ExoPlayer player, boolean isTopLevelSource, Listener listener) { - super.prepareSource(player, isTopLevelSource, listener); + public synchronized void prepareSourceInternal( + ExoPlayer player, boolean isTopLevelSource) { + super.prepareSourceInternal(player, isTopLevelSource); // We've queued a source info refresh on the playback thread's event queue. Allow the // 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. 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 465e08b5d2..257966f5c3 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 @@ -270,6 +270,63 @@ public final class ConcatenatingMediaSourceTest { } } + @Test + public void testDuplicateMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + ConcatenatingMediaSource mediaSource = + new ConcatenatingMediaSource(childSource, childSource, childSource); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1); + + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5)); + + testRunner.releaseSource(); + childSource.assertReleased(); + } finally { + testRunner.release(); + } + } + + @Test + public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + ConcatenatingMediaSource nestedConcatenation = + new ConcatenatingMediaSource(childSource, childSource); + ConcatenatingMediaSource mediaSource = + new ConcatenatingMediaSource(childSource, nestedConcatenation, nestedConcatenation); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + + testRunner.releaseSource(); + childSource.assertReleased(); + } finally { + testRunner.release(); + } + } + /** * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the * concatenated timeline. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 4e4628acdf..c2da872789 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -205,7 +205,7 @@ public final class DynamicConcatenatingMediaSourceTest { timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSource.releaseSource(); + testRunner.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); } @@ -406,24 +406,6 @@ public final class DynamicConcatenatingMediaSourceTest { } catch (NullPointerException e) { // Expected. } - - // Duplicate sources. - mediaSource.addMediaSource(validSource); - try { - mediaSource.addMediaSource(validSource); - fail("Duplicate mediaSource not allowed."); - } catch (IllegalArgumentException e) { - // Expected. - } - - mediaSources = - new MediaSource[] {new FakeMediaSource(createFakeTimeline(2), null), validSource}; - try { - mediaSource.addMediaSources(Arrays.asList(mediaSources)); - fail("Duplicate mediaSource not allowed."); - } catch (IllegalArgumentException e) { - // Expected. - } } @Test @@ -782,6 +764,63 @@ public final class DynamicConcatenatingMediaSourceTest { testRunner.releaseSource(); } + @Test + public void testDuplicateMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); + + mediaSource.addMediaSource(childSource); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7)); + + testRunner.releaseSource(); + childSource.assertReleased(); + } + + @Test + public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { + FakeMediaSource childSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); + DynamicConcatenatingMediaSource nestedConcatenation = new DynamicConcatenatingMediaSource(); + + testRunner.prepareSource(); + mediaSource.addMediaSources( + Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); + + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + + testRunner.releaseSource(); + childSource.assertReleased(); + } + private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { 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 98783ac93e..d73ff5447f 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -59,7 +60,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; /** A DASH {@link MediaSource}. */ -public final class DashMediaSource implements MediaSource { +public final class DashMediaSource extends BaseMediaSource { static { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); @@ -283,7 +284,6 @@ public final class DashMediaSource implements MediaSource { private final PlayerEmsgCallback playerEmsgCallback; private final LoaderErrorThrower manifestLoadErrorThrower; - private Listener sourceListener; private DataSource dataSource; private Loader loader; @@ -497,8 +497,7 @@ public final class DashMediaSource implements MediaSource { // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { if (sideloadedManifest) { processManifest(false); } else { @@ -544,7 +543,7 @@ public final class DashMediaSource implements MediaSource { } @Override - public void releaseSource() { + public void releaseSourceInternal() { manifestLoadPending = false; dataSource = null; if (loader != null) { @@ -810,7 +809,7 @@ public final class DashMediaSource implements MediaSource { DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs, firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, manifest); - sourceListener.onSourceInfoRefreshed(this, timeline, manifest); + refreshSourceInfo(timeline, manifest); if (!sideloadedManifest) { // Remove any pending simulated refresh. 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 5113bef6e0..3259598e18 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 @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -42,11 +43,9 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; -/** - * An HLS {@link MediaSource}. - */ -public final class HlsMediaSource implements MediaSource, - HlsPlaylistTracker.PrimaryPlaylistListener { +/** An HLS {@link MediaSource}. */ +public final class HlsMediaSource extends BaseMediaSource + implements HlsPlaylistTracker.PrimaryPlaylistListener { static { ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); @@ -224,7 +223,6 @@ public final class HlsMediaSource implements MediaSource, private final boolean allowChunklessPreparation; private HlsPlaylistTracker playlistTracker; - private Listener sourceListener; /** * @param manifestUri The {@link Uri} of the HLS manifest. @@ -323,8 +321,7 @@ public final class HlsMediaSource implements MediaSource, } @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, minLoadableRetryCount, this, playlistParser); playlistTracker.start(); @@ -355,12 +352,11 @@ public final class HlsMediaSource implements MediaSource, } @Override - public void releaseSource() { + public void releaseSourceInternal() { if (playlistTracker != null) { playlistTracker.release(); playlistTracker = null; } - sourceListener = null; } @Override @@ -389,8 +385,7 @@ public final class HlsMediaSource implements MediaSource, playlist.startTimeUs + playlist.durationUs, playlist.durationUs, playlist.startTimeUs, windowDefaultStartPositionUs, true, false); } - sourceListener.onSourceInfoRefreshed(this, timeline, - new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); + refreshSourceInfo(timeline, new HlsManifest(playlistTracker.getMasterPlaylist(), playlist)); } } 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 da9024a5b5..f6973cc97e 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 @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -46,11 +47,9 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; -/** - * A SmoothStreaming {@link MediaSource}. - */ -public final class SsMediaSource implements MediaSource, - Loader.Callback> { +/** A SmoothStreaming {@link MediaSource}. */ +public final class SsMediaSource extends BaseMediaSource + implements Loader.Callback> { static { ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); @@ -256,7 +255,6 @@ public final class SsMediaSource implements MediaSource, private final ParsingLoadable.Parser manifestParser; private final ArrayList mediaPeriods; - private Listener sourceListener; private DataSource manifestDataSource; private Loader manifestLoader; private LoaderErrorThrower manifestLoaderErrorThrower; @@ -418,8 +416,7 @@ public final class SsMediaSource implements MediaSource, // MediaSource implementation. @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - sourceListener = listener; + public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); processManifest(); @@ -454,8 +451,7 @@ public final class SsMediaSource implements MediaSource, } @Override - public void releaseSource() { - sourceListener = null; + public void releaseSourceInternal() { manifest = sideloadedManifest ? manifest : null; manifestDataSource = null; manifestLoadStartTimestamp = 0; @@ -544,7 +540,7 @@ public final class SsMediaSource implements MediaSource, timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0, true /* isSeekable */, false /* isDynamic */); } - sourceListener.onSourceInfoRefreshed(this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } private void scheduleManifestRefresh() { 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 da81bbb62c..85e19409de 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 @@ -22,6 +22,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -33,10 +34,10 @@ import java.util.ArrayList; import java.util.List; /** - * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a - * {@link FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. + * Fake {@link MediaSource} that provides a given timeline. Creating the period will return a {@link + * FakeMediaPeriod} with a {@link TrackGroupArray} using the given {@link Format}s. */ -public class FakeMediaSource implements MediaSource { +public class FakeMediaSource extends BaseMediaSource { private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; @@ -46,7 +47,6 @@ public class FakeMediaSource implements MediaSource { private Object manifest; private boolean preparedSource; private boolean releasedSource; - private Listener listener; private Handler sourceInfoRefreshHandler; /** @@ -75,15 +75,13 @@ public class FakeMediaSource implements MediaSource { } @Override - public synchronized void prepareSource( - ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { assertThat(preparedSource).isFalse(); preparedSource = true; releasedSource = false; - this.listener = listener; sourceInfoRefreshHandler = new Handler(); if (timeline != null) { - listener.onSourceInfoRefreshed(this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } } @@ -113,7 +111,7 @@ public class FakeMediaSource implements MediaSource { } @Override - public void releaseSource() { + public void releaseSourceInternal() { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); assertThat(activeMediaPeriods.isEmpty()).isTrue(); @@ -121,7 +119,6 @@ public class FakeMediaSource implements MediaSource { preparedSource = false; sourceInfoRefreshHandler.removeCallbacksAndMessages(null); sourceInfoRefreshHandler = null; - listener = null; } /** @@ -138,7 +135,7 @@ public class FakeMediaSource implements MediaSource { assertThat(preparedSource).isTrue(); timeline = newTimeline; manifest = newManifest; - listener.onSourceInfoRefreshed(FakeMediaSource.this, timeline, manifest); + refreshSourceInfo(timeline, manifest); } }); } else { 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 7b27d3bd80..521c8ee52a 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 @@ -42,14 +42,6 @@ public final class FakeTimeline extends Timeline { public final long durationUs; public final AdPlaybackState adPlaybackState; - /** - * Creates a seekable, non-dynamic window definition with one period with a duration of - * {@link #DEFAULT_WINDOW_DURATION_US}. - */ - public TimelineWindowDefinition() { - this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US); - } - /** * Creates a seekable, non-dynamic window definition with a duration of * {@link #DEFAULT_WINDOW_DURATION_US}. @@ -217,7 +209,9 @@ public final class FakeTimeline extends Timeline { private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; - Arrays.fill(windowDefinitions, new TimelineWindowDefinition()); + for (int i = 0; i < windowCount; i++) { + windowDefinitions[i] = new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ i); + } return windowDefinitions; } 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 fbb48c9529..ac6901463d 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 @@ -193,13 +193,13 @@ public class MediaSourceTestRunner { }); } - /** Calls {@link MediaSource#releaseSource()} on the playback thread. */ + /** Calls {@link MediaSource#releaseSource(Listener)} on the playback thread. */ public void releaseSource() { runOnPlaybackThread( new Runnable() { @Override public void run() { - mediaSource.releaseSource(); + mediaSource.releaseSource(mediaSourceListener); } }); } diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index abef8e06be..17045f749a 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -118,6 +118,7 @@ public final class TimelineAsserts { */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); + assertThat(windowCount).isEqualTo(expectedPeriodCounts.length); int[] accumulatedPeriodCounts = new int[windowCount + 1]; accumulatedPeriodCounts[0] = 0; for (int i = 0; i < windowCount; i++) { From 38914f0ba5a6e1fd1f32c8684699a64d885d5eaf Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 28 Feb 2018 01:12:23 -0800 Subject: [PATCH 077/376] Allow clipping offset windows ClippingMediaSource provides a timeline where the period and window have the same start/end positions, so when clipping a child timeline with a non-zero offset between the window and period it is necessary to clear the offset then apply the offset to the start/end positions used in the ClippingMediaPeriod. Also add a message to clipping exceptions. Also fix adjustment of seeks to the start of the clipped view. Issue: #3888 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187292506 --- RELEASENOTES.md | 3 + .../source/ClippingMediaPeriod.java | 2 +- .../source/ClippingMediaSource.java | 89 ++++++++++--------- 3 files changed, 50 insertions(+), 44 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b1ae541411..e93badfe70 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -7,6 +7,9 @@ * MediaSources: Allow reusing media sources after they have been released and also in parallel to allow adding them multiple times to a concatenation. ([#3498](https://github.com/google/ExoPlayer/issues/3498)). +* Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource` + ([#3888](https://github.com/google/ExoPlayer/issues/3888)). ### 2.7.0 ### 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 f14c0faad4..e71c278e37 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 @@ -175,7 +175,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - if (positionUs == startUs) { + if (positionUs == 0) { // Never adjust seeks to the start of the clipped view. return 0; } 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 e25795443e..d6504c99b7 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 @@ -29,53 +29,47 @@ import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source must consist of a single period that starts at the beginning of the - * corresponding window. + * positions. The wrapped source must consist of a single period. */ public final class ClippingMediaSource extends CompositeMediaSource { - /** - * Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. - */ + /** Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. */ public static final class IllegalClippingException extends IOException { - /** - * The reason the clipping failed. - */ + /** The reason clipping failed. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW, - REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) + @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) public @interface Reason {} - /** - * The wrapped source doesn't consist of a single period. - */ + /** The wrapped source doesn't consist of a single period. */ public static final int REASON_INVALID_PERIOD_COUNT = 0; - /** - * The wrapped source period doesn't start at the beginning of the corresponding window. - */ - public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1; - /** - * The wrapped source is not seekable and a non-zero clipping start position was specified. - */ - public static final int REASON_NOT_SEEKABLE_TO_START = 2; - /** - * The wrapped source ends before the specified clipping start position. - */ - public static final int REASON_START_EXCEEDS_END = 3; + /** The wrapped source is not seekable and a non-zero clipping start position was specified. */ + public static final int REASON_NOT_SEEKABLE_TO_START = 1; + /** The wrapped source ends before the specified clipping start position. */ + public static final int REASON_START_EXCEEDS_END = 2; - /** - * The reason clipping failed. - */ - @Reason - public final int reason; + /** The reason clipping failed. */ + public final @Reason int reason; /** * @param reason The reason clipping failed. */ public IllegalClippingException(@Reason int reason) { + super("Illegal clipping: " + getReasonDescription(reason)); this.reason = reason; } + private static String getReasonDescription(@Reason int reason) { + switch (reason) { + case REASON_INVALID_PERIOD_COUNT: + return "invalid period count"; + case REASON_NOT_SEEKABLE_TO_START: + return "not seekable to start"; + case REASON_START_EXCEEDS_END: + return "start exceeds end"; + default: + return "unknown"; + } + } } private final MediaSource mediaSource; @@ -85,14 +79,16 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final ArrayList mediaPeriods; private IllegalClippingException clippingError; + private long periodStartUs; + private long periodEndUs; /** * Creates a new clipping source that wraps the specified source. * * @param mediaSource The single-period source to wrap. - * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to - * start providing samples, in microseconds. - * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop + * @param startPositionUs The start position within {@code mediaSource}'s window at which to start + * providing samples, in microseconds. + * @param endPositionUs The end position within {@code mediaSource}'s window at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples * from the specified start point up to the end of the source. Specifying a position that * exceeds the {@code mediaSource}'s duration will also result in the end of the source not @@ -148,7 +144,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); - mediaPeriod.setClipping(startUs, endUs); + mediaPeriod.setClipping(periodStartUs, periodEndUs); return mediaPeriod; } @@ -178,9 +174,16 @@ public final class ClippingMediaSource extends CompositeMediaSource { return; } refreshSourceInfo(clippingTimeline, manifest); + long windowPositionInPeriodUs = + timeline + .getWindow(/* windowIndex= */ 0, new Timeline.Window()) + .getPositionInFirstPeriodUs(); + periodStartUs = windowPositionInPeriodUs + startUs; + periodEndUs = + endUs == C.TIME_END_OF_SOURCE ? C.TIME_END_OF_SOURCE : windowPositionInPeriodUs + endUs; int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { - mediaPeriods.get(i).setClipping(startUs, endUs); + mediaPeriods.get(i).setClipping(periodStartUs, periodEndUs); } } @@ -191,6 +194,7 @@ public final class ClippingMediaSource extends CompositeMediaSource { private final long startUs; private final long endUs; + private final long durationUs; /** * Creates a new clipping timeline that wraps the specified timeline. @@ -207,9 +211,6 @@ public final class ClippingMediaSource extends CompositeMediaSource { if (timeline.getPeriodCount() != 1) { throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); } - if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) { - throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW); - } Window window = timeline.getWindow(0, new Window(), false); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { @@ -225,13 +226,15 @@ public final class ClippingMediaSource extends CompositeMediaSource { } this.startUs = startUs; this.endUs = resolvedEndUs; + durationUs = endUs == C.TIME_UNSET ? C.TIME_UNSET : endUs - startUs; } @Override public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { - window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); - window.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; + timeline.getWindow(/* windowIndex= */ 0, window, setIds, defaultPositionProjectionUs); + window.positionInFirstPeriodUs = 0; + window.durationUs = durationUs; if (window.defaultPositionUs != C.TIME_UNSET) { window.defaultPositionUs = Math.max(window.defaultPositionUs, startUs); window.defaultPositionUs = endUs == C.TIME_UNSET ? window.defaultPositionUs @@ -250,9 +253,9 @@ public final class ClippingMediaSource extends CompositeMediaSource { @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - period = timeline.getPeriod(0, period, setIds); - period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; - return period; + timeline.getPeriod(/* periodIndex= */ 0, period, setIds); + return period.set( + period.id, period.uid, /* windowIndex= */ 0, durationUs, /* positionInWindowUs= */ 0); } } From 32b4db361f09436f13075b6d4dd2d3150151a28e Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Feb 2018 01:46:40 -0800 Subject: [PATCH 078/376] Send empty timeline for empty DynamicConcatenatingMediaSource immediately. Without child sources, the timeline can't possibly updated again within the current looper message. Thus, we can send the empty timeline immediately. This fixes a recently introduced flakiness in the test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187295700 --- .../source/DynamicConcatenatingMediaSource.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 index 8aef778df3..317166a75e 100644 --- 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 @@ -322,9 +322,13 @@ public final class DynamicConcatenatingMediaSource extends CompositeMediaSource< public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { super.prepareSourceInternal(player, isTopLevelSource); this.player = player; - shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); - addMediaSourcesInternal(0, mediaSourcesPublic); - scheduleListenerNotification(/* actionOnCompletion= */ null); + if (mediaSourcesPublic.isEmpty()) { + notifyListener(); + } else { + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); + addMediaSourcesInternal(0, mediaSourcesPublic); + scheduleListenerNotification(/* actionOnCompletion= */ null); + } } @Override From 2f4a3d2e5da2a45723089e1e53e5edb07b58b812 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Feb 2018 02:30:20 -0800 Subject: [PATCH 079/376] Replace ConcatenatingMediaSource with DynamicConcatenatingMediaSource. The non-dynamic media source has a strict subset of features of the dynamic one and thus can be replaced. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187299432 --- RELEASENOTES.md | 15 +- .../exoplayer2/castdemo/PlayerManager.java | 16 +- .../ext/mediasession/TimelineQueueEditor.java | 38 +- .../google/android/exoplayer2/ExoPlayer.java | 4 +- .../source/ConcatenatingMediaSource.java | 849 +++++++++++++--- .../DynamicConcatenatingMediaSource.java | 811 +-------------- .../source/ConcatenatingMediaSourceTest.java | 957 ++++++++++++++---- .../DynamicConcatenatingMediaSourceTest.java | 871 ---------------- 8 files changed, 1512 insertions(+), 2049 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e93badfe70..4498093717 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,12 +4,15 @@ * Downloading: Add `DownloadService`, `DownloadManager` and related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). -* MediaSources: Allow reusing media sources after they have been released and - also in parallel to allow adding them multiple times to a concatenation. - ([#3498](https://github.com/google/ExoPlayer/issues/3498)). -* Allow clipping of child media sources where the period and window have a - non-zero offset with `ClippingMediaSource` - ([#3888](https://github.com/google/ExoPlayer/issues/3888)). +* MediaSources: + * Allow reusing media sources after they have been released and + also in parallel to allow adding them multiple times to a concatenation. + ([#3498](https://github.com/google/ExoPlayer/issues/3498)). + * Merged `DynamicConcatenatingMediaSource` into `ConcatenatingMediaSource` and + deprecated `DynamicConcatenatingMediaSource`. + * Allow clipping of child media sources where the period and window have a + non-zero offset with `ClippingMediaSource` + ([#3888](https://github.com/google/ExoPlayer/issues/3888)). ### 2.7.0 ### 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 ac488ff3fd..70c4831bc5 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 @@ -31,7 +31,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -80,7 +80,7 @@ import java.util.ArrayList; private final ArrayList mediaQueue; private final QueuePositionListener queuePositionListener; - private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; + private ConcatenatingMediaSource concatenatingMediaSource; private boolean castMediaQueueCreationPending; private int currentItemIndex; private Player currentPlayer; @@ -155,7 +155,7 @@ import java.util.ArrayList; public void addItem(Sample sample) { mediaQueue.add(sample); if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + concatenatingMediaSource.addMediaSource(buildMediaSource(sample)); } else { castPlayer.addItems(buildMediaQueueItem(sample)); } @@ -186,7 +186,7 @@ import java.util.ArrayList; */ public boolean removeItem(int itemIndex) { if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); + concatenatingMediaSource.removeMediaSource(itemIndex); } else { if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); @@ -215,7 +215,7 @@ import java.util.ArrayList; public boolean moveItem(int fromIndex, int toIndex) { // Player update. if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + concatenatingMediaSource.moveMediaSource(fromIndex, toIndex); } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { Timeline castTimeline = castPlayer.getCurrentTimeline(); int periodCount = castTimeline.getPeriodCount(); @@ -349,11 +349,11 @@ import java.util.ArrayList; // Media queue management. castMediaQueueCreationPending = currentPlayer == castPlayer; if (currentPlayer == exoPlayer) { - dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); + concatenatingMediaSource = new ConcatenatingMediaSource(); for (int i = 0; i < mediaQueue.size(); i++) { - dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); + concatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); } - exoPlayer.prepare(dynamicConcatenatingMediaSource); + exoPlayer.prepare(concatenatingMediaSource); } // Playback transition. 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 65090a3c1c..048bd70640 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 @@ -25,21 +25,21 @@ import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Util; import java.util.List; /** - * A {@link MediaSessionConnector.QueueEditor} implementation based on the - * {@link DynamicConcatenatingMediaSource}. - *

- * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles + * A {@link MediaSessionConnector.QueueEditor} implementation based on the {@link + * ConcatenatingMediaSource}. + * + *

This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. * This allows to move the currently playing window without interrupting playback. */ -public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor, - MediaSessionConnector.CommandReceiver { +public final class TimelineQueueEditor + implements MediaSessionConnector.QueueEditor, MediaSessionConnector.CommandReceiver { public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window"; public static final String EXTRA_FROM_INDEX = "from_index"; @@ -125,20 +125,21 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi private final QueueDataAdapter queueDataAdapter; private final MediaSourceFactory sourceFactory; private final MediaDescriptionEqualityChecker equalityChecker; - private final DynamicConcatenatingMediaSource queueMediaSource; + private final ConcatenatingMediaSource queueMediaSource; /** * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to - * manipulate. + * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param sourceFactory The {@link MediaSourceFactory} to build media sources. */ - public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, - @NonNull DynamicConcatenatingMediaSource queueMediaSource, - @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) { + public TimelineQueueEditor( + @NonNull MediaControllerCompat mediaController, + @NonNull ConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, + @NonNull MediaSourceFactory sourceFactory) { this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, new MediaIdEqualityChecker()); } @@ -147,15 +148,16 @@ public final class TimelineQueueEditor implements MediaSessionConnector.QueueEdi * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. * * @param mediaController A {@link MediaControllerCompat} to read the current queue. - * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to - * manipulate. + * @param queueMediaSource The {@link ConcatenatingMediaSource} to manipulate. * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. * @param sourceFactory The {@link MediaSourceFactory} to build media sources. * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. */ - public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, - @NonNull DynamicConcatenatingMediaSource queueMediaSource, - @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory, + public TimelineQueueEditor( + @NonNull MediaControllerCompat mediaController, + @NonNull ConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, + @NonNull MediaSourceFactory sourceFactory, @NonNull MediaDescriptionEqualityChecker equalityChecker) { this.mediaController = mediaController; this.queueMediaSource = queueMediaSource; 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 c13fd6cacd..39a6243933 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 @@ -21,7 +21,6 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -54,8 +53,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's * most often used for side-loaded subtitle files, and implementations for building more * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link - * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link - * LoopingMediaSource} and {@link ClippingMediaSource}). + * ConcatenatingMediaSource}, {@link LoopingMediaSource} and {@link ClippingMediaSource}). *

  • {@link Renderer}s that render individual components of the media. The library * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A 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 ee6600b098..b37b6249b5 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 @@ -15,199 +15,700 @@ */ package com.google.android.exoplayer2.source; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.SparseIntArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource.MediaSourceHolder; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; /** - * Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance - * to be present more than once in the concatenation. + * 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 concatenation. Access to this class is thread-safe. */ -public final class ConcatenatingMediaSource extends CompositeMediaSource { +public class ConcatenatingMediaSource extends CompositeMediaSource + implements PlayerMessage.Target { - private final MediaSource[] mediaSources; - private final Timeline[] timelines; - private final Object[] manifests; - private final Map sourceIndexByMediaPeriod; + private static final int MSG_ADD = 0; + private static final int MSG_ADD_MULTIPLE = 1; + private static final int MSG_REMOVE = 2; + private static final int MSG_MOVE = 3; + private static final int MSG_NOTIFY_LISTENER = 4; + private static final int MSG_ON_COMPLETION = 5; + + // Accessed on the app thread. + private final List mediaSourcesPublic; + + // Accessed on the playback thread. + private final List mediaSourceHolders; + private final MediaSourceHolder query; + private final Map mediaSourceByMediaPeriod; + private final List deferredMediaPeriods; + private final List pendingOnCompletionActions; private final boolean isAtomic; - private final ShuffleOrder shuffleOrder; - private ConcatenatedTimeline timeline; + private ExoPlayer player; + private boolean listenerNotificationScheduled; + private ShuffleOrder shuffleOrder; + private int windowCount; + private int periodCount; + + /** Creates a new concatenating media source. */ + public ConcatenatingMediaSource() { + this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new concatenating media source. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + */ + public ConcatenatingMediaSource(boolean isAtomic) { + this(isAtomic, new DefaultShuffleOrder(0)); + } + + /** + * Creates a new concatenating media source with a custom shuffle order. + * + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + */ + public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { + this(isAtomic, shuffleOrder, new MediaSource[0]); + } /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(MediaSource... mediaSources) { - this(false, mediaSources); + this(/* isAtomic= */ false, mediaSources); } /** - * @param isAtomic Whether the concatenated media source shall be treated as atomic, - * i.e., treated as a single item for repeating and shuffling. - * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same - * {@link MediaSource} instance to be present more than once in the array. + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { - this(isAtomic, new DefaultShuffleOrder(mediaSources.length), mediaSources); + this(isAtomic, new DefaultShuffleOrder(0), mediaSources); } /** - * @param isAtomic Whether the concatenated media source shall be treated as atomic, - * i.e., treated as a single item for repeating and shuffling. - * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. The - * number of elements in the shuffle order must match the number of concatenated - * {@link MediaSource}s. - * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same - * {@link MediaSource} instance to be present more than once in the array. + * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated + * as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same {@link + * MediaSource} instance to be present more than once in the array. */ - public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder, - MediaSource... mediaSources) { + public ConcatenatingMediaSource( + boolean isAtomic, ShuffleOrder shuffleOrder, MediaSource... mediaSources) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); } - Assertions.checkArgument(shuffleOrder.getLength() == mediaSources.length); - this.mediaSources = mediaSources; + this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; + this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); + this.mediaSourcesPublic = new ArrayList<>(Arrays.asList(mediaSources)); + this.mediaSourceHolders = new ArrayList<>(); + this.deferredMediaPeriods = new ArrayList<>(1); + this.pendingOnCompletionActions = new ArrayList<>(); + this.query = new MediaSourceHolder(null, null, -1, -1, -1); this.isAtomic = isAtomic; - this.shuffleOrder = shuffleOrder; - timelines = new Timeline[mediaSources.length]; - manifests = new Object[mediaSources.length]; - sourceIndexByMediaPeriod = new HashMap<>(); + } + + /** + * Appends a {@link MediaSource} to the playlist. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + */ + public final synchronized void addMediaSource(MediaSource mediaSource) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, null); + } + + /** + * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public final synchronized void addMediaSource( + MediaSource mediaSource, @Nullable Runnable actionOnCompletion) { + addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); + } + + /** + * Adds a {@link MediaSource} to the playlist. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * 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) { + addMediaSource(index, mediaSource, null); + } + + /** + * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource} will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @param mediaSource The {@link MediaSource} to be added to the list. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been added to the playlist. + */ + public final synchronized void addMediaSource( + int index, MediaSource mediaSource, @Nullable Runnable actionOnCompletion) { + Assertions.checkNotNull(mediaSource); + mediaSourcesPublic.add(index, mediaSource); + if (player != null) { + player + .createMessage(this) + .setType(MSG_ADD) + .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Appends multiple {@link MediaSource}s to the playlist. + * + * @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) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, null); + } + + /** + * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on + * completion. + * + * @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. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public final synchronized void addMediaSources( + Collection mediaSources, @Nullable Runnable actionOnCompletion) { + addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @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) { + addMediaSources(index, mediaSources, null); + } + + /** + * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. + * + * @param index The index at which the new {@link MediaSource}s will be inserted. This index must + * be in the range of 0 <= index <= {@link #getSize()}. + * @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. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * sources have been added to the playlist. + */ + public final synchronized void addMediaSources( + int index, Collection mediaSources, @Nullable Runnable actionOnCompletion) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } + mediaSourcesPublic.addAll(index, mediaSources); + if (player != null && !mediaSources.isEmpty()) { + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Removes a {@link MediaSource} from the playlist. + * + *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. + * + * @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) { + removeMediaSource(index, null); + } + + /** + * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. + * + *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, + * int)} instead. + * + * @param index The index at which the media source will be removed. This index must be in the + * range of 0 <= index < {@link #getSize()}. + * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been removed from the playlist. + */ + public final synchronized void removeMediaSource( + int index, @Nullable Runnable actionOnCompletion) { + mediaSourcesPublic.remove(index); + if (player != null) { + player + .createMessage(this) + .setType(MSG_REMOVE) + .setPayload(new MessageData<>(index, null, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** + * Moves an existing {@link MediaSource} 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()}. + */ + public final synchronized void moveMediaSource(int currentIndex, int newIndex) { + moveMediaSource(currentIndex, newIndex, null); + } + + /** + * Moves an existing {@link MediaSource} within the playlist and executes a custom action on + * completion. + * + * @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 actionOnCompletion A {@link Runnable} which is executed immediately after the media + * source has been moved. + */ + public final synchronized void moveMediaSource( + int currentIndex, int newIndex, @Nullable Runnable actionOnCompletion) { + if (currentIndex == newIndex) { + return; + } + mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); + if (player != null) { + player + .createMessage(this) + .setType(MSG_MOVE) + .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); + } else if (actionOnCompletion != null) { + actionOnCompletion.run(); + } + } + + /** Returns the number of media sources in the playlist. */ + public final synchronized int getSize() { + return mediaSourcesPublic.size(); + } + + /** + * Returns the {@link MediaSource} at a specified index. + * + * @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) { + return mediaSourcesPublic.get(index); } @Override - public void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { + public final synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { super.prepareSourceInternal(player, isTopLevelSource); - boolean[] duplicateFlags = buildDuplicateFlags(mediaSources); - if (mediaSources.length == 0) { - refreshSourceInfo(Timeline.EMPTY, /* manifest= */ null); + this.player = player; + if (mediaSourcesPublic.isEmpty()) { + notifyListener(); } else { - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - prepareChildSource(i, mediaSources[i]); - } - } + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); + addMediaSourcesInternal(0, mediaSourcesPublic); + scheduleListenerNotification(/* actionOnCompletion= */ null); } } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); - MediaPeriodId periodIdInSource = id.copyWithPeriodIndex( - id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); - MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); - sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); + public final MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); + MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); + MediaPeriodId idInSource = + id.copyWithPeriodIndex(id.periodIndex - holder.firstPeriodIndexInChild); + MediaPeriod mediaPeriod; + if (!holder.isPrepared) { + mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); + deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod); + } else { + mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); + } + mediaSourceByMediaPeriod.put(mediaPeriod, holder); + holder.activeMediaPeriods++; return mediaPeriod; } @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - int sourceIndex = sourceIndexByMediaPeriod.get(mediaPeriod); - sourceIndexByMediaPeriod.remove(mediaPeriod); - mediaSources[sourceIndex].releasePeriod(mediaPeriod); + public final void releasePeriod(MediaPeriod mediaPeriod) { + MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod); + if (mediaPeriod instanceof DeferredMediaPeriod) { + deferredMediaPeriods.remove(mediaPeriod); + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); + } else { + holder.mediaSource.releasePeriod(mediaPeriod); + } + holder.activeMediaPeriods--; + if (holder.activeMediaPeriods == 0 && holder.isRemoved) { + releaseChildSource(holder); + } } @Override - public void releaseSourceInternal() { + public final void releaseSourceInternal() { super.releaseSourceInternal(); - timeline = null; + mediaSourceHolders.clear(); + player = null; + shuffleOrder = shuffleOrder.cloneAndClear(); + windowCount = 0; + periodCount = 0; } @Override - protected void onChildSourceInfoRefreshed( - Integer index, + protected final void onChildSourceInfoRefreshed( + MediaSourceHolder mediaSourceHolder, MediaSource mediaSource, - Timeline sourceTimeline, - @Nullable Object sourceManifest) { - // Set the timeline and manifest. - timelines[index] = sourceTimeline; - manifests[index] = sourceManifest; - // Also set the timeline and manifest for any duplicate entries of the same source. - for (int i = index + 1; i < mediaSources.length; i++) { - if (mediaSources[i] == mediaSource) { - timelines[i] = sourceTimeline; - manifests[i] = sourceManifest; - } - } - for (Timeline timeline : timelines) { - if (timeline == null) { - // Don't invoke the listener until all sources have timelines. - return; - } - } - timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder); - refreshSourceInfo(timeline, manifests.clone()); + Timeline timeline, + @Nullable Object manifest) { + updateMediaSourceInternal(mediaSourceHolder, timeline); } - private static boolean[] buildDuplicateFlags(MediaSource[] mediaSources) { - boolean[] duplicateFlags = new boolean[mediaSources.length]; - IdentityHashMap sources = new IdentityHashMap<>(mediaSources.length); - for (int i = 0; i < mediaSources.length; i++) { - MediaSource source = mediaSources[i]; - if (!sources.containsKey(source)) { - sources.put(source, null); - } else { - duplicateFlags[i] = true; - } + @Override + @SuppressWarnings("unchecked") + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case MSG_ADD: + MessageData addMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); + addMediaSourceInternal(addMessage.index, addMessage.customData); + scheduleListenerNotification(addMessage.actionOnCompletion); + break; + case MSG_ADD_MULTIPLE: + MessageData> addMultipleMessage = + (MessageData>) message; + shuffleOrder = + shuffleOrder.cloneAndInsert( + addMultipleMessage.index, addMultipleMessage.customData.size()); + addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); + scheduleListenerNotification(addMultipleMessage.actionOnCompletion); + break; + case MSG_REMOVE: + MessageData removeMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); + removeMediaSourceInternal(removeMessage.index); + scheduleListenerNotification(removeMessage.actionOnCompletion); + break; + case MSG_MOVE: + MessageData moveMessage = (MessageData) message; + shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); + shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); + moveMediaSourceInternal(moveMessage.index, moveMessage.customData); + scheduleListenerNotification(moveMessage.actionOnCompletion); + break; + case MSG_NOTIFY_LISTENER: + notifyListener(); + break; + case MSG_ON_COMPLETION: + List actionsOnCompletion = ((List) message); + for (int i = 0; i < actionsOnCompletion.size(); i++) { + actionsOnCompletion.get(i).dispatchEvent(); + } + break; + default: + throw new IllegalStateException(); } - return duplicateFlags; } - /** - * A {@link Timeline} that is the concatenation of one or more {@link Timeline}s. - */ + private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) { + if (!listenerNotificationScheduled) { + player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); + listenerNotificationScheduled = true; + } + if (actionOnCompletion != null) { + pendingOnCompletionActions.add(actionOnCompletion); + } + } + + private void notifyListener() { + listenerNotificationScheduled = false; + List actionsOnCompletion = + pendingOnCompletionActions.isEmpty() + ? Collections.emptyList() + : new ArrayList<>(pendingOnCompletionActions); + pendingOnCompletionActions.clear(); + refreshSourceInfo( + new ConcatenatedTimeline( + mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), + /* manifest= */ null); + if (!actionsOnCompletion.isEmpty()) { + player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send(); + } + } + + private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) { + final MediaSourceHolder newMediaSourceHolder; + DeferredTimeline newTimeline = new DeferredTimeline(); + if (newIndex > 0) { + MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); + newMediaSourceHolder = + new MediaSourceHolder( + newMediaSource, + newTimeline, + newIndex, + previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), + previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); + } else { + newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0); + } + correctOffsets( + newIndex, + /* childIndexUpdate= */ 1, + newTimeline.getWindowCount(), + newTimeline.getPeriodCount()); + mediaSourceHolders.add(newIndex, newMediaSourceHolder); + prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); + } + + private void addMediaSourcesInternal(int index, Collection mediaSources) { + for (MediaSource mediaSource : mediaSources) { + addMediaSourceInternal(index++, mediaSource); + } + } + + private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { + if (mediaSourceHolder == null) { + throw new IllegalArgumentException(); + } + DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; + 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); + } + mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); + if (!mediaSourceHolder.isPrepared) { + for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) { + if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) { + deferredMediaPeriods.get(i).createPeriod(); + deferredMediaPeriods.remove(i); + } + } + } + mediaSourceHolder.isPrepared = true; + scheduleListenerNotification(/* actionOnCompletion= */ null); + } + + private void removeMediaSourceInternal(int index) { + MediaSourceHolder holder = mediaSourceHolders.get(index); + mediaSourceHolders.remove(index); + Timeline oldTimeline = holder.timeline; + correctOffsets( + index, + /* childIndexUpdate= */ -1, + -oldTimeline.getWindowCount(), + -oldTimeline.getPeriodCount()); + holder.isRemoved = true; + if (holder.activeMediaPeriods == 0) { + releaseChildSource(holder); + } + } + + 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.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; + for (int i = startIndex; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).childIndex += childIndexUpdate; + mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; + mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; + } + } + + private int findMediaSourceHolderByPeriodIndex(int periodIndex) { + query.firstPeriodIndexInChild = periodIndex; + int index = Collections.binarySearch(mediaSourceHolders, query); + if (index < 0) { + return -index - 2; + } + while (index < mediaSourceHolders.size() - 1 + && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { + index++; + } + return index; + } + + /** Data class to hold playlist media sources together with meta data needed to process them. */ + /* package */ static final class MediaSourceHolder implements Comparable { + + public final MediaSource mediaSource; + public final int uid; + + public DeferredTimeline timeline; + public int childIndex; + public int firstWindowIndexInChild; + public int firstPeriodIndexInChild; + public boolean isPrepared; + public boolean isRemoved; + public int activeMediaPeriods; + + public MediaSourceHolder( + MediaSource mediaSource, + DeferredTimeline timeline, + int childIndex, + int window, + int period) { + this.mediaSource = mediaSource; + this.timeline = timeline; + this.childIndex = childIndex; + this.firstWindowIndexInChild = window; + this.firstPeriodIndexInChild = period; + this.uid = System.identityHashCode(this); + } + + @Override + public int compareTo(@NonNull MediaSourceHolder other) { + return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; + } + } + + /** Can be used to dispatch a runnable on the thread the object was created on. */ + private static final class EventDispatcher { + + public final Handler eventHandler; + public final Runnable runnable; + + public EventDispatcher(Runnable runnable) { + this.runnable = runnable; + this.eventHandler = + new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); + } + + public void dispatchEvent() { + eventHandler.post(runnable); + } + } + + /** Message used to post actions from app thread to playback thread. */ + private static final class MessageData { + + public final int index; + public final T customData; + public final @Nullable EventDispatcher actionOnCompletion; + + public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) { + this.index = index; + this.actionOnCompletion = + actionOnCompletion != null ? new EventDispatcher(actionOnCompletion) : null; + this.customData = customData; + } + } + + /** Timeline exposing concatenated timelines of playlist media sources. */ private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { + private final int windowCount; + private final int periodCount; + private final int[] firstPeriodInChildIndices; + private final int[] firstWindowInChildIndices; private final Timeline[] timelines; - private final int[] sourcePeriodOffsets; - private final int[] sourceWindowOffsets; + private final int[] uids; + private final SparseIntArray childIndexByUid; - public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) { + public ConcatenatedTimeline( + Collection mediaSourceHolders, + int windowCount, + int periodCount, + ShuffleOrder shuffleOrder, + boolean isAtomic) { super(isAtomic, shuffleOrder); - int[] sourcePeriodOffsets = new int[timelines.length]; - int[] sourceWindowOffsets = new int[timelines.length]; - long periodCount = 0; - int windowCount = 0; - for (int i = 0; i < timelines.length; i++) { - Timeline timeline = timelines[i]; - periodCount += timeline.getPeriodCount(); - Assertions.checkState(periodCount <= Integer.MAX_VALUE, - "ConcatenatingMediaSource children contain too many periods"); - sourcePeriodOffsets[i] = (int) periodCount; - windowCount += timeline.getWindowCount(); - sourceWindowOffsets[i] = windowCount; + this.windowCount = windowCount; + this.periodCount = periodCount; + int childCount = mediaSourceHolders.size(); + firstPeriodInChildIndices = new int[childCount]; + firstWindowInChildIndices = new int[childCount]; + timelines = new Timeline[childCount]; + uids = new int[childCount]; + childIndexByUid = new SparseIntArray(); + int index = 0; + for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { + timelines[index] = mediaSourceHolder.timeline; + firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; + firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; + uids[index] = mediaSourceHolder.uid; + childIndexByUid.put(uids[index], index++); } - this.timelines = timelines; - this.sourcePeriodOffsets = sourcePeriodOffsets; - this.sourceWindowOffsets = sourceWindowOffsets; - } - - @Override - public int getWindowCount() { - return sourceWindowOffsets[sourceWindowOffsets.length - 1]; - } - - @Override - public int getPeriodCount() { - return sourcePeriodOffsets[sourcePeriodOffsets.length - 1]; } @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1; + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1; + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); } @Override @@ -215,7 +716,8 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource 0 + ? timeline.getPeriod(0, period, true).uid + : replacedId); + } + + 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); + } + } + + /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ + private static final class DummyTimeline extends Timeline { + + @Override + public int getWindowCount() { + return 1; + } + + @Override + public Window getWindow( + int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { + // Dynamic window to indicate pending timeline updates. + return window.set( + /* id= */ null, + /* presentationStartTimeMs= */ C.TIME_UNSET, + /* windowStartTimeMs= */ C.TIME_UNSET, + /* isSeekable= */ false, + /* 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= */ null, + /* uid= */ null, + /* windowIndex= */ 0, + /* durationUs = */ C.TIME_UNSET, + /* positionInWindowUs= */ C.TIME_UNSET); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid == null ? 0 : C.INDEX_UNSET; + } + } } 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 index 317166a75e..37313fd1ab 100644 --- 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 @@ -15,811 +15,26 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.SparseIntArray; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource.MediaSourceHolder; -import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.upstream.Allocator; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; +/** @deprecated Use {@link ConcatenatingMediaSource} instead. */ +@Deprecated +public final class DynamicConcatenatingMediaSource extends ConcatenatingMediaSource { -/** - * 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 concatenation. Access to this class is thread-safe. - */ -public final class DynamicConcatenatingMediaSource extends CompositeMediaSource - implements PlayerMessage.Target { + /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource()} instead. */ + @Deprecated + public DynamicConcatenatingMediaSource() {} - private static final int MSG_ADD = 0; - private static final int MSG_ADD_MULTIPLE = 1; - private static final int MSG_REMOVE = 2; - private static final int MSG_MOVE = 3; - private static final int MSG_NOTIFY_LISTENER = 4; - private static final int MSG_ON_COMPLETION = 5; - - // Accessed on the app thread. - private final List mediaSourcesPublic; - - // Accessed on the playback thread. - private final List mediaSourceHolders; - private final MediaSourceHolder query; - private final Map mediaSourceByMediaPeriod; - private final List deferredMediaPeriods; - private final List pendingOnCompletionActions; - private final boolean isAtomic; - - private ExoPlayer player; - private boolean listenerNotificationScheduled; - private ShuffleOrder shuffleOrder; - private int windowCount; - private int periodCount; - - /** - * Creates a new dynamic concatenating media source. - */ - public DynamicConcatenatingMediaSource() { - this(/* isAtomic= */ false, new DefaultShuffleOrder(0)); - } - - /** - * Creates a new dynamic concatenating media source. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - */ + /** @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean)} instead. */ + @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic) { - this(isAtomic, new DefaultShuffleOrder(0)); + super(isAtomic); } /** - * Creates a new dynamic concatenating media source with a custom shuffle order. - * - * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated - * as a single item for repeating and shuffling. - * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. - * This shuffle order must be empty. + * @deprecated Use {@link ConcatenatingMediaSource#ConcatenatingMediaSource(boolean, + * ShuffleOrder)} instead. */ + @Deprecated public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) { - this.shuffleOrder = shuffleOrder; - this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); - this.mediaSourcesPublic = new ArrayList<>(); - this.mediaSourceHolders = new ArrayList<>(); - this.deferredMediaPeriods = new ArrayList<>(1); - this.pendingOnCompletionActions = new ArrayList<>(); - this.query = new MediaSourceHolder(null, null, -1, -1, -1); - this.isAtomic = isAtomic; - } - - /** - * Appends a {@link MediaSource} to the playlist. - * - * @param mediaSource The {@link MediaSource} to be added to the list. - */ - public synchronized void addMediaSource(MediaSource mediaSource) { - addMediaSource(mediaSourcesPublic.size(), mediaSource, null); - } - - /** - * Appends a {@link MediaSource} to the playlist and executes a custom action on completion. - * - * @param mediaSource The {@link MediaSource} to be added to the list. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been added to the playlist. - */ - public synchronized void addMediaSource(MediaSource mediaSource, - @Nullable Runnable actionOnCompletion) { - addMediaSource(mediaSourcesPublic.size(), mediaSource, actionOnCompletion); - } - - /** - * Adds a {@link MediaSource} to the playlist. - * - * @param index The index at which the new {@link MediaSource} will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSource The {@link MediaSource} to be added to the list. - */ - public synchronized void addMediaSource(int index, MediaSource mediaSource) { - addMediaSource(index, mediaSource, null); - } - - /** - * Adds a {@link MediaSource} to the playlist and executes a custom action on completion. - * - * @param index The index at which the new {@link MediaSource} will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @param mediaSource The {@link MediaSource} to be added to the list. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been added to the playlist. - */ - public synchronized void addMediaSource(int index, MediaSource mediaSource, - @Nullable Runnable actionOnCompletion) { - Assertions.checkNotNull(mediaSource); - mediaSourcesPublic.add(index, mediaSource); - if (player != null) { - player - .createMessage(this) - .setType(MSG_ADD) - .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Appends multiple {@link MediaSource}s to the playlist. - * - * @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 synchronized void addMediaSources(Collection mediaSources) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, null); - } - - /** - * Appends multiple {@link MediaSource}s to the playlist and executes a custom action on - * completion. - * - * @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. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * sources have been added to the playlist. - */ - public synchronized void addMediaSources(Collection mediaSources, - @Nullable Runnable actionOnCompletion) { - addMediaSources(mediaSourcesPublic.size(), mediaSources, actionOnCompletion); - } - - /** - * Adds multiple {@link MediaSource}s to the playlist. - * - * @param index The index at which the new {@link MediaSource}s will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @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 synchronized void addMediaSources(int index, Collection mediaSources) { - addMediaSources(index, mediaSources, null); - } - - /** - * Adds multiple {@link MediaSource}s to the playlist and executes a custom action on completion. - * - * @param index The index at which the new {@link MediaSource}s will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. - * @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. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * sources have been added to the playlist. - */ - public synchronized void addMediaSources(int index, Collection mediaSources, - @Nullable Runnable actionOnCompletion) { - for (MediaSource mediaSource : mediaSources) { - Assertions.checkNotNull(mediaSource); - } - mediaSourcesPublic.addAll(index, mediaSources); - if (player != null && !mediaSources.isEmpty()) { - player - .createMessage(this) - .setType(MSG_ADD_MULTIPLE) - .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null){ - actionOnCompletion.run(); - } - } - - /** - * Removes a {@link MediaSource} from the playlist. - * - *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, - * int)} instead. - * - * @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 synchronized void removeMediaSource(int index) { - removeMediaSource(index, null); - } - - /** - * Removes a {@link MediaSource} from the playlist and executes a custom action on completion. - * - *

    Note: If you want to move the instance, it's preferable to use {@link #moveMediaSource(int, - * int)} instead. - * - * @param index The index at which the media source will be removed. This index must be in the - * range of 0 <= index < {@link #getSize()}. - * @param actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been removed from the playlist. - */ - public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { - mediaSourcesPublic.remove(index); - if (player != null) { - player - .createMessage(this) - .setType(MSG_REMOVE) - .setPayload(new MessageData<>(index, null, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Moves an existing {@link MediaSource} 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()}. - */ - public synchronized void moveMediaSource(int currentIndex, int newIndex) { - moveMediaSource(currentIndex, newIndex, null); - } - - /** - * Moves an existing {@link MediaSource} within the playlist and executes a custom action on - * completion. - * - * @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 actionOnCompletion A {@link Runnable} which is executed immediately after the media - * source has been moved. - */ - public synchronized void moveMediaSource(int currentIndex, int newIndex, - @Nullable Runnable actionOnCompletion) { - if (currentIndex == newIndex) { - return; - } - mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); - if (player != null) { - player - .createMessage(this) - .setType(MSG_MOVE) - .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) - .send(); - } else if (actionOnCompletion != null) { - actionOnCompletion.run(); - } - } - - /** - * Returns the number of media sources in the playlist. - */ - public synchronized int getSize() { - return mediaSourcesPublic.size(); - } - - /** - * Returns the {@link MediaSource} at a specified index. - * - * @param index An index in the range of 0 <= index <= {@link #getSize()}. - * @return The {@link MediaSource} at this index. - */ - public synchronized MediaSource getMediaSource(int index) { - return mediaSourcesPublic.get(index); - } - - @Override - public synchronized void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) { - super.prepareSourceInternal(player, isTopLevelSource); - this.player = player; - if (mediaSourcesPublic.isEmpty()) { - notifyListener(); - } else { - shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); - addMediaSourcesInternal(0, mediaSourcesPublic); - scheduleListenerNotification(/* actionOnCompletion= */ null); - } - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex); - MediaSourceHolder holder = mediaSourceHolders.get(mediaSourceHolderIndex); - MediaPeriodId idInSource = id.copyWithPeriodIndex( - id.periodIndex - holder.firstPeriodIndexInChild); - MediaPeriod mediaPeriod; - if (!holder.isPrepared) { - mediaPeriod = new DeferredMediaPeriod(holder.mediaSource, idInSource, allocator); - deferredMediaPeriods.add((DeferredMediaPeriod) mediaPeriod); - } else { - mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator); - } - mediaSourceByMediaPeriod.put(mediaPeriod, holder); - holder.activeMediaPeriods++; - return mediaPeriod; - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod); - if (mediaPeriod instanceof DeferredMediaPeriod) { - deferredMediaPeriods.remove(mediaPeriod); - ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); - } else { - holder.mediaSource.releasePeriod(mediaPeriod); - } - holder.activeMediaPeriods--; - if (holder.activeMediaPeriods == 0 && holder.isRemoved) { - releaseChildSource(holder); - } - } - - @Override - public void releaseSourceInternal() { - super.releaseSourceInternal(); - mediaSourceHolders.clear(); - player = null; - shuffleOrder = shuffleOrder.cloneAndClear(); - windowCount = 0; - periodCount = 0; - } - - @Override - protected void onChildSourceInfoRefreshed( - MediaSourceHolder mediaSourceHolder, - MediaSource mediaSource, - Timeline timeline, - @Nullable Object manifest) { - updateMediaSourceInternal(mediaSourceHolder, timeline); - } - - @Override - @SuppressWarnings("unchecked") - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_ADD: - MessageData addMessage = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, 1); - addMediaSourceInternal(addMessage.index, addMessage.customData); - scheduleListenerNotification(addMessage.actionOnCompletion); - break; - case MSG_ADD_MULTIPLE: - MessageData> addMultipleMessage = - (MessageData>) message; - shuffleOrder = - shuffleOrder.cloneAndInsert( - addMultipleMessage.index, addMultipleMessage.customData.size()); - addMediaSourcesInternal(addMultipleMessage.index, addMultipleMessage.customData); - scheduleListenerNotification(addMultipleMessage.actionOnCompletion); - break; - case MSG_REMOVE: - MessageData removeMessage = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(removeMessage.index); - removeMediaSourceInternal(removeMessage.index); - scheduleListenerNotification(removeMessage.actionOnCompletion); - break; - case MSG_MOVE: - MessageData moveMessage = (MessageData) message; - shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index); - shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); - moveMediaSourceInternal(moveMessage.index, moveMessage.customData); - scheduleListenerNotification(moveMessage.actionOnCompletion); - break; - case MSG_NOTIFY_LISTENER: - notifyListener(); - break; - case MSG_ON_COMPLETION: - List actionsOnCompletion = ((List) message); - for (int i = 0; i < actionsOnCompletion.size(); i++) { - actionsOnCompletion.get(i).dispatchEvent(); - } - break; - default: - throw new IllegalStateException(); - } - } - - private void scheduleListenerNotification(@Nullable EventDispatcher actionOnCompletion) { - if (!listenerNotificationScheduled) { - player.createMessage(this).setType(MSG_NOTIFY_LISTENER).send(); - listenerNotificationScheduled = true; - } - if (actionOnCompletion != null) { - pendingOnCompletionActions.add(actionOnCompletion); - } - } - - private void notifyListener() { - listenerNotificationScheduled = false; - List actionsOnCompletion = - pendingOnCompletionActions.isEmpty() - ? Collections.emptyList() - : new ArrayList<>(pendingOnCompletionActions); - pendingOnCompletionActions.clear(); - refreshSourceInfo( - new ConcatenatedTimeline( - mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), - /* manifest= */ null); - if (!actionsOnCompletion.isEmpty()) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionsOnCompletion).send(); - } - } - - private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) { - final MediaSourceHolder newMediaSourceHolder; - DeferredTimeline newTimeline = new DeferredTimeline(); - if (newIndex > 0) { - MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1); - newMediaSourceHolder = - new MediaSourceHolder( - newMediaSource, - newTimeline, - newIndex, - previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(), - previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount()); - } else { - newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0); - } - correctOffsets( - newIndex, - /* childIndexUpdate= */ 1, - newTimeline.getWindowCount(), - newTimeline.getPeriodCount()); - mediaSourceHolders.add(newIndex, newMediaSourceHolder); - prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource); - } - - private void addMediaSourcesInternal(int index, Collection mediaSources) { - for (MediaSource mediaSource : mediaSources) { - addMediaSourceInternal(index++, mediaSource); - } - } - - private void updateMediaSourceInternal(MediaSourceHolder mediaSourceHolder, Timeline timeline) { - if (mediaSourceHolder == null) { - throw new IllegalArgumentException(); - } - DeferredTimeline deferredTimeline = mediaSourceHolder.timeline; - 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); - } - mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline); - if (!mediaSourceHolder.isPrepared) { - for (int i = deferredMediaPeriods.size() - 1; i >= 0; i--) { - if (deferredMediaPeriods.get(i).mediaSource == mediaSourceHolder.mediaSource) { - deferredMediaPeriods.get(i).createPeriod(); - deferredMediaPeriods.remove(i); - } - } - } - mediaSourceHolder.isPrepared = true; - scheduleListenerNotification(/* actionOnCompletion= */ null); - } - - private void removeMediaSourceInternal(int index) { - MediaSourceHolder holder = mediaSourceHolders.get(index); - mediaSourceHolders.remove(index); - Timeline oldTimeline = holder.timeline; - correctOffsets( - index, - /* childIndexUpdate= */ -1, - -oldTimeline.getWindowCount(), - -oldTimeline.getPeriodCount()); - holder.isRemoved = true; - if (holder.activeMediaPeriods == 0) { - releaseChildSource(holder); - } - } - - 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.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; - for (int i = startIndex; i < mediaSourceHolders.size(); i++) { - mediaSourceHolders.get(i).childIndex += childIndexUpdate; - mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate; - mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate; - } - } - - private int findMediaSourceHolderByPeriodIndex(int periodIndex) { - query.firstPeriodIndexInChild = periodIndex; - int index = Collections.binarySearch(mediaSourceHolders, query); - if (index < 0) { - return -index - 2; - } - while (index < mediaSourceHolders.size() - 1 - && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { - index++; - } - return index; - } - - /** Data class to hold playlist media sources together with meta data needed to process them. */ - /* package */ static final class MediaSourceHolder implements Comparable { - - public final MediaSource mediaSource; - public final int uid; - - public DeferredTimeline timeline; - public int childIndex; - public int firstWindowIndexInChild; - public int firstPeriodIndexInChild; - public boolean isPrepared; - public boolean isRemoved; - public int activeMediaPeriods; - - public MediaSourceHolder( - MediaSource mediaSource, - DeferredTimeline timeline, - int childIndex, - int window, - int period) { - this.mediaSource = mediaSource; - this.timeline = timeline; - this.childIndex = childIndex; - this.firstWindowIndexInChild = window; - this.firstPeriodIndexInChild = period; - this.uid = System.identityHashCode(this); - } - - @Override - public int compareTo(@NonNull MediaSourceHolder other) { - return this.firstPeriodIndexInChild - other.firstPeriodIndexInChild; - } - } - - /** - * Can be used to dispatch a runnable on the thread the object was created on. - */ - private static final class EventDispatcher { - - public final Handler eventHandler; - public final Runnable runnable; - - public EventDispatcher(Runnable runnable) { - this.runnable = runnable; - this.eventHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() - : Looper.getMainLooper()); - } - - public void dispatchEvent() { - eventHandler.post(runnable); - } - - } - - /** Message used to post actions from app thread to playback thread. */ - private static final class MessageData { - - public final int index; - public final T customData; - public final @Nullable EventDispatcher actionOnCompletion; - - public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) { - this.index = index; - this.actionOnCompletion = actionOnCompletion != null - ? new EventDispatcher(actionOnCompletion) : null; - this.customData = customData; - } - - } - - /** - * Timeline exposing concatenated timelines of playlist media sources. - */ - private static final class ConcatenatedTimeline extends AbstractConcatenatedTimeline { - - private final int windowCount; - private final int periodCount; - private final int[] firstPeriodInChildIndices; - private final int[] firstWindowInChildIndices; - private final Timeline[] timelines; - private final int[] uids; - private final SparseIntArray childIndexByUid; - - 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]; - timelines = new Timeline[childCount]; - uids = new int[childCount]; - childIndexByUid = new SparseIntArray(); - int index = 0; - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - timelines[index] = mediaSourceHolder.timeline; - firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild; - firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild; - uids[index] = mediaSourceHolder.uid; - childIndexByUid.put(uids[index], index++); - } - } - - @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) { - if (!(childUid instanceof Integer)) { - return C.INDEX_UNSET; - } - int index = childIndexByUid.get((int) childUid, -1); - return index == -1 ? 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; - } - - } - - /** - * Timeline used as placeholder for an unprepared media source. After preparation, a copy of the - * DeferredTimeline is used to keep the originally assigned first period ID. - */ - private static final class DeferredTimeline extends ForwardingTimeline { - - private static final Object DUMMY_ID = new Object(); - private static final Period period = new Period(); - private static final DummyTimeline dummyTimeline = new DummyTimeline(); - - private final Object replacedId; - - public DeferredTimeline() { - this(dummyTimeline, /* replacedId= */ null); - } - - private DeferredTimeline(Timeline timeline, Object replacedId) { - super(timeline); - this.replacedId = replacedId; - } - - public DeferredTimeline cloneWithNewTimeline(Timeline timeline) { - return new DeferredTimeline( - timeline, - replacedId == null && timeline.getPeriodCount() > 0 - ? timeline.getPeriod(0, period, true).uid - : replacedId); - } - - 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); - } - } - - /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */ - private static final class DummyTimeline extends Timeline { - - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - // Dynamic window to indicate pending timeline updates. - return window.set( - /* id= */ null, - /* presentationStartTimeMs= */ C.TIME_UNSET, - /* windowStartTimeMs= */ C.TIME_UNSET, - /* isSeekable= */ false, - /* 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= */ null, - /* uid= */ null, - /* windowIndex= */ 0, - /* durationUs = */ C.TIME_UNSET, - /* positionInWindowUs= */ C.TIME_UNSET); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return uid == null ? 0 : C.INDEX_UNSET; - } + super(isAtomic, shuffleOrder); } } - 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 257966f5c3..038ff5505e 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,11 +16,15 @@ package com.google.android.exoplayer2.source; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; +import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -29,8 +33,13 @@ import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -39,142 +48,292 @@ import org.robolectric.annotation.Config; @Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public final class ConcatenatingMediaSourceTest { - @Test - public void testEmptyConcatenation() throws IOException { - for (boolean atomic : new boolean[] {false, true}) { - Timeline timeline = getConcatenatedTimeline(atomic); - TimelineAsserts.assertEmpty(timeline); + private ConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); + @Before + public void setUp() throws Exception { + mediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + } - timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); - TimelineAsserts.assertEmpty(timeline); - } + @After + public void tearDown() throws Exception { + testRunner.release(); } @Test - public void testSingleMediaSource() throws IOException { - Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); + public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + FakeMediaSource[] childSources = createMediaSources(7); + + // Add first source. + mediaSource.addMediaSource(childSources[0]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + + // Add at front of queue. + mediaSource.addMediaSource(0, childSources[1]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1); + TimelineAsserts.assertWindowIds(timeline, 222, 111); + + // Add at back of queue. + mediaSource.addMediaSource(childSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); + + // Add in the middle. + mediaSource.addMediaSource(1, childSources[3]); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); + + // Add bulk. + mediaSource.addMediaSources( + 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Move sources. + mediaSource.moveMediaSource(2, 3); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); + mediaSource.moveMediaSource(3, 2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + mediaSource.moveMediaSource(0, 6); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); + TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); + mediaSource.moveMediaSource(6, 0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); + + // Remove in the middle. + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(3); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(1); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); + for (int i = 3; i <= 6; i++) { + childSources[i].assertReleased(); } - timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); - TimelineAsserts.assertWindowIds(timeline, 111); - TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); - } - } - - @Test - public void testMultipleMediaSources() throws IOException { - Timeline[] timelines = { - createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + // Assert correct next and previous indices behavior after some insertions and removals. TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); TimelineAsserts.assertNextWindowIndices( timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + // Assert all periods can be prepared. + testRunner.assertPrepareAndReleaseAllPeriods(); + + // Remove at front of queue. + mediaSource.removeMediaSource(0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 3); + TimelineAsserts.assertWindowIds(timeline, 111, 333); + childSources[1].assertReleased(); + + // Remove at back of queue. + mediaSource.removeMediaSource(1); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + TimelineAsserts.assertWindowIds(timeline, 111); + childSources[2].assertReleased(); + + // Remove last source. + mediaSource.removeMediaSource(0); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + childSources[3].assertReleased(); + } + + @Test + public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { + FakeMediaSource[] childSources = createMediaSources(4); + mediaSource.addMediaSource(childSources[0]); + mediaSource.addMediaSource(childSources[1]); + mediaSource.addMediaSource(0, childSources[2]); + mediaSource.moveMediaSource(0, 2); + mediaSource.removeMediaSource(0); + mediaSource.moveMediaSource(1, 0); + mediaSource.addMediaSource(1, childSources[3]); + testRunner.assertNoTimelineChange(); + + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); + TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + + testRunner.assertPrepareAndReleaseAllPeriods(); + testRunner.releaseSource(); + for (int i = 1; i < 4; i++) { + childSources[i].assertReleased(); } } @Test - public void testNestedMediaSources() throws IOException { - Timeline timeline = - getConcatenatedTimeline( - false, - getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), - getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, false, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 3, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); + public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; + for (int i = 0; i < 4; i++) { + lazySources[i] = new FakeMediaSource(null, null); + } + + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. + mediaSource.addMediaSource(lazySources[0]); + mediaSource.addMediaSource(0, fastSources[0]); + mediaSource.removeMediaSource(1); + mediaSource.addMediaSource(1, lazySources[1]); + testRunner.assertNoTimelineChange(); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + TimelineAsserts.assertWindowIds(timeline, 111, null); + TimelineAsserts.assertWindowIsDynamic(timeline, false, true); + + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + testRunner.runOnPlaybackThread( + new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 9); + TimelineAsserts.assertWindowIds(timeline, 111, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + testRunner.assertPrepareAndReleaseAllPeriods(); + + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. + mediaSource.addMediaSource(1, lazySources[2]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(2, fastSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(0, lazySources[3]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.removeMediaSource(2); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. + MediaPeriod lazyPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + 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(/* 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( + new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); + TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); + TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertThat(preparedCondition.getCount()).isEqualTo(0); + + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } @Test - public void testEmptyTimelineMediaSources() throws IOException { - // Empty timelines in the front, back, and the middle (single and multiple in a row). - Timeline[] timelines = { - Timeline.EMPTY, - createFakeTimeline(1, 111), - Timeline.EMPTY, - Timeline.EMPTY, - createFakeTimeline(2, 222), - Timeline.EMPTY, - createFakeTimeline(3, 333), - Timeline.EMPTY - }; - Timeline timeline = getConcatenatedTimeline(false, timelines); + public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + 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) + })); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertEmpty(timeline); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(4, mediaSources[1]); + testRunner.assertTimelineChangeBlocking(); + mediaSource.addMediaSource(6, mediaSources[2]); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices( @@ -197,29 +356,263 @@ public final class ConcatenatingMediaSourceTest { assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); + testRunner.assertPrepareAndReleaseAllPeriods(); + } - timeline = getConcatenatedTimeline(true, timelines); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - for (boolean shuffled : new boolean[] {false, true}) { - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, shuffled, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, shuffled, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(shuffled)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(shuffled)).isEqualTo(2); + @Test + 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), + }; + Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); + + mediaSource.addMediaSources(Arrays.asList(childSources)); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertEmpty(timeline); + + childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1); + + childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1); + + childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); + timeline = testRunner.assertTimelineChangeBlocking(); + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); + } + + @Test + public void testIllegalArguments() { + MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); + + // Null sources. + try { + mediaSource.addMediaSource(null); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + + MediaSource[] mediaSources = {validSource, null}; + try { + mediaSource.addMediaSources(Arrays.asList(mediaSources)); + fail("Null mediaSource not allowed."); + } catch (NullPointerException e) { + // Expected. + } + } + + @Test + public void testCustomCallbackBeforePreparationAddSingle() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultiple() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddSingleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationRemove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.removeMediaSource(/* index */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackBeforePreparationMove() { + Runnable runnable = Mockito.mock(Runnable.class); + + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); + verify(runnable).run(); + } + + @Test + public void testCustomCallbackAfterPreparationAddSingle() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(1); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + /* index */ 0, + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationRemove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(0); + } finally { + dummyMainThread.release(); + } + } + + @Test + public void testCustomCallbackAfterPreparationMove() throws IOException { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); + + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread( + new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertThat(timeline.getWindowCount()).isEqualTo(2); + } finally { + dummyMainThread.release(); } } @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { - // Create media source with ad child source. + // Create concatenated media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); @@ -235,125 +628,243 @@ public final class ConcatenatingMediaSourceTest { /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); + mediaSource.addMediaSource(mediaSourceContentOnly); + mediaSource.addMediaSource(mediaSourceWithAds); + + Timeline timeline = testRunner.prepareSource(); + + // Assert the timeline contains ad groups. + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + + // Create all periods and assert period creation of child media sources has been called. + testRunner.assertPrepareAndReleaseAllPeriods(); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSourceContentOnly.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 0, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + mediaSourceWithAds.assertMediaPeriodCreated( + new MediaPeriodId( + /* periodIndex= */ 1, + /* adGroupIndex= */ 0, + /* adIndexInAdGroup= */ 0, + /* windowSequenceNumber= */ 1)); + } + + @Test + public void testAtomicTimelineWindowOrder() throws IOException { + // Release default test runner with non-atomic media source and replace with new test runner. + testRunner.release(); ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); + assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); + assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); + } - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + @Test + public void testNestedTimeline() throws IOException { + ConcatenatingMediaSource nestedSource1 = + new ConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); + ConcatenatingMediaSource nestedSource2 = + new ConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); + mediaSource.addMediaSource(nestedSource1); + mediaSource.addMediaSource(nestedSource2); + testRunner.prepareSource(); + FakeMediaSource[] childSources = createMediaSources(4); + nestedSource1.addMediaSource(childSources[0]); + testRunner.assertTimelineChangeBlocking(); + nestedSource1.addMediaSource(childSources[1]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[2]); + testRunner.assertTimelineChangeBlocking(); + nestedSource2.addMediaSource(childSources[3]); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - // Create all periods and assert period creation of child media sources has been called. - testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 0, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 1, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - } finally { - testRunner.release(); - } + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); + } + + @Test + public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { + FakeMediaSource childSource = createFakeMediaSource(); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + MediaPeriod mediaPeriod = + testRunner.createPeriod( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); + mediaSource.removeMediaSource(/* index= */ 0); + testRunner.assertTimelineChangeBlocking(); + testRunner.releasePeriod(mediaPeriod); + childSource.assertReleased(); + testRunner.releaseSource(); } @Test public void testDuplicateMediaSources() throws IOException, InterruptedException { FakeMediaSource childSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(childSource, childSource, childSource); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1); - testRunner.assertPrepareAndReleaseAllPeriods(); - assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5)); + mediaSource.addMediaSource(childSource); + mediaSource.addMediaSource(childSource); + testRunner.prepareSource(); + mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - testRunner.releaseSource(); - childSource.assertReleased(); - } finally { - testRunner.release(); - } + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5), + new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7)); + + testRunner.releaseSource(); + childSource.assertReleased(); } @Test public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { FakeMediaSource childSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); - ConcatenatingMediaSource nestedConcatenation = - new ConcatenatingMediaSource(childSource, childSource); - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource(childSource, nestedConcatenation, nestedConcatenation); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + ConcatenatingMediaSource nestedConcatenation = new ConcatenatingMediaSource(); - testRunner.assertPrepareAndReleaseAllPeriods(); - assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + testRunner.prepareSource(); + mediaSource.addMediaSources( + Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + testRunner.assertTimelineChangeBlocking(); + nestedConcatenation.addMediaSource(childSource); + Timeline timeline = testRunner.assertTimelineChangeBlocking(); - testRunner.releaseSource(); - childSource.assertReleased(); - } finally { - testRunner.release(); - } + TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); + testRunner.assertPrepareAndReleaseAllPeriods(); + assertThat(childSource.getCreatedMediaPeriods()) + .containsAllOf( + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), + new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); + + testRunner.releaseSource(); + childSource.assertReleased(); } - /** - * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns the - * concatenated timeline. - */ - private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, 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); + 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); } - ConcatenatingMediaSource mediaSource = - new ConcatenatingMediaSource( - isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); - try { - Timeline timeline = testRunner.prepareSource(); - testRunner.releaseSource(); - for (int i = 0; i < mediaSources.length; i++) { - mediaSources[i].assertReleased(); + return sources; + } + + private static FakeMediaSource createFakeMediaSource() { + return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); + } + + private static FakeTimeline createFakeTimeline(int index) { + return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); + } + + private static final class TimelineGrabber implements Runnable { + + private final MediaSourceTestRunner testRunner; + private final ConditionVariable finishedCondition; + + private Timeline timeline; + private AssertionError error; + + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); + } + + @Override + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; + } + finishedCondition.open(); + } + + public Timeline assertTimelineChangeBlocking() { + assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); + if (error != null) { + throw error; } return timeline; - } finally { - testRunner.release(); } } - - private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { - return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId)); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java deleted file mode 100644 index c2da872789..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ /dev/null @@ -1,871 +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; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.verify; - -import android.os.ConditionVariable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.testutil.DummyMainThread; -import com.google.android.exoplayer2.testutil.FakeMediaSource; -import com.google.android.exoplayer2.testutil.FakeShuffleOrder; -import com.google.android.exoplayer2.testutil.FakeTimeline; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.RobolectricUtil; -import com.google.android.exoplayer2.testutil.TimelineAsserts; -import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** Unit tests for {@link DynamicConcatenatingMediaSource} */ -@RunWith(RobolectricTestRunner.class) -@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) -public final class DynamicConcatenatingMediaSourceTest { - - private DynamicConcatenatingMediaSource mediaSource; - private MediaSourceTestRunner testRunner; - - @Before - public void setUp() throws Exception { - mediaSource = - new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null); - } - - @After - public void tearDown() throws Exception { - testRunner.release(); - } - - @Test - public void testPlaylistChangesAfterPreparation() throws IOException, InterruptedException { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - FakeMediaSource[] childSources = createMediaSources(7); - - // Add first source. - mediaSource.addMediaSource(childSources[0]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertWindowIds(timeline, 111); - - // Add at front of queue. - mediaSource.addMediaSource(0, childSources[1]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1); - TimelineAsserts.assertWindowIds(timeline, 222, 111); - - // Add at back of queue. - mediaSource.addMediaSource(childSources[2]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); - - // Add in the middle. - mediaSource.addMediaSource(1, childSources[3]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); - - // Add bulk. - mediaSource.addMediaSources( - 3, Arrays.asList(childSources[4], childSources[5], childSources[6])); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - - // Move sources. - mediaSource.moveMediaSource(2, 3); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); - mediaSource.moveMediaSource(3, 2); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - mediaSource.moveMediaSource(0, 6); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); - TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); - mediaSource.moveMediaSource(6, 0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); - - // Remove in the middle. - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(3); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(1); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); - for (int i = 3; i <= 6; i++) { - childSources[i].assertReleased(); - } - - // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(timeline.getWindowCount() - 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(timeline.getWindowCount() - 1); - assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - - // Assert all periods can be prepared. - testRunner.assertPrepareAndReleaseAllPeriods(); - - // Remove at front of queue. - mediaSource.removeMediaSource(0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 3); - TimelineAsserts.assertWindowIds(timeline, 111, 333); - childSources[1].assertReleased(); - - // Remove at back of queue. - mediaSource.removeMediaSource(1); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertWindowIds(timeline, 111); - childSources[2].assertReleased(); - - // Remove last source. - mediaSource.removeMediaSource(0); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertEmpty(timeline); - childSources[3].assertReleased(); - } - - @Test - public void testPlaylistChangesBeforePreparation() throws IOException, InterruptedException { - FakeMediaSource[] childSources = createMediaSources(4); - mediaSource.addMediaSource(childSources[0]); - mediaSource.addMediaSource(childSources[1]); - mediaSource.addMediaSource(0, childSources[2]); - mediaSource.moveMediaSource(0, 2); - mediaSource.removeMediaSource(0); - mediaSource.moveMediaSource(1, 0); - mediaSource.addMediaSource(1, childSources[3]); - testRunner.assertNoTimelineChange(); - - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); - TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - - testRunner.assertPrepareAndReleaseAllPeriods(); - testRunner.releaseSource(); - for (int i = 1; i < 4; i++) { - childSources[i].assertReleased(); - } - } - - @Test - public void testPlaylistWithLazyMediaSource() throws IOException, InterruptedException { - // Create some normal (immediately preparing) sources and some lazy sources whose timeline - // updates need to be triggered. - FakeMediaSource[] fastSources = createMediaSources(2); - final FakeMediaSource[] lazySources = new FakeMediaSource[4]; - for (int i = 0; i < 4; i++) { - lazySources[i] = new FakeMediaSource(null, null); - } - - // Add lazy sources and normal sources before preparation. Also remove one lazy source again - // before preparation to check it doesn't throw or change the result. - mediaSource.addMediaSource(lazySources[0]); - mediaSource.addMediaSource(0, fastSources[0]); - mediaSource.removeMediaSource(1); - mediaSource.addMediaSource(1, lazySources[1]); - testRunner.assertNoTimelineChange(); - - // Prepare and assert that the timeline contains all information for normal sources while having - // placeholder information for lazy sources. - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1); - TimelineAsserts.assertWindowIds(timeline, 111, null); - TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - - // Trigger source info refresh for lazy source and check that the timeline now contains all - // information for all windows. - testRunner.runOnPlaybackThread( - new Runnable() { - @Override - public void run() { - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - } - }); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 9); - TimelineAsserts.assertWindowIds(timeline, 111, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - testRunner.assertPrepareAndReleaseAllPeriods(); - - // Add further lazy and normal sources after preparation. Also remove one lazy source again to - // check it doesn't throw or change the result. - mediaSource.addMediaSource(1, lazySources[2]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(2, fastSources[1]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(0, lazySources[3]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.removeMediaSource(2); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); - TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); - - // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not - // called yet. - MediaPeriod lazyPeriod = - testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - 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(/* 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( - new Runnable() { - @Override - public void run() { - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - } - }); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); - TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); - TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); - assertThat(preparedCondition.getCount()).isEqualTo(0); - - // Release the period and source. - testRunner.releasePeriod(lazyPeriod); - testRunner.releaseSource(); - - // Assert all sources were fully released. - for (FakeMediaSource fastSource : fastSources) { - fastSource.assertReleased(); - } - for (FakeMediaSource lazySource : lazySources) { - lazySource.assertReleased(); - } - } - - @Test - public void testEmptyTimelineMediaSource() throws IOException, InterruptedException { - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); - 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) - })); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertEmpty(timeline); - - // Insert non-empty media source to leave empty sources at the start, the end, and the middle - // (with single and multiple empty sources in a row). - MediaSource[] mediaSources = createMediaSources(3); - mediaSource.addMediaSource(1, mediaSources[0]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(4, mediaSources[1]); - testRunner.assertTimelineChangeBlocking(); - mediaSource.addMediaSource(6, mediaSources[2]); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); - assertThat(timeline.getFirstWindowIndex(false)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(false)).isEqualTo(2); - assertThat(timeline.getFirstWindowIndex(true)).isEqualTo(2); - assertThat(timeline.getLastWindowIndex(true)).isEqualTo(0); - testRunner.assertPrepareAndReleaseAllPeriods(); - } - - @Test - 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), - }; - Timeline nonEmptyTimeline = new FakeTimeline(/* windowCount = */ 1); - - mediaSource.addMediaSources(Arrays.asList(childSources)); - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertEmpty(timeline); - - childSources[0].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1); - - childSources[2].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1); - - childSources[1].setNewSourceInfo(nonEmptyTimeline, /* newManifest== */ null); - timeline = testRunner.assertTimelineChangeBlocking(); - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - } - - @Test - public void testIllegalArguments() { - MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); - - // Null sources. - try { - mediaSource.addMediaSource(null); - fail("Null mediaSource not allowed."); - } catch (NullPointerException e) { - // Expected. - } - - MediaSource[] mediaSources = {validSource, null}; - try { - mediaSource.addMediaSources(Arrays.asList(mediaSources)); - fail("Null mediaSource not allowed."); - } catch (NullPointerException e) { - // Expected. - } - } - - @Test - public void testCustomCallbackBeforePreparationAddSingle() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(createFakeMediaSource(), runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddMultiple() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - /* index */ 0, - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationRemove() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSource(createFakeMediaSource()); - mediaSource.removeMediaSource(/* index */ 0, runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackBeforePreparationMove() { - Runnable runnable = Mockito.mock(Runnable.class); - - mediaSource.addMediaSources( - Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); - verify(runnable).run(); - } - - @Test - public void testCustomCallbackAfterPreparationAddSingle() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(1); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(1); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - /* index */ 0, - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), - timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationRemove() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSource(createFakeMediaSource()); - } - }); - testRunner.assertTimelineChangeBlocking(); - - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(0); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testCustomCallbackAfterPreparationMove() throws IOException { - DummyMainThread dummyMainThread = new DummyMainThread(); - try { - testRunner.prepareSource(); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.addMediaSources( - Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); - } - }); - testRunner.assertTimelineChangeBlocking(); - - final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); - dummyMainThread.runOnMainThread( - new Runnable() { - @Override - public void run() { - mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, timelineGrabber); - } - }); - Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); - assertThat(timeline.getWindowCount()).isEqualTo(2); - } finally { - dummyMainThread.release(); - } - } - - @Test - public void testPeriodCreationWithAds() throws IOException, InterruptedException { - // Create dynamic media source with ad child source. - Timeline timelineContentOnly = - new FakeTimeline( - new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); - Timeline timelineWithAds = - new FakeTimeline( - new TimelineWindowDefinition( - 2, - 222, - true, - false, - 10 * C.MICROS_PER_SECOND, - FakeTimeline.createAdPlaybackState( - /* adsPerAdGroup= */ 1, /* adGroupTimesUs= */ 0))); - FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); - FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - mediaSource.addMediaSource(mediaSourceContentOnly); - mediaSource.addMediaSource(mediaSourceWithAds); - - Timeline timeline = testRunner.prepareSource(); - - // Assert the timeline contains ad groups. - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); - - // Create all periods and assert period creation of child media sources has been called. - testRunner.assertPrepareAndReleaseAllPeriods(); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSourceContentOnly.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 0)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 0, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - mediaSourceWithAds.assertMediaPeriodCreated( - new MediaPeriodId( - /* periodIndex= */ 1, - /* adGroupIndex= */ 0, - /* adIndexInAdGroup= */ 0, - /* windowSequenceNumber= */ 1)); - } - - @Test - public void testAtomicTimelineWindowOrder() throws IOException { - // Release default test runner with non-atomic media source and replace with new test runner. - testRunner.release(); - DynamicConcatenatingMediaSource mediaSource = - new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null); - mediaSource.addMediaSources(Arrays.asList(createMediaSources(3))); - Timeline timeline = testRunner.prepareSource(); - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 2, 0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(0); - assertThat(timeline.getFirstWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(0); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ false)).isEqualTo(2); - assertThat(timeline.getLastWindowIndex(/* shuffleModeEnabled= */ true)).isEqualTo(2); - } - - @Test - public void testNestedTimeline() throws IOException { - DynamicConcatenatingMediaSource nestedSource1 = - new DynamicConcatenatingMediaSource(/* isAtomic= */ false, new FakeShuffleOrder(0)); - DynamicConcatenatingMediaSource nestedSource2 = - new DynamicConcatenatingMediaSource(/* isAtomic= */ true, new FakeShuffleOrder(0)); - mediaSource.addMediaSource(nestedSource1); - mediaSource.addMediaSource(nestedSource2); - testRunner.prepareSource(); - FakeMediaSource[] childSources = createMediaSources(4); - nestedSource1.addMediaSource(childSources[0]); - testRunner.assertTimelineChangeBlocking(); - nestedSource1.addMediaSource(childSources[1]); - testRunner.assertTimelineChangeBlocking(); - nestedSource2.addMediaSource(childSources[2]); - testRunner.assertTimelineChangeBlocking(); - nestedSource2.addMediaSource(childSources[3]); - Timeline timeline = testRunner.assertTimelineChangeBlocking(); - - TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); - TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3, 4); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ false, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ false, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ false, 1, 2, 3, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, 1, 3, C.INDEX_UNSET, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 1, 3, 0, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true, C.INDEX_UNSET, 0, 3, 1); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ONE, /* shuffleModeEnabled= */ true, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_ALL, /* shuffleModeEnabled= */ true, 2, 0, 3, 1); - } - - @Test - public void testRemoveChildSourceWithActiveMediaPeriod() throws IOException { - FakeMediaSource childSource = createFakeMediaSource(); - mediaSource.addMediaSource(childSource); - testRunner.prepareSource(); - MediaPeriod mediaPeriod = - testRunner.createPeriod( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0)); - mediaSource.removeMediaSource(/* index= */ 0); - testRunner.assertTimelineChangeBlocking(); - testRunner.releasePeriod(mediaPeriod); - childSource.assertReleased(); - testRunner.releaseSource(); - } - - @Test - public void testDuplicateMediaSources() throws IOException, InterruptedException { - FakeMediaSource childSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 2), /* manifest= */ null); - - mediaSource.addMediaSource(childSource); - mediaSource.addMediaSource(childSource); - testRunner.prepareSource(); - mediaSource.addMediaSources(Arrays.asList(childSource, childSource)); - Timeline timeline = testRunner.assertTimelineChangeBlocking(); - - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1); - testRunner.assertPrepareAndReleaseAllPeriods(); - assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 6), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 3), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 5), - new MediaPeriodId(/* periodIndex= */ 1, /* windowSequenceNumber= */ 7)); - - testRunner.releaseSource(); - childSource.assertReleased(); - } - - @Test - public void testDuplicateNestedMediaSources() throws IOException, InterruptedException { - FakeMediaSource childSource = - new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), /* manifest= */ null); - DynamicConcatenatingMediaSource nestedConcatenation = new DynamicConcatenatingMediaSource(); - - testRunner.prepareSource(); - mediaSource.addMediaSources( - Arrays.asList(childSource, nestedConcatenation, nestedConcatenation)); - testRunner.assertTimelineChangeBlocking(); - nestedConcatenation.addMediaSource(childSource); - testRunner.assertTimelineChangeBlocking(); - nestedConcatenation.addMediaSource(childSource); - Timeline timeline = testRunner.assertTimelineChangeBlocking(); - - TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1); - testRunner.assertPrepareAndReleaseAllPeriods(); - assertThat(childSource.getCreatedMediaPeriods()) - .containsAllOf( - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 0), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 1), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 2), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 3), - new MediaPeriodId(/* periodIndex= */ 0, /* windowSequenceNumber= */ 4)); - - testRunner.releaseSource(); - childSource.assertReleased(); - } - - 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); - } - return sources; - } - - private static FakeMediaSource createFakeMediaSource() { - return new FakeMediaSource(createFakeTimeline(/* index */ 0), null); - } - - private static FakeTimeline createFakeTimeline(int index) { - return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); - } - - private static final class TimelineGrabber implements Runnable { - - private final MediaSourceTestRunner testRunner; - private final ConditionVariable finishedCondition; - - private Timeline timeline; - private AssertionError error; - - public TimelineGrabber(MediaSourceTestRunner testRunner) { - this.testRunner = testRunner; - finishedCondition = new ConditionVariable(); - } - - @Override - public void run() { - try { - timeline = testRunner.assertTimelineChange(); - } catch (AssertionError e) { - error = e; - } - finishedCondition.open(); - } - - public Timeline assertTimelineChangeBlocking() { - assertThat(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); - if (error != null) { - throw error; - } - return timeline; - } - } -} From fcb796a80cd717f8797957b0187df44d2347ab3e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 28 Feb 2018 05:24:42 -0800 Subject: [PATCH 080/376] Migrate ExoPlayer Gradle build files. - Change 'compile' configuration (deprecared) to using 'implementation' and 'api' configurations instead. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187311778 --- RELEASENOTES.md | 6 ++++++ demos/cast/build.gradle | 16 +++++++++------- demos/ima/build.gradle | 13 +++++++------ demos/main/build.gradle | 23 ++++++++++++----------- extensions/cast/README.md | 2 +- extensions/cast/build.gradle | 25 +++++++++++++++---------- extensions/cronet/build.gradle | 13 +++++++------ extensions/ffmpeg/build.gradle | 2 +- extensions/flac/build.gradle | 4 ++-- extensions/gvr/README.md | 2 +- extensions/gvr/build.gradle | 4 ++-- extensions/ima/README.md | 2 +- extensions/ima/build.gradle | 13 ++++--------- extensions/jobdispatcher/README.md | 2 +- extensions/jobdispatcher/build.gradle | 4 ++-- extensions/leanback/README.md | 2 +- extensions/leanback/build.gradle | 4 ++-- extensions/mediasession/README.md | 2 +- extensions/mediasession/build.gradle | 4 ++-- extensions/okhttp/README.md | 2 +- extensions/okhttp/build.gradle | 5 +++-- extensions/opus/build.gradle | 2 +- extensions/rtmp/README.md | 2 +- extensions/rtmp/build.gradle | 5 +++-- extensions/vp9/build.gradle | 5 +++-- library/all/build.gradle | 10 +++++----- library/core/build.gradle | 18 +++++++++--------- library/dash/build.gradle | 6 +++--- library/hls/build.gradle | 6 +++--- library/smoothstreaming/build.gradle | 6 +++--- library/ui/build.gradle | 4 ++-- playbacktests/build.gradle | 8 ++++---- testutils/build.gradle | 9 +++++---- testutils_robolectric/build.gradle | 6 ++++-- 34 files changed, 127 insertions(+), 110 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4498093717..2af5d134d6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,12 @@ ### dev-v2 (not yet released) ### +* Gradle: Replace 'compile' configuration (deprecated) with 'implementation' and + 'api'. Note: This may lead to build breakage for applications upgrading from + previous version that rely on indirect dependency for certain modules. In such + cases, application developers need to add the missing dependency directly to + resolve this issue. You can read more about the new dependency configurations + [here](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations). * Downloading: Add `DownloadService`, `DownloadManager` and related classes ([#2643](https://github.com/google/ExoPlayer/issues/2643)). * MediaSources: diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 8f074c9238..9bf62c8fc3 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -42,11 +42,13 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'extension-cast') - compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'library-ui') + implementation project(modulePrefix + 'extension-cast') + implementation 'com.android.support:support-v4:' + supportLibraryVersion + implementation 'com.android.support:appcompat-v7:' + supportLibraryVersion + implementation 'com.android.support:recyclerview-v7:' + supportLibraryVersion } diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 5225c260f8..710e878d14 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -41,10 +41,11 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-ui') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'extension-ima') + 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-ima') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion } diff --git a/demos/main/build.gradle b/demos/main/build.gradle index f637e39ce4..5b199faf7a 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -55,15 +55,16 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') - withExtensionsCompile project(path: modulePrefix + 'extension-ffmpeg') - withExtensionsCompile project(path: modulePrefix + 'extension-flac') - withExtensionsCompile project(path: modulePrefix + 'extension-ima') - withExtensionsCompile project(path: modulePrefix + 'extension-opus') - withExtensionsCompile project(path: modulePrefix + 'extension-vp9') - withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') + implementation project(modulePrefix + 'library-hls') + implementation project(modulePrefix + 'library-smoothstreaming') + implementation project(modulePrefix + 'library-ui') + withExtensionsImplementation project(path: modulePrefix + 'extension-ffmpeg') + withExtensionsImplementation project(path: modulePrefix + 'extension-flac') + withExtensionsImplementation project(path: modulePrefix + 'extension-ima') + withExtensionsImplementation project(path: modulePrefix + 'extension-opus') + withExtensionsImplementation project(path: modulePrefix + 'extension-vp9') + withExtensionsImplementation project(path: modulePrefix + 'extension-rtmp') } diff --git a/extensions/cast/README.md b/extensions/cast/README.md index 8666690661..a0c34236c6 100644 --- a/extensions/cast/README.md +++ b/extensions/cast/README.md @@ -12,7 +12,7 @@ Cast receiver app. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-cast:rX.X.X' +implementation 'com.google.android.exoplayer:extension-cast:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 2f79c7a0ee..253a1922a8 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -26,19 +26,24 @@ android { } dependencies { - // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) - // is included via: + // These dependencies are necessary to force the supportLibraryVersion of + // com.android.support:support-v4, com.android.support:appcompat-v7 and + // com.android.support:mediarouter-v7 to be used. Else older versions are + // used, for example: // com.google.android.gms:play-services-cast-framework:11.4.2 // |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.android.support:support-v4:25.2.0 - compile 'com.android.support:support-v4:' + supportLibraryVersion - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion - compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion - compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-ui') - testCompile project(modulePrefix + 'testutils-robolectric') + api 'com.android.support:support-v4:' + supportLibraryVersion + api 'com.android.support:appcompat-v7:' + supportLibraryVersion + api 'com.android.support:mediarouter-v7:' + supportLibraryVersion + api 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-ui') + testImplementation project(modulePrefix + 'testutils') + testImplementation 'junit:junit:' + junitVersion + testImplementation 'org.mockito:mockito-core:' + mockitoVersion + testImplementation 'org.robolectric:robolectric:' + robolectricVersion + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 2d25c7299c..1cfb4f5513 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -35,12 +35,13 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile files('libs/cronet_api.jar') - compile files('libs/cronet_impl_common_java.jar') - compile files('libs/cronet_impl_native_java.jar') - testCompile project(modulePrefix + 'library') - testCompile project(modulePrefix + 'testutils-robolectric') + api files('libs/cronet_api.jar') + implementation files('libs/cronet_impl_common_java.jar') + implementation files('libs/cronet_impl_native_java.jar') + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + testImplementation project(modulePrefix + 'library') + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index 9820818f3e..e2d3a08e36 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-core') } ext { diff --git a/extensions/flac/build.gradle b/extensions/flac/build.gradle index 4d840d34ac..f617064ce5 100644 --- a/extensions/flac/build.gradle +++ b/extensions/flac/build.gradle @@ -31,8 +31,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - androidTestCompile project(modulePrefix + 'testutils') + implementation project(modulePrefix + 'library-core') + androidTestImplementation project(modulePrefix + 'testutils') } ext { diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 250cf58c2f..f5a52d3162 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -12,7 +12,7 @@ of surround sound and ambisonic soundfields. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' +implementation 'com.google.android.exoplayer:extension-gvr:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 8236024512..f146ba4df6 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -25,8 +25,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.google.vr:sdk-audio:1.80.0' + implementation project(modulePrefix + 'library-core') + implementation 'com.google.vr:sdk-audio:1.80.0' } ext { diff --git a/extensions/ima/README.md b/extensions/ima/README.md index a796ca8694..208d64fe71 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -12,7 +12,7 @@ alongside content. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-ima:rX.X.X' +implementation 'com.google.android.exoplayer:extension-ima:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 5038aaf5b9..3a20e378ae 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -26,7 +26,6 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') // This dependency is necessary to force the supportLibraryVersion of // com.android.support:support-v4 to be used. Else an older version (25.2.0) // is included via: @@ -34,14 +33,10 @@ dependencies { // |-- com.google.android.gms:play-services-ads-lite:11.4.2 // |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.android.support:support-v4:25.2.0 - compile 'com.android.support:support-v4:' + supportLibraryVersion - compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' - compile 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion - androidTestCompile project(modulePrefix + 'library') - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion - androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion + api 'com.android.support:support-v4:' + supportLibraryVersion + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' + implementation project(modulePrefix + 'library-core') + implementation 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion } ext { diff --git a/extensions/jobdispatcher/README.md b/extensions/jobdispatcher/README.md index d9efc77012..146aa3ca55 100644 --- a/extensions/jobdispatcher/README.md +++ b/extensions/jobdispatcher/README.md @@ -9,7 +9,7 @@ This extension provides a Scheduler implementation which uses [Firebase JobDispa The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-jobdispatcher:rX.X.X' +implementation 'com.google.android.exoplayer:extension-jobdispatcher:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/jobdispatcher/build.gradle b/extensions/jobdispatcher/build.gradle index fd5fce9ec8..f4a8751c67 100644 --- a/extensions/jobdispatcher/build.gradle +++ b/extensions/jobdispatcher/build.gradle @@ -27,8 +27,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.firebase:firebase-jobdispatcher:0.8.5' + implementation project(modulePrefix + 'library-core') + implementation 'com.firebase:firebase-jobdispatcher:0.8.5' } ext { diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md index 1fa71c9a8c..e9a0e6fbc4 100644 --- a/extensions/leanback/README.md +++ b/extensions/leanback/README.md @@ -11,7 +11,7 @@ ExoPlayer. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-leanback:rX.X.X' +implementation 'com.google.android.exoplayer:extension-leanback:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index d8952ca2b8..dc187a5709 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -25,8 +25,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile('com.android.support:leanback-v17:' + supportLibraryVersion) + implementation project(modulePrefix + 'library-core') + implementation('com.android.support:leanback-v17:' + supportLibraryVersion) } ext { diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 3278e8dba5..f89b27f0b4 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -12,7 +12,7 @@ behaviour can be extended to support other playback and custom actions. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-mediasession:rX.X.X' +implementation 'com.google.android.exoplayer:extension-mediasession:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 651bd952f8..eaaf078b5c 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -25,8 +25,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.android.support:support-media-compat:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-media-compat:' + supportLibraryVersion } ext { diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index e40535d4e8..1469b6cd51 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -19,7 +19,7 @@ licensed separately. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-okhttp:rX.X.X' +implementation 'com.google.android.exoplayer:extension-okhttp:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 13bcff8a4e..2da245b1a5 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -30,8 +30,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile('com.squareup.okhttp3:okhttp:3.9.0') { + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation('com.squareup.okhttp3:okhttp:3.9.0') { exclude group: 'org.json' } } diff --git a/extensions/opus/build.gradle b/extensions/opus/build.gradle index 41b428070f..2d20c65697 100644 --- a/extensions/opus/build.gradle +++ b/extensions/opus/build.gradle @@ -31,7 +31,7 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-core') } ext { diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index fb822b8326..cf7edd7643 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -20,7 +20,7 @@ Android, which is licensed separately. The easiest way to use the extension is to add it as a gradle dependency: ```gradle -compile 'com.google.android.exoplayer:extension-rtmp:rX.X.X' +implementation 'com.google.android.exoplayer:extension-rtmp:rX.X.X' ``` where `rX.X.X` is the version, which must match the version of the ExoPlayer diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index 2afa4a4ea7..c34e0b9999 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -25,8 +25,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'net.butterflytv.utils:rtmp-client:3.0.1' + implementation project(modulePrefix + 'library-core') + implementation 'net.butterflytv.utils:rtmp-client:3.0.1' + implementation 'com.android.support:support-annotations:' + supportLibraryVersion } ext { diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle index 3d68e1428f..7dc95b388f 100644 --- a/extensions/vp9/build.gradle +++ b/extensions/vp9/build.gradle @@ -31,8 +31,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - androidTestCompile 'com.google.truth:truth:' + truthVersion + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestImplementation 'com.google.truth:truth:' + truthVersion } ext { diff --git a/library/all/build.gradle b/library/all/build.gradle index 79ed9c747b..bb832ba0ff 100644 --- a/library/all/build.gradle +++ b/library/all/build.gradle @@ -25,11 +25,11 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile project(modulePrefix + 'library-dash') - compile project(modulePrefix + 'library-hls') - compile project(modulePrefix + 'library-smoothstreaming') - compile project(modulePrefix + 'library-ui') + api project(modulePrefix + 'library-core') + api project(modulePrefix + 'library-dash') + api project(modulePrefix + 'library-hls') + api project(modulePrefix + 'library-smoothstreaming') + api project(modulePrefix + 'library-ui') } ext { diff --git a/library/core/build.gradle b/library/core/build.gradle index 3d655ba543..fe6045c2e7 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -45,15 +45,15 @@ android { } dependencies { - compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion - androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion - androidTestCompile 'com.google.truth:truth:' + truthVersion - androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion - testCompile 'com.google.truth:truth:' + truthVersion - testCompile 'junit:junit:' + junitVersion - testCompile 'org.mockito:mockito-core:' + mockitoVersion - testCompile 'org.robolectric:robolectric:' + robolectricVersion + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestImplementation 'com.google.dexmaker:dexmaker:' + dexmakerVersion + androidTestImplementation 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion + androidTestImplementation 'com.google.truth:truth:' + truthVersion + androidTestImplementation 'org.mockito:mockito-core:' + mockitoVersion + testImplementation 'com.google.truth:truth:' + truthVersion + testImplementation 'junit:junit:' + junitVersion + testImplementation 'org.mockito:mockito-core:' + mockitoVersion + testImplementation 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 6cf6f64443..d2692eb7d9 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -33,9 +33,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.android.support:support-annotations:' + supportLibraryVersion - testCompile project(modulePrefix + 'testutils-robolectric') + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 41e0b71da2..c2268a3007 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -33,9 +33,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.android.support:support-annotations:' + supportLibraryVersion - testCompile project(modulePrefix + 'testutils-robolectric') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index b85f25e656..6ca5570a93 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -33,9 +33,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.android.support:support-annotations:' + supportLibraryVersion - testCompile project(modulePrefix + 'testutils-robolectric') + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + testImplementation project(modulePrefix + 'testutils-robolectric') } ext { diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 89734ed806..9689fcef97 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -33,8 +33,8 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'com.android.support:support-annotations:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion } ext { diff --git a/playbacktests/build.gradle b/playbacktests/build.gradle index 6cd56868f9..d5d524b5a5 100644 --- a/playbacktests/build.gradle +++ b/playbacktests/build.gradle @@ -25,8 +25,8 @@ android { } dependencies { - androidTestCompile project(modulePrefix + 'library-core') - androidTestCompile project(modulePrefix + 'library-dash') - androidTestCompile project(modulePrefix + 'library-hls') - androidTestCompile project(modulePrefix + 'testutils') + androidTestImplementation project(modulePrefix + 'library-core') + androidTestImplementation project(modulePrefix + 'library-dash') + androidTestImplementation project(modulePrefix + 'library-hls') + androidTestImplementation project(modulePrefix + 'testutils') } diff --git a/testutils/build.gradle b/testutils/build.gradle index 11ec55c047..a7f05a2c5e 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -32,8 +32,9 @@ android { } dependencies { - compile project(modulePrefix + 'library-core') - compile 'org.mockito:mockito-core:' + mockitoVersion - compile 'com.google.truth:truth:' + truthVersion - testCompile project(modulePrefix + 'testutils-robolectric') + api 'org.mockito:mockito-core:' + mockitoVersion + api 'com.google.truth:truth:' + truthVersion + implementation 'com.android.support:support-annotations:' + supportLibraryVersion + implementation project(modulePrefix + 'library-core') + testImplementation project(modulePrefix + 'testutils-robolectric') } diff --git a/testutils_robolectric/build.gradle b/testutils_robolectric/build.gradle index f5a427a4b3..c221149c29 100644 --- a/testutils_robolectric/build.gradle +++ b/testutils_robolectric/build.gradle @@ -32,6 +32,8 @@ android { } dependencies { - compile project(modulePrefix + 'testutils') - compile 'org.robolectric:robolectric:' + robolectricVersion + api 'org.robolectric:robolectric:' + robolectricVersion + api project(modulePrefix + 'testutils') + implementation project(modulePrefix + 'library-core') + implementation 'com.android.support:support-annotations:' + supportLibraryVersion } From 75c3bfb55c6827b796fad7f322b3c3b06083bac0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 28 Feb 2018 05:46:18 -0800 Subject: [PATCH 081/376] Rename Listener for timeline update to avoid confusion with MediaSourceEventListener. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=187313128 --- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 4 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 4 ++-- .../exoplayer2/source/BaseMediaSource.java | 9 +++---- .../source/CompositeMediaSource.java | 8 +++---- .../exoplayer2/source/MediaPeriod.java | 5 ++-- .../exoplayer2/source/MediaSource.java | 24 +++++++++---------- .../source/dash/DashMediaSource.java | 4 ++-- .../testutil/MediaSourceTestRunner.java | 7 ++++-- 8 files changed, 35 insertions(+), 30 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 1010a27178..d3e1d9725e 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -38,7 +38,7 @@ public final class ImaAdsMediaSource extends BaseMediaSource { private final AdsMediaSource adsMediaSource; - private Listener adsMediaSourceListener; + private SourceInfoRefreshListener adsMediaSourceListener; /** * Constructs a new source that inserts ads linearly with the content specified by @@ -79,7 +79,7 @@ public final class ImaAdsMediaSource extends BaseMediaSource { @Override public void prepareSourceInternal(final ExoPlayer player, boolean isTopLevelSource) { adsMediaSourceListener = - new Listener() { + new SourceInfoRefreshListener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { 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 2272bef573..3dd3fbcded 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 @@ -48,7 +48,7 @@ import java.util.Collections; implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, - MediaSource.Listener, + MediaSource.SourceInfoRefreshListener, PlaybackParameterListener, PlayerMessage.Sender { @@ -243,7 +243,7 @@ import java.util.Collections; return internalPlaybackThread.getLooper(); } - // MediaSource.Listener implementation. + // MediaSource.SourceInfoRefreshListener implementation. @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { 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 a6924b5e05..d3af28da92 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 @@ -29,7 +29,7 @@ import java.util.ArrayList; */ public abstract class BaseMediaSource implements MediaSource { - private final ArrayList sourceInfoListeners; + private final ArrayList sourceInfoListeners; private ExoPlayer player; private Timeline timeline; @@ -65,13 +65,14 @@ public abstract class BaseMediaSource implements MediaSource { protected final void refreshSourceInfo(Timeline timeline, @Nullable Object manifest) { this.timeline = timeline; this.manifest = manifest; - for (Listener listener : sourceInfoListeners) { + for (SourceInfoRefreshListener listener : sourceInfoListeners) { listener.onSourceInfoRefreshed(/* source= */ this, timeline, manifest); } } @Override - public final void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + public final void prepareSource( + ExoPlayer player, boolean isTopLevelSource, SourceInfoRefreshListener listener) { Assertions.checkArgument(this.player == null || this.player == player); sourceInfoListeners.add(listener); if (this.player == null) { @@ -83,7 +84,7 @@ public abstract class BaseMediaSource implements MediaSource { } @Override - public final void releaseSource(Listener listener) { + public final void releaseSource(SourceInfoRefreshListener listener) { sourceInfoListeners.remove(listener); if (sourceInfoListeners.isEmpty()) { player = 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 4f5a3c7a4e..f43010dcd4 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 @@ -88,8 +88,8 @@ public abstract class CompositeMediaSource extends BaseMediaSource { */ protected final void prepareChildSource(@Nullable final T id, MediaSource mediaSource) { Assertions.checkArgument(!childSources.containsKey(id)); - Listener sourceListener = - new Listener() { + SourceInfoRefreshListener sourceListener = + new SourceInfoRefreshListener() { @Override public void onSourceInfoRefreshed( MediaSource source, Timeline timeline, @Nullable Object manifest) { @@ -113,9 +113,9 @@ public abstract class CompositeMediaSource extends BaseMediaSource { private static final class MediaSourceAndListener { public final MediaSource mediaSource; - public final Listener listener; + public final SourceInfoRefreshListener listener; - public MediaSourceAndListener(MediaSource mediaSource, Listener listener) { + public MediaSourceAndListener(MediaSource mediaSource, SourceInfoRefreshListener listener) { this.mediaSource = mediaSource; this.listener = listener; } 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 a5b2314d78..63a1ef53d0 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 @@ -53,8 +53,9 @@ public interface MediaPeriod extends SequenceableLoader { * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * *

    If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, - * Object)} will be called before {@code callback.onPrepared}. + * becoming known), {@link + * MediaSource.SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} + * 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 aec8ed47af..5cf35bcccc 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 @@ -29,8 +29,9 @@ import java.io.IOException; *