From 91c2f891a0d514178d2dcc987daa65a5177790d5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Feb 2021 10:17:57 +0000 Subject: [PATCH] Ensure BandwidthProfileDataSource loading is fully deterministic We currently block the loading thread until the calculated load time has past and then unblock again by a message sent from the playback thread. However, because the loading thread itself is not using a Looper and runs freely, we don't control when the short calculations on the loader thread that determine how long we have to wait are happening, and we also don't control how long it takes to start and stop this thread. To solve these problems and to make the playback deterministic we can 1. Send a message on the playback thread to block until the loader thread has started. 2. Block the playback thread whenever a loading thread is doing its short calculation of wait times. The playback thread knows when it can continue because loading either enter a new waiting state for a simulated load time or loading is finished completely. 3. Also wait on the playback thread until the loader has shut down. As this is waiting for a message on the playback thread, we can achieve this by sending messages to ourselves at the current time until the loader is shut down. All 3 steps together ensure that the loading thread interaction is compeltely deterministic when simulating bandwidth profiles with the BandwidthProfileDataSource. As we need to notify the source before and after the load started/finished, we also need a small wrapper for the chunk source when running the playback. PiperOrigin-RevId: 355810408 --- .../exoplayer2/util/HandlerWrapper.java | 3 +++ .../exoplayer2/util/SystemHandlerWrapper.java | 5 +++++ .../exoplayer2/testutil/FakeChunkSource.java | 19 +++++++----------- .../exoplayer2/testutil/FakeClock.java | 20 ++++++++++++++++--- .../exoplayer2/testutil/FakeDataSource.java | 6 ++++++ .../exoplayer2/testutil/FakeClockTest.java | 6 ++++-- 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index 637db2fe0d..8247447d93 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -76,4 +76,7 @@ public interface HandlerWrapper { /** See {@link Handler#postDelayed(Runnable, long)}. */ boolean postDelayed(Runnable runnable, long delayMs); + + /** See {@link android.os.Handler#postAtFrontOfQueue(Runnable)}. */ + boolean postAtFrontOfQueue(Runnable runnable); } diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index a595245bc8..ecb5ad64b1 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -110,6 +110,11 @@ import java.util.List; return handler.postDelayed(runnable, delayMs); } + @Override + public boolean postAtFrontOfQueue(Runnable runnable) { + return handler.postAtFrontOfQueue(runnable); + } + private static SystemMessage obtainSystemMessage() { synchronized (messagePool) { return messagePool.isEmpty() diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index cc48c30690..41dd691329 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -35,18 +35,14 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import java.util.List; -/** - * Fake {@link ChunkSource} with adaptive media chunks of a given duration. - */ -public final class FakeChunkSource implements ChunkSource { +/** Fake {@link ChunkSource} with adaptive media chunks of a given duration. */ +public class FakeChunkSource implements ChunkSource { - /** - * Factory for a {@link FakeChunkSource}. - */ - public static final class Factory { + /** Factory for a {@link FakeChunkSource}. */ + public static class Factory { - private final FakeAdaptiveDataSet.Factory dataSetFactory; - private final FakeDataSource.Factory dataSourceFactory; + protected final FakeAdaptiveDataSet.Factory dataSetFactory; + protected final FakeDataSource.Factory dataSourceFactory; public Factory(FakeAdaptiveDataSet.Factory dataSetFactory, FakeDataSource.Factory dataSourceFactory) { @@ -61,13 +57,12 @@ public final class FakeChunkSource implements ChunkSource { FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs); dataSourceFactory.setFakeDataSet(dataSet); - DataSource dataSource = dataSourceFactory.createDataSource(); + FakeDataSource dataSource = dataSourceFactory.createDataSource(); if (transferListener != null) { dataSource.addTransferListener(transferListener); } return new FakeChunkSource(trackSelection, dataSource, dataSet); } - } private final ExoTrackSelection trackSelection; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 7bce01f5ce..59a06fc6f6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Ordering; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -309,7 +310,10 @@ public class FakeClock implements Clock { public int compareTo(HandlerMessage other) { return ComparisonChain.start() .compare(this.timeMs, other.timeMs) - .compare(this.messageId, other.messageId) + .compare( + this.messageId, + other.messageId, + timeMs == Long.MIN_VALUE ? Ordering.natural().reverse() : Ordering.natural()) .result(); } } @@ -412,8 +416,19 @@ public class FakeClock implements Clock { @Override public boolean postDelayed(Runnable runnable, long delayMs) { + postRunnableAtTime(runnable, uptimeMillis() + delayMs); + return true; + } + + @Override + public boolean postAtFrontOfQueue(Runnable runnable) { + postRunnableAtTime(runnable, /* timeMs= */ Long.MIN_VALUE); + return true; + } + + private void postRunnableAtTime(Runnable runnable, long timeMs) { new HandlerMessage( - uptimeMillis() + delayMs, + timeMs, /* handler= */ this, /* what= */ 0, /* arg1= */ 0, @@ -421,7 +436,6 @@ public class FakeClock implements Clock { /* obj= */ null, runnable) .sendToTarget(); - return true; } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 5f858bea99..d517fd4fde 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -210,6 +210,7 @@ public class FakeDataSource extends BaseDataSource { transferEnded(); } fakeData = null; + onClosed(); } /** @@ -227,8 +228,13 @@ public class FakeDataSource extends BaseDataSource { return sourceOpened; } + /** Called when data is being read. */ protected void onDataRead(int bytesRead) throws IOException { // Do nothing. Can be overridden. } + /** Called when the source is closed. */ + protected void onClosed() { + // Do nothing. Can be overridden. + } } 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 index 28e57d8e66..bcd6fa902e 100644 --- a/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java +++ b/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java @@ -143,15 +143,17 @@ public final class FakeClockTest { handler.obtainMessage(/* what= */ 1).sendToTarget(); handler.sendMessageAtFrontOfQueue(handler.obtainMessage(/* what= */ 2)); - handler.obtainMessage(/* what= */ 3).sendToTarget(); + handler.sendMessageAtFrontOfQueue(handler.obtainMessage(/* what= */ 3)); + handler.obtainMessage(/* what= */ 4).sendToTarget(); ShadowLooper.idleMainLooper(); shadowOf(handler.getLooper()).idle(); assertThat(callback.messages) .containsExactly( + new MessageData(/* what= */ 3, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), new MessageData(/* what= */ 2, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), new MessageData(/* what= */ 1, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null), - new MessageData(/* what= */ 3, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)) + new MessageData(/* what= */ 4, /* arg1= */ 0, /* arg2= */ 0, /* obj=*/ null)) .inOrder(); }