Schedule exoplayer work task to when renderers can make progress
Currently ExoPlayer schedules its main work loop on a 10 ms interval. When renderers cannot make any more progress(ex: hardware buffers are fully written with audio data), ExoPlayer should be able to schedule the next work task further than 10Ms out. Through `experimentalSetDynamicSchedulingEnabled`, ExoPlayer will dynamically schedule its work tasks based on when renderers are expected to be able to make progress. PiperOrigin-RevId: 638676318
This commit is contained in:
parent
8c8bf1334e
commit
9e0f533a11
@ -41,6 +41,15 @@
|
||||
* Rename `onTimelineRefreshed` to `onSourcePrepared` and `onPrepared` to
|
||||
`onTracksSelected` in `PreloadMediaSource.PreloadControl`. Also rename
|
||||
the IntDefs in `DefaultPreloadManager.Stage` accordingly.
|
||||
* Add experimental support for dynamic scheduling to better align work
|
||||
with CPU wake-cycles and delay waking up to when renderers can progress.
|
||||
You can enable this using `experimentalSetDynamicSchedulingEnabled` when
|
||||
setting up your ExoPlayer instance.
|
||||
* Add `Renderer.getDurationToProgressMs`. A `Renderer` can implement this
|
||||
method to return to ExoPlayer the duration that playback must advance in
|
||||
order for the renderer to progress. If `ExoPlayer` is set with
|
||||
`experimentalSetDynamicSchedulingEnabled` then `ExoPlayer` will call
|
||||
this method when calculating the time to schedule its work task.
|
||||
* Transformer:
|
||||
* Work around a decoder bug where the number of audio channels was capped
|
||||
at stereo when handling PCM input.
|
||||
|
@ -504,6 +504,7 @@ public interface ExoPlayer extends Player {
|
||||
/* package */ boolean buildCalled;
|
||||
/* package */ boolean suppressPlaybackOnUnsuitableOutput;
|
||||
/* package */ String playerName;
|
||||
/* package */ boolean dynamicSchedulingEnabled;
|
||||
|
||||
/**
|
||||
* Creates a builder.
|
||||
@ -547,6 +548,7 @@ public interface ExoPlayer extends Player {
|
||||
* <li>{@code usePlatformDiagnostics}: {@code true}
|
||||
* <li>{@link Clock}: {@link Clock#DEFAULT}
|
||||
* <li>{@code playbackLooper}: {@code null} (create new thread)
|
||||
* <li>{@code dynamicSchedulingEnabled}: {@code false}
|
||||
* </ul>
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
@ -726,6 +728,24 @@ public interface ExoPlayer extends Player {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether dynamic scheduling is enabled.
|
||||
*
|
||||
* <p>If enabled, ExoPlayer's playback loop will run as rarely as possible by scheduling work
|
||||
* for when {@link Renderer} progress can be made.
|
||||
*
|
||||
* <p>This method is experimental, and will be renamed or removed in a future release.
|
||||
*
|
||||
* @param dynamicSchedulingEnabled Whether to enable dynamic scheduling.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder experimentalSetDynamicSchedulingEnabled(boolean dynamicSchedulingEnabled) {
|
||||
checkState(!buildCalled);
|
||||
this.dynamicSchedulingEnabled = dynamicSchedulingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the player should suppress playback that is attempted on an unsuitable output.
|
||||
* An example of an unsuitable audio output is the built-in speaker on a Wear OS device (unless
|
||||
|
@ -372,6 +372,7 @@ import java.util.concurrent.TimeoutException;
|
||||
builder.livePlaybackSpeedControl,
|
||||
builder.releaseTimeoutMs,
|
||||
pauseAtEndOfMediaItems,
|
||||
builder.dynamicSchedulingEnabled,
|
||||
applicationLooper,
|
||||
clock,
|
||||
playbackInfoUpdateListener,
|
||||
|
@ -174,8 +174,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private static final int MSG_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS = 27;
|
||||
private static final int MSG_SET_PRELOAD_CONFIGURATION = 28;
|
||||
|
||||
private static final int ACTIVE_INTERVAL_MS = 10;
|
||||
private static final int IDLE_INTERVAL_MS = 1000;
|
||||
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
|
||||
Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US);
|
||||
private static final long READY_MAXIMUM_INTERVAL_MS = 1000;
|
||||
|
||||
/**
|
||||
* Duration for which the player needs to appear stuck before the playback is failed on the
|
||||
@ -214,6 +215,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private final LivePlaybackSpeedControl livePlaybackSpeedControl;
|
||||
private final long releaseTimeoutMs;
|
||||
private final PlayerId playerId;
|
||||
private final boolean dynamicSchedulingEnabled;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private SeekParameters seekParameters;
|
||||
@ -234,6 +236,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
private int enabledRendererCount;
|
||||
@Nullable private SeekPosition pendingInitialSeekPosition;
|
||||
private long rendererPositionUs;
|
||||
private long rendererPositionElapsedRealtimeUs;
|
||||
private int nextPendingMessageIndexHint;
|
||||
private boolean deliverPendingMessageAtStartPositionRequired;
|
||||
@Nullable private ExoPlaybackException pendingRecoverableRendererError;
|
||||
@ -255,6 +258,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
LivePlaybackSpeedControl livePlaybackSpeedControl,
|
||||
long releaseTimeoutMs,
|
||||
boolean pauseAtEndOfWindow,
|
||||
boolean dynamicSchedulingEnabled,
|
||||
Looper applicationLooper,
|
||||
Clock clock,
|
||||
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
||||
@ -274,6 +278,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
this.releaseTimeoutMs = releaseTimeoutMs;
|
||||
this.setForegroundModeTimeoutMs = releaseTimeoutMs;
|
||||
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
|
||||
this.dynamicSchedulingEnabled = dynamicSchedulingEnabled;
|
||||
this.clock = clock;
|
||||
this.playerId = playerId;
|
||||
this.preloadConfiguration = preloadConfiguration;
|
||||
@ -1111,7 +1116,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
|
||||
if (playingPeriodHolder == null) {
|
||||
// We're still waiting until the playing period is available.
|
||||
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
||||
scheduleNextWork(operationStartTimeMs);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1122,7 +1127,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
boolean renderersEnded = true;
|
||||
boolean renderersAllowPlayback = true;
|
||||
if (playingPeriodHolder.prepared) {
|
||||
long rendererPositionElapsedRealtimeUs = msToUs(clock.elapsedRealtime());
|
||||
rendererPositionElapsedRealtimeUs = msToUs(clock.elapsedRealtime());
|
||||
playingPeriodHolder.mediaPeriod.discardBuffer(
|
||||
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
@ -1230,12 +1235,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
if (sleepingForOffload || playbackInfo.playbackState == Player.STATE_ENDED) {
|
||||
// No need to schedule next work.
|
||||
} else if (isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING) {
|
||||
// We are actively playing or waiting for data to be ready. Schedule next work quickly.
|
||||
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
||||
} else if (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0) {
|
||||
// We are ready, but not playing. Schedule next work less often to handle non-urgent updates.
|
||||
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
|
||||
} else if ((isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING)
|
||||
|| (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0)) {
|
||||
// Schedule next work as either we are actively playing, buffering, or we
|
||||
// are ready but not playing.
|
||||
scheduleNextWork(operationStartTimeMs);
|
||||
}
|
||||
|
||||
TraceUtil.endSection();
|
||||
@ -1266,8 +1270,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
return window.isLive() && window.isDynamic && window.windowStartTimeMs != C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
|
||||
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
|
||||
private void scheduleNextWork(long thisOperationStartTimeMs) {
|
||||
long wakeUpTimeIntervalMs =
|
||||
playbackInfo.playbackState == Player.STATE_READY
|
||||
&& (dynamicSchedulingEnabled || !shouldPlayWhenReady())
|
||||
? READY_MAXIMUM_INTERVAL_MS
|
||||
: BUFFERING_MAXIMUM_INTERVAL_MS;
|
||||
if (dynamicSchedulingEnabled && shouldPlayWhenReady()) {
|
||||
for (Renderer renderer : renderers) {
|
||||
if (isRendererEnabled(renderer)) {
|
||||
wakeUpTimeIntervalMs =
|
||||
min(
|
||||
wakeUpTimeIntervalMs,
|
||||
Util.usToMs(
|
||||
renderer.getDurationToProgressUs(
|
||||
rendererPositionUs, rendererPositionElapsedRealtimeUs)));
|
||||
}
|
||||
}
|
||||
}
|
||||
handler.sendEmptyMessageAtTime(
|
||||
MSG_DO_SOME_WORK, thisOperationStartTimeMs + wakeUpTimeIntervalMs);
|
||||
}
|
||||
|
||||
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
||||
@ -2769,7 +2791,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@Override
|
||||
public void onWakeup() {
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
if (dynamicSchedulingEnabled || offloadSchedulingEnabled) {
|
||||
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -62,6 +62,14 @@ import java.util.List;
|
||||
@UnstableApi
|
||||
public interface Renderer extends PlayerMessage.Target {
|
||||
|
||||
/**
|
||||
* Default minimum duration that the playback clock must advance before {@link #render} can make
|
||||
* progress.
|
||||
*
|
||||
* @see #getDurationToProgressUs
|
||||
*/
|
||||
long DEFAULT_DURATION_TO_PROGRESS_US = 10_000L;
|
||||
|
||||
/**
|
||||
* Some renderers can signal when {@link #render(long, long)} should be called.
|
||||
*
|
||||
@ -434,6 +442,23 @@ public interface Renderer extends PlayerMessage.Target {
|
||||
*/
|
||||
long getReadingPositionUs();
|
||||
|
||||
/**
|
||||
* Returns minimum amount of playback clock time that must pass in order for the {@link #render}
|
||||
* call to make progress.
|
||||
*
|
||||
* <p>The default return time is {@link #DEFAULT_DURATION_TO_PROGRESS_US}.
|
||||
*
|
||||
* @param positionUs The current render position in microseconds, measured at the start of the
|
||||
* current iteration of the rendering loop.
|
||||
* @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
|
||||
* measured at the start of the current iteration of the rendering loop.
|
||||
* @return Minimum amount of playback clock time that must pass before renderer is able to make
|
||||
* progress.
|
||||
*/
|
||||
default long getDurationToProgressUs(long positionUs, long elapsedRealtimeUs) {
|
||||
return DEFAULT_DURATION_TO_PROGRESS_US;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
|
||||
* before it is next disabled or reset.
|
||||
|
@ -10528,6 +10528,162 @@ public class ExoPlayerTest {
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void play_withDynamicSchedulingEnabled_usesRendererDurationSchedulingInterval()
|
||||
throws Exception {
|
||||
AtomicInteger renderCounter = new AtomicInteger();
|
||||
FakeDurationToProgressRenderer fakeRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_AUDIO, /* durationToProgressUs= */ 150_000L, renderCounter);
|
||||
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setClock(clock)
|
||||
.setDynamicSchedulingEnabled(true)
|
||||
.setRenderers(fakeRenderer)
|
||||
.build();
|
||||
player.setMediaSource(
|
||||
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
player.prepare();
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 500);
|
||||
renderCounter.set(0);
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 800);
|
||||
|
||||
assertThat(renderCounter.get()).isEqualTo(2);
|
||||
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void play_withDynamicSchedulingDisabled_usesDefaultSchedulingInterval() throws Exception {
|
||||
AtomicInteger renderCounter = new AtomicInteger();
|
||||
FakeDurationToProgressRenderer fakeRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_AUDIO, /* durationToProgressUs= */ 150_000L, renderCounter);
|
||||
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context).setClock(clock).setRenderers(fakeRenderer).build();
|
||||
player.setMediaSource(
|
||||
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
player.prepare();
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 500);
|
||||
renderCounter.set(0);
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 800);
|
||||
|
||||
assertThat(renderCounter.get()).isEqualTo(30);
|
||||
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void
|
||||
play_withDynamicSchedulingEnabledAndMultipleRenderers_usesMinimumDurationSchedulingInterval()
|
||||
throws Exception {
|
||||
AtomicInteger renderCounter = new AtomicInteger();
|
||||
FakeDurationToProgressRenderer fakeAudioRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_AUDIO, /* durationToProgressUs= */ 150_000L, renderCounter);
|
||||
FakeDurationToProgressRenderer fakeVideoRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_VIDEO, /* durationToProgressUs= */ 30_000L, /* renderCounter= */ null);
|
||||
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setClock(clock)
|
||||
.setDynamicSchedulingEnabled(true)
|
||||
.setRenderers(fakeAudioRenderer, fakeVideoRenderer)
|
||||
.build();
|
||||
player.setMediaSource(
|
||||
new FakeMediaSource(
|
||||
new FakeTimeline(),
|
||||
ExoPlayerTestRunner.AUDIO_FORMAT,
|
||||
ExoPlayerTestRunner.VIDEO_FORMAT));
|
||||
player.prepare();
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 500);
|
||||
renderCounter.set(0);
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 800);
|
||||
|
||||
assertThat(renderCounter.get()).isEqualTo(10);
|
||||
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepareOnly_withDynamicSchedulingEnabled_usesDefaultIdleSchedulingInterval()
|
||||
throws Exception {
|
||||
AtomicInteger renderCounter = new AtomicInteger();
|
||||
FakeDurationToProgressRenderer fakeRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_AUDIO, /* durationToProgressUs= */ 150_000L, renderCounter);
|
||||
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setClock(clock)
|
||||
.setDynamicSchedulingEnabled(true)
|
||||
.setRenderers(fakeRenderer)
|
||||
.build();
|
||||
player.setMediaSource(
|
||||
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT));
|
||||
player.prepare();
|
||||
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 1000);
|
||||
renderCounter.set(0);
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 3000);
|
||||
|
||||
assertThat(renderCounter.get()).isEqualTo(2);
|
||||
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void play_withDynamicSchedulingEnabledAndInBufferingState_usesBufferingSchedulingInterval()
|
||||
throws Exception {
|
||||
AtomicInteger renderCounter = new AtomicInteger();
|
||||
AtomicBoolean allowStreamRead = new AtomicBoolean();
|
||||
FakeDurationToProgressRenderer fakeRenderer =
|
||||
new FakeDurationToProgressRenderer(
|
||||
C.TRACK_TYPE_AUDIO, /* durationToProgressUs= */ 150_000L, renderCounter) {
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
// Always return false so player will stay in a buffering state.
|
||||
return false;
|
||||
}
|
||||
;
|
||||
};
|
||||
FakeClock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||
ExoPlayer player =
|
||||
new TestExoPlayerBuilder(context)
|
||||
.setClock(clock)
|
||||
.setDynamicSchedulingEnabled(true)
|
||||
.setRenderers(fakeRenderer)
|
||||
.build();
|
||||
// Prevent reading any samples to keep player in a buffering state.
|
||||
FakeDelayedMediaSource fakeDelayedMediaSource =
|
||||
new FakeDelayedMediaSource(
|
||||
new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT, allowStreamRead);
|
||||
player.setMediaSource(fakeDelayedMediaSource);
|
||||
player.prepare();
|
||||
player.play();
|
||||
runUntilPlaybackState(player, Player.STATE_BUFFERING);
|
||||
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 200);
|
||||
renderCounter.set(0);
|
||||
run(player).untilBackgroundThreadCondition(() -> clock.currentTimeMillis() >= 500);
|
||||
|
||||
assertThat(renderCounter.get()).isEqualTo(30);
|
||||
|
||||
player.release();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enablingOffload_withAudioOnly_playerSleeps() throws Exception {
|
||||
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO);
|
||||
@ -14435,71 +14591,7 @@ public class ExoPlayerTest {
|
||||
Timeline timeline = new FakeTimeline();
|
||||
AtomicBoolean allowStreamRead = new AtomicBoolean();
|
||||
MediaSource delayedStreamSource =
|
||||
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
long startPositionUs =
|
||||
-timeline
|
||||
.getPeriodByUid(id.periodUid, new Timeline.Period())
|
||||
.getPositionInWindowUs();
|
||||
// Add enough samples to the source so that the decoder can't decode everything at once.
|
||||
return new FakeMediaPeriod(
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
(format, mediaPerioid) ->
|
||||
ImmutableList.of(
|
||||
oneByteSample(startPositionUs, C.BUFFER_FLAG_KEY_FRAME),
|
||||
oneByteSample(startPositionUs + 10_000),
|
||||
oneByteSample(startPositionUs + 20_000),
|
||||
oneByteSample(startPositionUs + 30_000),
|
||||
oneByteSample(startPositionUs + 40_000),
|
||||
oneByteSample(startPositionUs + 50_000),
|
||||
oneByteSample(startPositionUs + 60_000),
|
||||
oneByteSample(startPositionUs + 70_000),
|
||||
oneByteSample(startPositionUs + 80_000),
|
||||
oneByteSample(startPositionUs + 90_000),
|
||||
oneByteSample(startPositionUs + 100_000),
|
||||
END_OF_STREAM_ITEM),
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
/* deferOnPrepared= */ false) {
|
||||
@Override
|
||||
protected FakeSampleStream createSampleStream(
|
||||
Allocator allocator,
|
||||
@Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
Format initialFormat,
|
||||
List<FakeSampleStreamItem> fakeSampleStreamItems) {
|
||||
return new FakeSampleStream(
|
||||
allocator,
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
initialFormat,
|
||||
fakeSampleStreamItems) {
|
||||
@Override
|
||||
public int readData(
|
||||
FormatHolder formatHolder,
|
||||
DecoderInputBuffer buffer,
|
||||
@ReadFlags int readFlags) {
|
||||
return allowStreamRead.get()
|
||||
? super.readData(formatHolder, buffer, readFlags)
|
||||
: C.RESULT_NOTHING_READ;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
new FakeDelayedMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT, allowStreamRead);
|
||||
player.addMediaSource(delayedStreamSource);
|
||||
Player.Listener listener = mock(Player.Listener.class);
|
||||
player.addListener(listener);
|
||||
@ -15219,6 +15311,36 @@ public class ExoPlayerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link FakeRenderer} that supports dynamic scheduling through its capability to return how much
|
||||
* playback time must advance before additional renderer progress can be made.
|
||||
*/
|
||||
private static class FakeDurationToProgressRenderer extends FakeRenderer {
|
||||
private final long durationToProgressUs;
|
||||
@Nullable private final AtomicInteger renderCounter;
|
||||
|
||||
public FakeDurationToProgressRenderer(
|
||||
int trackType, long durationToProgressUs, @Nullable AtomicInteger renderCounter) {
|
||||
super(trackType);
|
||||
|
||||
this.durationToProgressUs = durationToProgressUs;
|
||||
this.renderCounter = renderCounter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationToProgressUs(long positionUs, long elapsedRealtimeUs) {
|
||||
return durationToProgressUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
super.render(positionUs, elapsedRealtimeUs);
|
||||
if (renderCounter != null) {
|
||||
renderCounter.getAndIncrement();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CountingMessageTarget implements PlayerMessage.Target {
|
||||
|
||||
public int messageCount;
|
||||
@ -15283,6 +15405,69 @@ public class ExoPlayerTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link FakeMediaSource} that allows prevention of reading any samples off the sample queue. */
|
||||
private static final class FakeDelayedMediaSource extends FakeMediaSource {
|
||||
private final AtomicBoolean allowStreamRead;
|
||||
|
||||
public FakeDelayedMediaSource(Timeline timeline, Format format, AtomicBoolean allowStreamRead) {
|
||||
super(timeline, format);
|
||||
this.allowStreamRead = allowStreamRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MediaPeriod createMediaPeriod(
|
||||
MediaPeriodId id,
|
||||
TrackGroupArray trackGroupArray,
|
||||
Allocator allocator,
|
||||
MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
@Nullable TransferListener transferListener) {
|
||||
long startPositionUs =
|
||||
-getTimeline()
|
||||
.getPeriodByUid(id.periodUid, new Timeline.Period())
|
||||
.getPositionInWindowUs();
|
||||
// Add enough samples to the source so that the decoder can't decode everything at once.
|
||||
return new FakeMediaPeriod(
|
||||
trackGroupArray,
|
||||
allocator,
|
||||
(format, mediaPeriodId) ->
|
||||
ImmutableList.of(
|
||||
oneByteSample(startPositionUs, C.BUFFER_FLAG_KEY_FRAME),
|
||||
oneByteSample(startPositionUs + 10_000),
|
||||
END_OF_STREAM_ITEM),
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
/* deferOnPrepared= */ false) {
|
||||
@Override
|
||||
protected FakeSampleStream createSampleStream(
|
||||
Allocator allocator,
|
||||
@Nullable MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher,
|
||||
DrmSessionManager drmSessionManager,
|
||||
DrmSessionEventListener.EventDispatcher drmEventDispatcher,
|
||||
Format initialFormat,
|
||||
List<FakeSampleStreamItem> fakeSampleStreamItems) {
|
||||
return new FakeSampleStream(
|
||||
allocator,
|
||||
mediaSourceEventDispatcher,
|
||||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
initialFormat,
|
||||
fakeSampleStreamItems) {
|
||||
@Override
|
||||
public int readData(
|
||||
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
|
||||
return allowStreamRead.get()
|
||||
? super.readData(formatHolder, buffer, readFlags)
|
||||
: C.RESULT_NOTHING_READ;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakeLoaderCallback implements Loader.Callback<Loader.Loadable> {
|
||||
@Override
|
||||
public void onLoadCompleted(
|
||||
|
@ -57,6 +57,7 @@ public class TestExoPlayerBuilder {
|
||||
private boolean deviceVolumeControlEnabled;
|
||||
private boolean suppressPlaybackWhenUnsuitableOutput;
|
||||
@Nullable private ExoPlayer.PreloadConfiguration preloadConfiguration;
|
||||
private boolean dynamicSchedulingEnabled;
|
||||
|
||||
public TestExoPlayerBuilder(Context context) {
|
||||
this.context = context;
|
||||
@ -329,6 +330,19 @@ public class TestExoPlayerBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ExoPlayer.Builder#experimentalSetDynamicSchedulingEnabled(boolean)} for details.
|
||||
*
|
||||
* @param dynamicSchedulingEnabled Whether the player should enable dynamically schedule its
|
||||
* playback loop for when {@link Renderer} progress can be made.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public TestExoPlayerBuilder setDynamicSchedulingEnabled(boolean dynamicSchedulingEnabled) {
|
||||
this.dynamicSchedulingEnabled = dynamicSchedulingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds an {@link ExoPlayer} using the provided values or their defaults. */
|
||||
public ExoPlayer build() {
|
||||
Assertions.checkNotNull(
|
||||
@ -366,7 +380,8 @@ public class TestExoPlayerBuilder {
|
||||
.setSeekBackIncrementMs(seekBackIncrementMs)
|
||||
.setSeekForwardIncrementMs(seekForwardIncrementMs)
|
||||
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
|
||||
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput);
|
||||
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput)
|
||||
.experimentalSetDynamicSchedulingEnabled(dynamicSchedulingEnabled);
|
||||
if (mediaSourceFactory != null) {
|
||||
builder.setMediaSourceFactory(mediaSourceFactory);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user