mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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
|
* Rename `onTimelineRefreshed` to `onSourcePrepared` and `onPrepared` to
|
||||||
`onTracksSelected` in `PreloadMediaSource.PreloadControl`. Also rename
|
`onTracksSelected` in `PreloadMediaSource.PreloadControl`. Also rename
|
||||||
the IntDefs in `DefaultPreloadManager.Stage` accordingly.
|
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:
|
* Transformer:
|
||||||
* Work around a decoder bug where the number of audio channels was capped
|
* Work around a decoder bug where the number of audio channels was capped
|
||||||
at stereo when handling PCM input.
|
at stereo when handling PCM input.
|
||||||
|
@ -504,6 +504,7 @@ public interface ExoPlayer extends Player {
|
|||||||
/* package */ boolean buildCalled;
|
/* package */ boolean buildCalled;
|
||||||
/* package */ boolean suppressPlaybackOnUnsuitableOutput;
|
/* package */ boolean suppressPlaybackOnUnsuitableOutput;
|
||||||
/* package */ String playerName;
|
/* package */ String playerName;
|
||||||
|
/* package */ boolean dynamicSchedulingEnabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a builder.
|
* Creates a builder.
|
||||||
@ -547,6 +548,7 @@ public interface ExoPlayer extends Player {
|
|||||||
* <li>{@code usePlatformDiagnostics}: {@code true}
|
* <li>{@code usePlatformDiagnostics}: {@code true}
|
||||||
* <li>{@link Clock}: {@link Clock#DEFAULT}
|
* <li>{@link Clock}: {@link Clock#DEFAULT}
|
||||||
* <li>{@code playbackLooper}: {@code null} (create new thread)
|
* <li>{@code playbackLooper}: {@code null} (create new thread)
|
||||||
|
* <li>{@code dynamicSchedulingEnabled}: {@code false}
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param context A {@link Context}.
|
* @param context A {@link Context}.
|
||||||
@ -726,6 +728,24 @@ public interface ExoPlayer extends Player {
|
|||||||
return this;
|
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.
|
* 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
|
* 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.livePlaybackSpeedControl,
|
||||||
builder.releaseTimeoutMs,
|
builder.releaseTimeoutMs,
|
||||||
pauseAtEndOfMediaItems,
|
pauseAtEndOfMediaItems,
|
||||||
|
builder.dynamicSchedulingEnabled,
|
||||||
applicationLooper,
|
applicationLooper,
|
||||||
clock,
|
clock,
|
||||||
playbackInfoUpdateListener,
|
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_UPDATE_MEDIA_SOURCES_WITH_MEDIA_ITEMS = 27;
|
||||||
private static final int MSG_SET_PRELOAD_CONFIGURATION = 28;
|
private static final int MSG_SET_PRELOAD_CONFIGURATION = 28;
|
||||||
|
|
||||||
private static final int ACTIVE_INTERVAL_MS = 10;
|
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
|
||||||
private static final int IDLE_INTERVAL_MS = 1000;
|
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
|
* 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 LivePlaybackSpeedControl livePlaybackSpeedControl;
|
||||||
private final long releaseTimeoutMs;
|
private final long releaseTimeoutMs;
|
||||||
private final PlayerId playerId;
|
private final PlayerId playerId;
|
||||||
|
private final boolean dynamicSchedulingEnabled;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private SeekParameters seekParameters;
|
private SeekParameters seekParameters;
|
||||||
@ -234,6 +236,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
private int enabledRendererCount;
|
private int enabledRendererCount;
|
||||||
@Nullable private SeekPosition pendingInitialSeekPosition;
|
@Nullable private SeekPosition pendingInitialSeekPosition;
|
||||||
private long rendererPositionUs;
|
private long rendererPositionUs;
|
||||||
|
private long rendererPositionElapsedRealtimeUs;
|
||||||
private int nextPendingMessageIndexHint;
|
private int nextPendingMessageIndexHint;
|
||||||
private boolean deliverPendingMessageAtStartPositionRequired;
|
private boolean deliverPendingMessageAtStartPositionRequired;
|
||||||
@Nullable private ExoPlaybackException pendingRecoverableRendererError;
|
@Nullable private ExoPlaybackException pendingRecoverableRendererError;
|
||||||
@ -255,6 +258,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
LivePlaybackSpeedControl livePlaybackSpeedControl,
|
LivePlaybackSpeedControl livePlaybackSpeedControl,
|
||||||
long releaseTimeoutMs,
|
long releaseTimeoutMs,
|
||||||
boolean pauseAtEndOfWindow,
|
boolean pauseAtEndOfWindow,
|
||||||
|
boolean dynamicSchedulingEnabled,
|
||||||
Looper applicationLooper,
|
Looper applicationLooper,
|
||||||
Clock clock,
|
Clock clock,
|
||||||
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
PlaybackInfoUpdateListener playbackInfoUpdateListener,
|
||||||
@ -274,6 +278,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
this.releaseTimeoutMs = releaseTimeoutMs;
|
this.releaseTimeoutMs = releaseTimeoutMs;
|
||||||
this.setForegroundModeTimeoutMs = releaseTimeoutMs;
|
this.setForegroundModeTimeoutMs = releaseTimeoutMs;
|
||||||
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
|
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
|
||||||
|
this.dynamicSchedulingEnabled = dynamicSchedulingEnabled;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.playerId = playerId;
|
this.playerId = playerId;
|
||||||
this.preloadConfiguration = preloadConfiguration;
|
this.preloadConfiguration = preloadConfiguration;
|
||||||
@ -1111,7 +1116,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
@Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
|
@Nullable MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
|
||||||
if (playingPeriodHolder == null) {
|
if (playingPeriodHolder == null) {
|
||||||
// We're still waiting until the playing period is available.
|
// We're still waiting until the playing period is available.
|
||||||
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
scheduleNextWork(operationStartTimeMs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1122,7 +1127,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
boolean renderersEnded = true;
|
boolean renderersEnded = true;
|
||||||
boolean renderersAllowPlayback = true;
|
boolean renderersAllowPlayback = true;
|
||||||
if (playingPeriodHolder.prepared) {
|
if (playingPeriodHolder.prepared) {
|
||||||
long rendererPositionElapsedRealtimeUs = msToUs(clock.elapsedRealtime());
|
rendererPositionElapsedRealtimeUs = msToUs(clock.elapsedRealtime());
|
||||||
playingPeriodHolder.mediaPeriod.discardBuffer(
|
playingPeriodHolder.mediaPeriod.discardBuffer(
|
||||||
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
|
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
|
||||||
for (int i = 0; i < renderers.length; i++) {
|
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) {
|
if (sleepingForOffload || playbackInfo.playbackState == Player.STATE_ENDED) {
|
||||||
// No need to schedule next work.
|
// No need to schedule next work.
|
||||||
} else if (isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING) {
|
} else if ((isPlaying || playbackInfo.playbackState == Player.STATE_BUFFERING)
|
||||||
// We are actively playing or waiting for data to be ready. Schedule next work quickly.
|
|| (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0)) {
|
||||||
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
// Schedule next work as either we are actively playing, buffering, or we
|
||||||
} else if (playbackInfo.playbackState == Player.STATE_READY && enabledRendererCount != 0) {
|
// are ready but not playing.
|
||||||
// We are ready, but not playing. Schedule next work less often to handle non-urgent updates.
|
scheduleNextWork(operationStartTimeMs);
|
||||||
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
@ -1266,8 +1270,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
return window.isLive() && window.isDynamic && window.windowStartTimeMs != C.TIME_UNSET;
|
return window.isLive() && window.isDynamic && window.windowStartTimeMs != C.TIME_UNSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
|
private void scheduleNextWork(long thisOperationStartTimeMs) {
|
||||||
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
|
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 {
|
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
||||||
@ -2769,7 +2791,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onWakeup() {
|
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
|
@UnstableApi
|
||||||
public interface Renderer extends PlayerMessage.Target {
|
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.
|
* Some renderers can signal when {@link #render(long, long)} should be called.
|
||||||
*
|
*
|
||||||
@ -434,6 +442,23 @@ public interface Renderer extends PlayerMessage.Target {
|
|||||||
*/
|
*/
|
||||||
long getReadingPositionUs();
|
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
|
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
|
||||||
* before it is next disabled or reset.
|
* before it is next disabled or reset.
|
||||||
|
@ -10528,6 +10528,162 @@ public class ExoPlayerTest {
|
|||||||
player.release();
|
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
|
@Test
|
||||||
public void enablingOffload_withAudioOnly_playerSleeps() throws Exception {
|
public void enablingOffload_withAudioOnly_playerSleeps() throws Exception {
|
||||||
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO);
|
FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO);
|
||||||
@ -14435,71 +14591,7 @@ public class ExoPlayerTest {
|
|||||||
Timeline timeline = new FakeTimeline();
|
Timeline timeline = new FakeTimeline();
|
||||||
AtomicBoolean allowStreamRead = new AtomicBoolean();
|
AtomicBoolean allowStreamRead = new AtomicBoolean();
|
||||||
MediaSource delayedStreamSource =
|
MediaSource delayedStreamSource =
|
||||||
new FakeMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT) {
|
new FakeDelayedMediaSource(timeline, ExoPlayerTestRunner.VIDEO_FORMAT, allowStreamRead);
|
||||||
@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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addMediaSource(delayedStreamSource);
|
player.addMediaSource(delayedStreamSource);
|
||||||
Player.Listener listener = mock(Player.Listener.class);
|
Player.Listener listener = mock(Player.Listener.class);
|
||||||
player.addListener(listener);
|
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 {
|
private static final class CountingMessageTarget implements PlayerMessage.Target {
|
||||||
|
|
||||||
public int messageCount;
|
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> {
|
private static final class FakeLoaderCallback implements Loader.Callback<Loader.Loadable> {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadCompleted(
|
public void onLoadCompleted(
|
||||||
|
@ -57,6 +57,7 @@ public class TestExoPlayerBuilder {
|
|||||||
private boolean deviceVolumeControlEnabled;
|
private boolean deviceVolumeControlEnabled;
|
||||||
private boolean suppressPlaybackWhenUnsuitableOutput;
|
private boolean suppressPlaybackWhenUnsuitableOutput;
|
||||||
@Nullable private ExoPlayer.PreloadConfiguration preloadConfiguration;
|
@Nullable private ExoPlayer.PreloadConfiguration preloadConfiguration;
|
||||||
|
private boolean dynamicSchedulingEnabled;
|
||||||
|
|
||||||
public TestExoPlayerBuilder(Context context) {
|
public TestExoPlayerBuilder(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
@ -329,6 +330,19 @@ public class TestExoPlayerBuilder {
|
|||||||
return this;
|
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. */
|
/** Builds an {@link ExoPlayer} using the provided values or their defaults. */
|
||||||
public ExoPlayer build() {
|
public ExoPlayer build() {
|
||||||
Assertions.checkNotNull(
|
Assertions.checkNotNull(
|
||||||
@ -366,7 +380,8 @@ public class TestExoPlayerBuilder {
|
|||||||
.setSeekBackIncrementMs(seekBackIncrementMs)
|
.setSeekBackIncrementMs(seekBackIncrementMs)
|
||||||
.setSeekForwardIncrementMs(seekForwardIncrementMs)
|
.setSeekForwardIncrementMs(seekForwardIncrementMs)
|
||||||
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
|
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
|
||||||
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput);
|
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput)
|
||||||
|
.experimentalSetDynamicSchedulingEnabled(dynamicSchedulingEnabled);
|
||||||
if (mediaSourceFactory != null) {
|
if (mediaSourceFactory != null) {
|
||||||
builder.setMediaSourceFactory(mediaSourceFactory);
|
builder.setMediaSourceFactory(mediaSourceFactory);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user