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:
michaelkatz 2024-05-30 09:16:45 -07:00 committed by Copybara-Service
parent 8c8bf1334e
commit 9e0f533a11
7 changed files with 358 additions and 79 deletions

View File

@ -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.

View File

@ -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

View File

@ -372,6 +372,7 @@ import java.util.concurrent.TimeoutException;
builder.livePlaybackSpeedControl,
builder.releaseTimeoutMs,
pauseAtEndOfMediaItems,
builder.dynamicSchedulingEnabled,
applicationLooper,
clock,
playbackInfoUpdateListener,

View File

@ -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);
}
}
});

View File

@ -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.

View File

@ -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(

View File

@ -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);
}