mirror of
https://github.com/samsonjs/media.git
synced 2026-03-26 09:35:47 +00:00
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
This commit is contained in:
parent
438bcada38
commit
91c2f891a0
6 changed files with 42 additions and 17 deletions
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue