[go: nahoru, domu]

Skip to content

Commit

Permalink
Schedule exoplayer work task to when renderers can make progress
Browse files Browse the repository at this point in the history
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
  • Loading branch information
microkatz authored and Copybara-Service committed May 30, 2024
1 parent 8c8bf13 commit 9e0f533
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 79 deletions.
9 changes: 9 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ final class Builder {
/* package */ boolean buildCalled;
/* package */ boolean suppressPlaybackOnUnsuitableOutput;
/* package */ String playerName;
/* package */ boolean dynamicSchedulingEnabled;

/**
* Creates a builder.
Expand Down Expand Up @@ -547,6 +548,7 @@ final class Builder {
* <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}.
Expand Down Expand Up @@ -726,6 +728,24 @@ public Builder experimentalSetForegroundModeTimeoutMs(long timeoutMs) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
builder.livePlaybackSpeedControl,
builder.releaseTimeoutMs,
pauseAtEndOfMediaItems,
builder.dynamicSchedulingEnabled,
applicationLooper,
clock,
playbackInfoUpdateListener,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ public interface PlaybackInfoUpdateListener {
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
Expand Down Expand Up @@ -214,6 +215,7 @@ public interface PlaybackInfoUpdateListener {
private final LivePlaybackSpeedControl livePlaybackSpeedControl;
private final long releaseTimeoutMs;
private final PlayerId playerId;
private final boolean dynamicSchedulingEnabled;

@SuppressWarnings("unused")
private SeekParameters seekParameters;
Expand All @@ -234,6 +236,7 @@ public interface PlaybackInfoUpdateListener {
private int enabledRendererCount;
@Nullable private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs;
private long rendererPositionElapsedRealtimeUs;
private int nextPendingMessageIndexHint;
private boolean deliverPendingMessageAtStartPositionRequired;
@Nullable private ExoPlaybackException pendingRecoverableRendererError;
Expand All @@ -255,6 +258,7 @@ public ExoPlayerImplInternal(
LivePlaybackSpeedControl livePlaybackSpeedControl,
long releaseTimeoutMs,
boolean pauseAtEndOfWindow,
boolean dynamicSchedulingEnabled,
Looper applicationLooper,
Clock clock,
PlaybackInfoUpdateListener playbackInfoUpdateListener,
Expand All @@ -274,6 +278,7 @@ public ExoPlayerImplInternal(
this.releaseTimeoutMs = releaseTimeoutMs;
this.setForegroundModeTimeoutMs = releaseTimeoutMs;
this.pauseAtEndOfWindow = pauseAtEndOfWindow;
this.dynamicSchedulingEnabled = dynamicSchedulingEnabled;
this.clock = clock;
this.playerId = playerId;
this.preloadConfiguration = preloadConfiguration;
Expand Down Expand Up @@ -1111,7 +1116,7 @@ private void doSomeWork() throws ExoPlaybackException, IOException {
@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;
}

Expand All @@ -1122,7 +1127,7 @@ private void doSomeWork() throws ExoPlaybackException, IOException {
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++) {
Expand Down Expand Up @@ -1230,12 +1235,11 @@ && isLoadingPossible()) {

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();
Expand Down Expand Up @@ -1266,8 +1270,26 @@ private boolean shouldUseLivePlaybackSpeedControl(
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 {
Expand Down Expand Up @@ -2769,7 +2791,9 @@ public void onSleep() {

@Override
public void onWakeup() {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
if (dynamicSchedulingEnabled || offloadSchedulingEnabled) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@
@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.
*
Expand Down Expand Up @@ -434,6 +442,23 @@ void replaceStream(
*/
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.
Expand Down
Loading

0 comments on commit 9e0f533

Please sign in to comment.