diff --git a/library/core/build.gradle b/library/core/build.gradle
index 00f5cd27eb..d45e405b41 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -72,6 +72,8 @@ dependencies {
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
testImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
testAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
+ androidTestImplementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
+ androidTestAnnotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
}
ext {
diff --git a/library/core/src/test/assets/mp4/testvid_1022ms.mp4 b/library/core/src/test/assets/mp4/testvid_1022ms.mp4
new file mode 100644
index 0000000000..bbd2729c4d
Binary files /dev/null and b/library/core/src/test/assets/mp4/testvid_1022ms.mp4 differ
diff --git a/testutils/build.gradle b/testutils/build.gradle
index e4faea1ee8..2ef377ba5d 100644
--- a/testutils/build.gradle
+++ b/testutils/build.gradle
@@ -41,5 +41,7 @@ dependencies {
api 'com.google.truth:truth:' + truthVersion
implementation 'com.android.support:support-annotations:' + supportLibraryVersion
implementation project(modulePrefix + 'library-core')
+ implementation 'com.google.auto.value:auto-value-annotations:' + autoValueVersion
+ annotationProcessor 'com.google.auto.value:auto-value:' + autoValueVersion
testImplementation project(modulePrefix + 'testutils-robolectric')
}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/MetadataRetrieverTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/MetadataRetrieverTestRunner.java
new file mode 100644
index 0000000000..152fcc6ee7
--- /dev/null
+++ b/testutils/src/main/java/com/google/android/exoplayer2/MetadataRetrieverTestRunner.java
@@ -0,0 +1,493 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.Player.TimelineChangeReason;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.testutil.FakeRenderer;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.ClosedSource;
+import com.google.android.exoplayer2.util.ConditionVariable;
+import com.google.auto.value.AutoValue;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Helper class to run {@link MetadataRetriever} tests.
+ *
+ *
The tests will be run on a separate thread with a looper.
+ */
+@ClosedSource(reason = "Not ready yet")
+/* package */ final class MetadataRetrieverTestRunner
+ implements MetadataRetriever.MediaSourceCallback, MetadataRetriever.MetadataCallback {
+
+ /**
+ * Represents callback data for {@link
+ * MetadataRetriever.MediaSourceCallback#onTimelineUpdated(Timeline, Object, int)}.
+ */
+ @AutoValue
+ public abstract static class PrepareCallbackData {
+ abstract @Nullable Timeline timeline();
+
+ abstract @Nullable Object manifest();
+
+ abstract @TimelineChangeReason int reason();
+
+ /** Creates a new {@link PrepareCallbackData}. */
+ public static PrepareCallbackData prepareCallbackData(
+ @Nullable Timeline expectedTimeline,
+ @Nullable Object expectedManifest,
+ @TimelineChangeReason int expectedReason) {
+ return new AutoValue_MetadataRetrieverTestRunner_PrepareCallbackData(
+ expectedTimeline, expectedManifest, expectedReason);
+ }
+ }
+
+ /**
+ * Represents callback data for {@link
+ * MetadataRetriever.MetadataCallback#onMetadataAvailable(TrackGroupArray, Timeline, int, int)}.
+ */
+ @AutoValue
+ public abstract static class MetadataCallbackData {
+ abstract @Nullable TrackGroupArray trackGroupArray();
+
+ abstract @Nullable Timeline timeline();
+
+ abstract int windowIndex();
+
+ abstract int periodIndex();
+
+ /** Creates a new {@link MetadataCallbackData}. */
+ public static MetadataCallbackData metadataCallbackData(
+ @Nullable TrackGroupArray expectedTrackGroupArray,
+ @Nullable Timeline expectedTimeline,
+ int expectedWindowIndex,
+ int expectedPeriodIndex) {
+ return new AutoValue_MetadataRetrieverTestRunner_MetadataCallbackData(
+ expectedTrackGroupArray, expectedTimeline, expectedWindowIndex, expectedPeriodIndex);
+ }
+ }
+
+ /** Factory to create the {@link MetadataRetriever} under test. */
+ /* package */ interface TestMetadataRetrieverFactory {
+ MetadataRetriever createMetadataRetriever(
+ Clock clock, Renderer[] renderers, Looper eventLooper);
+ }
+
+ private static final TestMetadataRetrieverFactory DEFAULT_TEST_METADATA_RETRIEVER_FACTORY =
+ new TestMetadataRetrieverFactory() {
+ @Override
+ public MetadataRetriever createMetadataRetriever(
+ Clock clock, Renderer[] renderers, Looper eventLooper) {
+ return new MetadataRetrieverImpl(clock, renderers, eventLooper);
+ }
+ };
+
+ private static final Renderer[] FAKE_RENDERERS = new Renderer[] {new FakeRenderer()};
+ private static final long DEFAULT_TIMEOUT_MS = 50_000;
+
+ private final Handler handler;
+ private final HandlerThread testThread;
+
+ private final List preparedCallbackData;
+ private final List metadataCallbackData;
+ private final List failedQueryExceptions;
+
+ private MetadataRetriever metadataRetriever;
+
+ /**
+ * Creates a new test runner, starts its test runner thread and creates a new {@link
+ * MetadataRetriever} under test using the default factory.
+ *
+ * @return The newly created test runner.
+ * @throws InterruptedException If the test thread gets interrupted while waiting for the {@link
+ * MetadataRetriever} under test being created.
+ */
+ public static MetadataRetrieverTestRunner newTestRunner() throws InterruptedException {
+ return newTestRunner(DEFAULT_TEST_METADATA_RETRIEVER_FACTORY);
+ }
+
+ /**
+ * Creates a new test runner, starts its test runner thread and creates a new {@link
+ * MetadataRetriever} under test using the given {@link TestMetadataRetrieverFactory}.
+ *
+ * @param metadataRetrieverFactory The factory used to create the {@link MetadataRetriever} under
+ * test.
+ * @return The newly created test runner.
+ * @throws InterruptedException If the test thread gets interrupted while waiting for the {@link
+ * MetadataRetriever} under test being created.
+ */
+ /* package */ static MetadataRetrieverTestRunner newTestRunner(
+ TestMetadataRetrieverFactory metadataRetrieverFactory) throws InterruptedException {
+ MetadataRetrieverTestRunner metadataRetrieverTestRunner = new MetadataRetrieverTestRunner();
+ metadataRetrieverTestRunner.startTestRunnerThreadBlocking(metadataRetrieverFactory);
+ return metadataRetrieverTestRunner;
+ }
+
+ private MetadataRetrieverTestRunner() {
+ testThread = new HandlerThread("Test thread");
+ testThread.start();
+ handler = new Handler(testThread.getLooper());
+ preparedCallbackData = new ArrayList<>();
+ metadataCallbackData = new ArrayList<>();
+ failedQueryExceptions = new ArrayList<>();
+ }
+
+ /** Returns the {@link MetadataRetriever} under-test. */
+ public MetadataRetriever getMetadataRetriever() {
+ return metadataRetriever;
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to prepare the given media source on the
+ * test runner thread, and return immediately.
+ *
+ * @param mediaSource The {@link MediaSource} to be prepared.
+ */
+ public void prepareAsync(MediaSource mediaSource) {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.prepare(mediaSource, MetadataRetrieverTestRunner.this);
+ }
+ });
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to prepare the given media source on the
+ * test runner thread, and wait until one of the callbacks from {@link
+ * MetadataRetriever.MediaSourceCallback} is called, or until the {@link #DEFAULT_TIMEOUT_MS}
+ * passed.
+ *
+ * @param mediaSource The {@link MediaSource} to be prepared.
+ * @throws TimeoutException If the test runner did not finish within the specified timeout.
+ * @throws InterruptedException If the test thread gets interrupted while waiting.
+ */
+ public void prepareBlocking(MediaSource mediaSource)
+ throws InterruptedException, TimeoutException {
+ ConditionVariable callbackReceived = new ConditionVariable();
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.prepare(
+ mediaSource, new UnblockingMediaSourceCallback(callbackReceived));
+ }
+ });
+ if (!callbackReceived.block(DEFAULT_TIMEOUT_MS)) {
+ throw new TimeoutException(
+ "Test metadata retriever timed out waiting for preparing media source.");
+ }
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to call {@link
+ * MetadataRetriever#getMetadata(MetadataRetriever.MetadataCallback)} and returns immediately.
+ */
+ public void getMetadataAsync() {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.getMetadata(MetadataRetrieverTestRunner.this);
+ }
+ });
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to call {@link
+ * MetadataRetriever#getMetadata(long, MetadataRetriever.MetadataCallback)} and returns
+ * immediately.
+ */
+ public void getMetadataAsync(long positionMs) {
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.getMetadata(positionMs, MetadataRetrieverTestRunner.this);
+ }
+ });
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to call {@link
+ * MetadataRetriever#getMetadata(MetadataRetriever.MetadataCallback)} on test runner thread, and
+ * wait until one of the callbacks from {@link MetadataRetriever.MetadataCallback} is called, or
+ * until the {@link #DEFAULT_TIMEOUT_MS} passed.
+ *
+ * @throws TimeoutException If the test runner did not finish within the specified timeout.
+ * @throws InterruptedException If the test thread gets interrupted while waiting.
+ */
+ public void getMetadataBlocking() throws InterruptedException, TimeoutException {
+ getMetadataBlockingImpl(/* callWithParam= */ false, /* positionMs= */ 0);
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to call {@link
+ * MetadataRetriever#getMetadata(long, MetadataRetriever.MetadataCallback)} on test runner thread,
+ * and wait until one of the callbacks from {@link MetadataRetriever.MetadataCallback}\ is called,
+ * or until the {@link #DEFAULT_TIMEOUT_MS} passed.
+ *
+ * @throws TimeoutException If the test runner did not finish within the specified timeout.
+ * @throws InterruptedException If the test thread gets interrupted while waiting.
+ */
+ public void getMetadataBlocking(long positionMs) throws InterruptedException, TimeoutException {
+ getMetadataBlockingImpl(/* callWithParam= */ true, positionMs);
+ }
+
+ /**
+ * Instructs the {@link MetadataRetriever} under test to call {@link
+ * MetadataRetriever#setWindowIndex(int)} on test runner thread, and wait until it's done, or
+ * until the {@link #DEFAULT_TIMEOUT_MS} passed.
+ *
+ * @throws InterruptedException If the test runner did not finish within the specified timeout.
+ */
+ public void setWindowIndex(int windowIndex) throws InterruptedException {
+ runOnTestThreadBlocking(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.setWindowIndex(windowIndex);
+ }
+ });
+ }
+
+ /** Releases the {@link MetadataRetriever} under test and stops the test thread. */
+ public void release() throws InterruptedException {
+ runOnTestThreadBlocking(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever.release();
+ }
+ });
+ handler.removeCallbacksAndMessages(null);
+ testThread.quit();
+ }
+
+ // Assertions on retriever behavior.
+
+ /**
+ * Asserts that the data reported by {@link
+ * MetadataRetriever.MediaSourceCallback#onTimelineUpdated(Timeline, Object, int)} are equal to
+ * the provided data.
+ *
+ * @param preparedCallbackData A list of expected {@link PrepareCallbackData}s.
+ */
+ public void assertPrepareCallbackDataEqual(PrepareCallbackData... preparedCallbackData) {
+ assertThat(this.preparedCallbackData).containsExactlyElementsIn(preparedCallbackData).inOrder();
+ }
+
+ /**
+ * Asserts that the data reported by {@link
+ * MetadataRetriever.MetadataCallback#onMetadataAvailable(TrackGroupArray, Timeline, int, int)}
+ * are equal to the provided data.
+ *
+ * @param metadataCallbackData A list of expected {@link MetadataCallbackData}s.
+ */
+ public void assertMetadataCallbackDataEqual(MetadataCallbackData... metadataCallbackData) {
+ assertThat(this.metadataCallbackData).containsExactlyElementsIn(metadataCallbackData).inOrder();
+ }
+
+ /** Asserts that no exception occurred during the test. */
+ public void assertNoException() {
+ assertThat(this.failedQueryExceptions).isEmpty();
+ }
+
+ /**
+ * Returns list of {@link PrepareCallbackData} that were reported in {@link
+ * MetadataRetriever.MediaSourceCallback#onTimelineUpdated(Timeline, Object, int)} in order of
+ * occurrence.
+ */
+ public List getPrepareCallbackData() {
+ return this.preparedCallbackData;
+ }
+
+ /**
+ * Returns list of {@link MetadataCallbackData} that were reported in {@link
+ * MetadataRetriever.MetadataCallback#onMetadataAvailable(TrackGroupArray, Timeline, int, int)}}
+ * in order of occurrence.
+ */
+ public List getMetadataCallbackData() {
+ return this.metadataCallbackData;
+ }
+
+ /**
+ * Returns list of {@link Exception} that were reported in either {@link
+ * MetadataRetriever.MediaSourceCallback#onTimelineUnavailable(Exception)} and {@link
+ * MetadataRetriever.MetadataCallback#onMetadataUnavailable(Exception)} in order of occurrence.
+ */
+ public List getFailedQueryExceptions() {
+ return this.failedQueryExceptions;
+ }
+
+ /**
+ * Asserts that the {@link MetadataRetriever#getWindowDurationMs()} is equal to the given value.
+ */
+ public void assertWindowDurationMs(long windowDurationMs) throws InterruptedException {
+ AtomicLong actualWindowDurationMs = new AtomicLong();
+ runOnTestThreadBlocking(
+ new Runnable() {
+ @Override
+ public void run() {
+ actualWindowDurationMs.set(metadataRetriever.getWindowDurationMs());
+ }
+ });
+ assertThat(actualWindowDurationMs.get()).isEqualTo(windowDurationMs);
+ }
+
+ // MetadataRetriever.MediaSourceCallback implementation.
+
+ @Override
+ public void onTimelineUpdated(Timeline timeline, @Nullable Object manifest, int reason) {
+ preparedCallbackData.add(PrepareCallbackData.prepareCallbackData(timeline, manifest, reason));
+ }
+
+ @Override
+ public void onTimelineUnavailable(Exception exception) {
+ failedQueryExceptions.add(exception);
+ }
+
+ // MetadataRetriever.MetadataCallback implementation.
+
+ @Override
+ public void onMetadataAvailable(
+ TrackGroupArray trackGroupArray, Timeline timeline, int windowIndex, int periodIndex) {
+ metadataCallbackData.add(
+ MetadataCallbackData.metadataCallbackData(
+ trackGroupArray, timeline, windowIndex, periodIndex));
+ }
+
+ @Override
+ public void onMetadataUnavailable(Exception exception) {
+ failedQueryExceptions.add(exception);
+ }
+
+ /**
+ * Starts the test runner on its own thread. This will trigger the creation of the {@link
+ * MetadataRetriever}.
+ *
+ * @param metadataRetrieverFactory The factory to create the {@link MetadataRetriever} under test.
+ */
+ private void startTestRunnerThreadBlocking(TestMetadataRetrieverFactory metadataRetrieverFactory)
+ throws InterruptedException {
+ runOnTestThreadBlocking(
+ new Runnable() {
+ @Override
+ public void run() {
+ metadataRetriever =
+ metadataRetrieverFactory.createMetadataRetriever(
+ Clock.DEFAULT, FAKE_RENDERERS, Looper.myLooper());
+ }
+ });
+ }
+
+ private void runOnTestThreadBlocking(Runnable runnable) throws InterruptedException {
+ ConditionVariable conditionVariable = new ConditionVariable();
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ conditionVariable.open();
+ }
+ });
+ conditionVariable.block(DEFAULT_TIMEOUT_MS);
+ }
+
+ private void getMetadataBlockingImpl(boolean callWithParam, long positionMs)
+ throws InterruptedException, TimeoutException {
+ ConditionVariable callbackReceived = new ConditionVariable();
+ handler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ MetadataRetriever.MetadataCallback unblockingMetadataCallback =
+ new UnblockingMetadataCallback(callbackReceived);
+ if (callWithParam) {
+ metadataRetriever.getMetadata(positionMs, unblockingMetadataCallback);
+ } else {
+ metadataRetriever.getMetadata(unblockingMetadataCallback);
+ }
+ }
+ });
+ if (!callbackReceived.block(DEFAULT_TIMEOUT_MS)) {
+ throw new TimeoutException(
+ "Test metadata retriever timed out waiting for get metadata callback.");
+ }
+ }
+
+ /**
+ * A {@link MetadataRetriever.MediaSourceCallback} that will unblock a {@link ConditionVariable}
+ * when one of the callback is called.
+ */
+ private class UnblockingMediaSourceCallback implements MetadataRetriever.MediaSourceCallback {
+ private final ConditionVariable blockedCondition;
+
+ public UnblockingMediaSourceCallback(ConditionVariable blockedCondition) {
+ this.blockedCondition = blockedCondition;
+ }
+
+ @Override
+ public void onTimelineUpdated(Timeline timeline, @Nullable Object manifest, int reason) {
+ MetadataRetrieverTestRunner.this.onTimelineUpdated(timeline, manifest, reason);
+ blockedCondition.open();
+ }
+
+ @Override
+ public void onTimelineUnavailable(Exception exception) {
+ MetadataRetrieverTestRunner.this.onTimelineUnavailable(exception);
+ blockedCondition.open();
+ }
+ }
+
+ /**
+ * A {@link MetadataRetriever.MetadataCallback} that will unblock a {@link ConditionVariable} when
+ * one of the callback is called.
+ */
+ private final class UnblockingMetadataCallback implements MetadataRetriever.MetadataCallback {
+ private final ConditionVariable blockedCondition;
+
+ private UnblockingMetadataCallback(ConditionVariable blockedCondition) {
+ this.blockedCondition = blockedCondition;
+ }
+
+ @Override
+ public void onMetadataAvailable(
+ TrackGroupArray trackGroupArray, Timeline timeline, int windowIndex, int periodIndex) {
+ MetadataRetrieverTestRunner.this.onMetadataAvailable(
+ trackGroupArray, timeline, windowIndex, periodIndex);
+ blockedCondition.open();
+ }
+
+ @Override
+ public void onMetadataUnavailable(Exception exception) {
+ MetadataRetrieverTestRunner.this.onMetadataUnavailable(exception);
+ blockedCondition.open();
+ }
+ }
+}