Do not advance SystemClock manually in tests

Changes MetadataRetriever and Transformer so that their
respective tests don't need to manually control the SystemClock
in order to execute taks posted with delay from Loader.

PiperOrigin-RevId: 345024140
This commit is contained in:
christosts 2020-12-01 16:20:44 +00:00 committed by Oliver Woodman
parent 1a5d79b78a
commit 1ff0965a10
6 changed files with 78 additions and 47 deletions

View File

@ -22,6 +22,7 @@ import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Message; import android.os.Message;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor;
@ -32,7 +33,8 @@ import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -56,13 +58,7 @@ public final class MetadataRetriever {
*/ */
public static ListenableFuture<TrackGroupArray> retrieveMetadata( public static ListenableFuture<TrackGroupArray> retrieveMetadata(
Context context, MediaItem mediaItem) { Context context, MediaItem mediaItem) {
ExtractorsFactory extractorsFactory = return retrieveMetadata(context, mediaItem, Clock.DEFAULT);
new DefaultExtractorsFactory()
.setMp4ExtractorFlags(
Mp4Extractor.FLAG_READ_MOTION_PHOTO_METADATA | Mp4Extractor.FLAG_READ_SEF_DATA);
MediaSourceFactory mediaSourceFactory =
new DefaultMediaSourceFactory(context, extractorsFactory);
return retrieveMetadata(mediaSourceFactory, mediaItem);
} }
/** /**
@ -77,9 +73,26 @@ public final class MetadataRetriever {
*/ */
public static ListenableFuture<TrackGroupArray> retrieveMetadata( public static ListenableFuture<TrackGroupArray> retrieveMetadata(
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) { MediaSourceFactory mediaSourceFactory, MediaItem mediaItem) {
return retrieveMetadata(mediaSourceFactory, mediaItem, Clock.DEFAULT);
}
@VisibleForTesting
/* package */ static ListenableFuture<TrackGroupArray> retrieveMetadata(
Context context, MediaItem mediaItem, Clock clock) {
ExtractorsFactory extractorsFactory =
new DefaultExtractorsFactory()
.setMp4ExtractorFlags(
Mp4Extractor.FLAG_READ_MOTION_PHOTO_METADATA | Mp4Extractor.FLAG_READ_SEF_DATA);
MediaSourceFactory mediaSourceFactory =
new DefaultMediaSourceFactory(context, extractorsFactory);
return retrieveMetadata(mediaSourceFactory, mediaItem, clock);
}
private static ListenableFuture<TrackGroupArray> retrieveMetadata(
MediaSourceFactory mediaSourceFactory, MediaItem mediaItem, Clock clock) {
// Recreate thread and handler every time this method is called so that it can be used // Recreate thread and handler every time this method is called so that it can be used
// concurrently. // concurrently.
return new MetadataRetrieverInternal(mediaSourceFactory).retrieveMetadata(mediaItem); return new MetadataRetrieverInternal(mediaSourceFactory, clock).retrieveMetadata(mediaItem);
} }
private static final class MetadataRetrieverInternal { private static final class MetadataRetrieverInternal {
@ -91,15 +104,15 @@ public final class MetadataRetriever {
private final MediaSourceFactory mediaSourceFactory; private final MediaSourceFactory mediaSourceFactory;
private final HandlerThread mediaSourceThread; private final HandlerThread mediaSourceThread;
private final Handler mediaSourceHandler; private final HandlerWrapper mediaSourceHandler;
private final SettableFuture<TrackGroupArray> trackGroupsFuture; private final SettableFuture<TrackGroupArray> trackGroupsFuture;
public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory) { public MetadataRetrieverInternal(MediaSourceFactory mediaSourceFactory, Clock clock) {
this.mediaSourceFactory = mediaSourceFactory; this.mediaSourceFactory = mediaSourceFactory;
mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever"); mediaSourceThread = new HandlerThread("ExoPlayer:MetadataRetriever");
mediaSourceThread.start(); mediaSourceThread.start();
mediaSourceHandler = mediaSourceHandler =
Util.createHandler(mediaSourceThread.getLooper(), new MediaSourceHandlerCallback()); clock.createHandler(mediaSourceThread.getLooper(), new MediaSourceHandlerCallback());
trackGroupsFuture = SettableFuture.create(); trackGroupsFuture = SettableFuture.create();
} }

View File

@ -44,6 +44,9 @@ public interface HandlerWrapper {
/** @see Handler#sendEmptyMessage(int) */ /** @see Handler#sendEmptyMessage(int) */
boolean sendEmptyMessage(int what); boolean sendEmptyMessage(int what);
/** @see Handler#sendEmptyMessageDelayed(int, long) */
boolean sendEmptyMessageDelayed(int what, int delayMs);
/** @see Handler#sendEmptyMessageAtTime(int, long) */ /** @see Handler#sendEmptyMessageAtTime(int, long) */
boolean sendEmptyMessageAtTime(int what, long uptimeMs); boolean sendEmptyMessageAtTime(int what, long uptimeMs);

View File

@ -58,6 +58,11 @@ import androidx.annotation.Nullable;
return handler.sendEmptyMessage(what); return handler.sendEmptyMessage(what);
} }
@Override
public boolean sendEmptyMessageDelayed(int what, int delayMs) {
return handler.sendEmptyMessageDelayed(what, delayMs);
}
@Override @Override
public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {
return handler.sendEmptyMessageAtTime(what, uptimeMs); return handler.sendEmptyMessageAtTime(what, uptimeMs);

View File

@ -22,17 +22,18 @@ import static org.junit.Assert.assertThrows;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata; import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData; import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -41,11 +42,15 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class MetadataRetrieverTest { public class MetadataRetrieverTest {
private static final long TEST_TIMEOUT_SEC = 10;
private Context context; private Context context;
private AutoAdvancingFakeClock clock;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext(); context = ApplicationProvider.getApplicationContext();
clock = new AutoAdvancingFakeClock();
} }
@Test @Test
@ -53,8 +58,9 @@ public class MetadataRetrieverTest {
MediaItem mediaItem = MediaItem mediaItem =
MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4")); MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample.mp4"));
ListenableFuture<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem); ListenableFuture<TrackGroupArray> trackGroupsFuture =
TrackGroupArray trackGroups = waitAndGetTrackGroups(trackGroupsFuture); retrieveMetadata(context, mediaItem, clock);
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2); assertThat(trackGroups.length).isEqualTo(2);
// Video group. // Video group.
@ -72,10 +78,12 @@ public class MetadataRetrieverTest {
MediaItem mediaItem2 = MediaItem mediaItem2 =
MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp3/bear-id3.mp3")); MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp3/bear-id3.mp3"));
ListenableFuture<TrackGroupArray> trackGroupsFuture1 = retrieveMetadata(context, mediaItem1); ListenableFuture<TrackGroupArray> trackGroupsFuture1 =
ListenableFuture<TrackGroupArray> trackGroupsFuture2 = retrieveMetadata(context, mediaItem2); retrieveMetadata(context, mediaItem1, clock);
TrackGroupArray trackGroups1 = waitAndGetTrackGroups(trackGroupsFuture1); ListenableFuture<TrackGroupArray> trackGroupsFuture2 =
TrackGroupArray trackGroups2 = waitAndGetTrackGroups(trackGroupsFuture2); retrieveMetadata(context, mediaItem2, clock);
TrackGroupArray trackGroups1 = trackGroupsFuture1.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
TrackGroupArray trackGroups2 = trackGroupsFuture2.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
// First track group. // First track group.
assertThat(trackGroups1.length).isEqualTo(2); assertThat(trackGroups1.length).isEqualTo(2);
@ -104,8 +112,9 @@ public class MetadataRetrieverTest {
/* videoStartPosition= */ 28_869, /* videoStartPosition= */ 28_869,
/* videoSize= */ 28_803); /* videoSize= */ 28_803);
ListenableFuture<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem); ListenableFuture<TrackGroupArray> trackGroupsFuture =
TrackGroupArray trackGroups = waitAndGetTrackGroups(trackGroupsFuture); retrieveMetadata(context, mediaItem, clock);
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1); assertThat(trackGroups.length).isEqualTo(1);
assertThat(trackGroups.get(0).length).isEqualTo(1); assertThat(trackGroups.get(0).length).isEqualTo(1);
@ -119,8 +128,9 @@ public class MetadataRetrieverTest {
MediaItem mediaItem = MediaItem mediaItem =
MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample_still_photo.heic")); MediaItem.fromUri(Uri.parse("asset://android_asset/media/mp4/sample_still_photo.heic"));
ListenableFuture<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem); ListenableFuture<TrackGroupArray> trackGroupsFuture =
TrackGroupArray trackGroups = waitAndGetTrackGroups(trackGroupsFuture); retrieveMetadata(context, mediaItem, clock);
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(1); assertThat(trackGroups.length).isEqualTo(1);
assertThat(trackGroups.get(0).length).isEqualTo(1); assertThat(trackGroups.get(0).length).isEqualTo(1);
@ -140,15 +150,14 @@ public class MetadataRetrieverTest {
/* startTimeMs= */ 1255, /* endTimeMs= */ 1970, /* speedDivisor= */ 8)); /* startTimeMs= */ 1255, /* endTimeMs= */ 1970, /* speedDivisor= */ 8));
SlowMotionData expectedSlowMotionData = new SlowMotionData(segments); SlowMotionData expectedSlowMotionData = new SlowMotionData(segments);
ListenableFuture<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem); ListenableFuture<TrackGroupArray> trackGroupsFuture =
TrackGroupArray trackGroups = waitAndGetTrackGroups(trackGroupsFuture); retrieveMetadata(context, mediaItem, clock);
TrackGroupArray trackGroups = trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS);
assertThat(trackGroups.length).isEqualTo(2); // Video and audio assertThat(trackGroups.length).isEqualTo(2); // Video and audio
// Audio // Audio
assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(1); assertThat(trackGroups.get(0).getFormat(0).metadata.length()).isEqualTo(1);
assertThat(trackGroups.get(0).getFormat(0).metadata.get(0)).isEqualTo(expectedSlowMotionData); assertThat(trackGroups.get(0).getFormat(0).metadata.get(0)).isEqualTo(expectedSlowMotionData);
// Video // Video
assertThat(trackGroups.get(1).getFormat(0).metadata.length()) assertThat(trackGroups.get(1).getFormat(0).metadata.length())
.isEqualTo(3); // 2 Mdta entries and 1 slow motion entry. .isEqualTo(3); // 2 Mdta entries and 1 slow motion entry.
@ -160,21 +169,10 @@ public class MetadataRetrieverTest {
MediaItem mediaItem = MediaItem mediaItem =
MediaItem.fromUri(Uri.parse("asset://android_asset/media/does_not_exist")); MediaItem.fromUri(Uri.parse("asset://android_asset/media/does_not_exist"));
ListenableFuture<TrackGroupArray> trackGroupsFuture = retrieveMetadata(context, mediaItem); ListenableFuture<TrackGroupArray> trackGroupsFuture =
retrieveMetadata(context, mediaItem, clock);
assertThrows(ExecutionException.class, () -> waitAndGetTrackGroups(trackGroupsFuture)); assertThrows(
} ExecutionException.class, () -> trackGroupsFuture.get(TEST_TIMEOUT_SEC, TimeUnit.SECONDS));
private static TrackGroupArray waitAndGetTrackGroups(
ListenableFuture<TrackGroupArray> trackGroupsFuture)
throws InterruptedException, ExecutionException {
while (!trackGroupsFuture.isDone()) {
// TODO: update once [Internal: b/168084145] is implemented.
// Advance SystemClock so that messages that are sent with a delay to the MetadataRetriever
// looper are received.
SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100);
Thread.sleep(/* millis= */ 100);
}
return trackGroupsFuture.get();
} }
} }

