diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 216e226036..aba925f1d3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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. diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index 5098a451a8..d79a4fdff3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -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 { *
  • {@code usePlatformDiagnostics}: {@code true} *
  • {@link Clock}: {@link Clock#DEFAULT} *
  • {@code playbackLooper}: {@code null} (create new thread) + *
  • {@code dynamicSchedulingEnabled}: {@code false} * * * @param context A {@link Context}. @@ -726,6 +728,24 @@ public interface ExoPlayer extends Player { return this; } + /** + * Sets whether dynamic scheduling is enabled. + * + *

    If enabled, ExoPlayer's playback loop will run as rarely as possible by scheduling work + * for when {@link Renderer} progress can be made. + * + *

    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 diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 0851e32225..f5e9fc929f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -372,6 +372,7 @@ import java.util.concurrent.TimeoutException; builder.livePlaybackSpeedControl, builder.releaseTimeoutMs, pauseAtEndOfMediaItems, + builder.dynamicSchedulingEnabled, applicationLooper, clock, playbackInfoUpdateListener, diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java index 872fbeec82..3878ba2036 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java @@ -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); + } } }); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java index 215d043d04..18a0c1abe1 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/Renderer.java @@ -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. + * + *

    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. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java index fc2db401d2..8ce6082f24 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java @@ -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 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 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 { @Override public void onLoadCompleted( diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java index 612da04df8..73bdb26ea4 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/TestExoPlayerBuilder.java @@ -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); }