View File

@ -15,20 +15,22 @@
*/ */
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.HandlerWrapper;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* {@link FakeClock} extension which automatically advances time whenever an empty message is * {@link FakeClock} extension which automatically advances time whenever an empty message is
* enqueued at a future time. * enqueued at a future time.
* *
* <p>The clock time is advanced to the time of enqueued empty messages. Only the first Handler * <p>The clock time is advanced to the time of enqueued empty messages. The first Handler sending
* sending messages at a future time will be allowed to advance time to ensure there is only one * messages at a future time will be allowed to advance time to ensure there is only one primary
* primary time source. This should usually be the Handler of the internal playback loop. * time source at a time. This should usually be the Handler of the internal playback loop. You can
* {@link #resetHandler() reset the handler} so that the next Handler that sends messages at a
* future time becomes the primary time source.
*/ */
public final class AutoAdvancingFakeClock extends FakeClock { public final class AutoAdvancingFakeClock extends FakeClock {
private @MonotonicNonNull HandlerWrapper autoAdvancingHandler; @Nullable private HandlerWrapper autoAdvancingHandler;
/** Creates the auto-advancing clock with an initial time of 0. */ /** Creates the auto-advancing clock with an initial time of 0. */
public AutoAdvancingFakeClock() { public AutoAdvancingFakeClock() {
@ -57,4 +59,9 @@ public final class AutoAdvancingFakeClock extends FakeClock {
} }
return result; return result;
} }
/** Resets the internal handler, so that this clock can later be used with another handler. */
public void resetHandler() {
autoAdvancingHandler = null;
}
} }

View File

@ -223,6 +223,11 @@ public class FakeClock implements Clock {
return handler.sendEmptyMessage(what); return handler.sendEmptyMessage(what);
} }
@Override
public boolean sendEmptyMessageDelayed(int what, int delayMs) {
return addHandlerMessageAtTime(this, what, uptimeMillis() + delayMs);
}
@Override @Override
public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { public boolean sendEmptyMessageAtTime(int what, long uptimeMs) {
return addHandlerMessageAtTime(this, what, uptimeMs); return addHandlerMessageAtTime(this, what, uptimeMs);