diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java
index 64887aecc3..34ca459482 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java
@@ -836,7 +836,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
public void handleMessage(@MessageType int messageType, @Nullable Object message)
throws ExoPlaybackException {
if (messageType == MSG_SET_WAKEUP_LISTENER) {
- this.wakeupListener = (WakeupListener) message;
+ wakeupListener = (WakeupListener) message;
+ onWakeupListenerSet(checkNotNull(wakeupListener));
} else {
super.handleMessage(messageType, message);
}
@@ -1542,6 +1543,17 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// Do nothing.
}
+ /**
+ * Called when a {@link WakeupListener} is set.
+ *
+ *
The default implementation is a no-op.
+ *
+ * @param wakeupListener The {@link WakeupListener}.
+ */
+ protected void onWakeupListenerSet(WakeupListener wakeupListener) {
+ // Do nothing.
+ }
+
/**
* Called when a new {@link Format} is read from the upstream {@link MediaPeriod}.
*
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java
index 7d532c1092..64ab313d1e 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DefaultVideoSink.java
@@ -26,6 +26,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.TimestampIterator;
import androidx.media3.exoplayer.ExoPlaybackException;
+import androidx.media3.exoplayer.Renderer;
import java.util.List;
import java.util.concurrent.Executor;
@@ -38,6 +39,7 @@ import java.util.concurrent.Executor;
*
* - Applying video effects
*
- Inputting bitmaps
+ *
- Setting WakeupListener
*
*
* The {@linkplain #getInputSurface() input} and {@linkplain #setOutputSurfaceInfo(Surface, Size)
@@ -220,6 +222,16 @@ import java.util.concurrent.Executor;
}
}
+ /**
+ * {@inheritDoc}
+ *
+ *
This method will always throw an {@link UnsupportedOperationException}.
+ */
+ @Override
+ public void setWakeupListener(Renderer.WakeupListener wakeupListener) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void join(boolean renderNextFrameImmediately) {
videoFrameReleaseControl.join(renderNextFrameImmediately);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
index 53d0f42468..6a604553f6 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java
@@ -757,6 +757,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
videoSink.setVideoEffects(videoEffects);
}
videoSink.onRendererEnabled(mayRenderStartOfStream);
+ @Nullable WakeupListener wakeupListener = getWakeupListener();
+ if (wakeupListener != null) {
+ videoSink.setWakeupListener(wakeupListener);
+ }
} else {
videoFrameReleaseControl.setClock(getClock());
videoFrameReleaseControl.onEnabled(mayRenderStartOfStream);
@@ -1250,6 +1254,13 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
eventDispatcher.videoCodecError(codecError);
}
+ @Override
+ protected void onWakeupListenerSet(WakeupListener wakeupListener) {
+ if (videoSink != null) {
+ videoSink.setWakeupListener(wakeupListener);
+ }
+ }
+
@Override
@Nullable
protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder)
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java
index f3510c3836..55c7834893 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaybackVideoGraphWrapper.java
@@ -51,6 +51,7 @@ import androidx.media3.common.util.TimestampIterator;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlaybackException;
+import androidx.media3.exoplayer.Renderer;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
@@ -252,6 +253,7 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
@Nullable private Pair currentSurfaceAndSize;
private int pendingFlushCount;
private @State int state;
+ @Nullable private Renderer.WakeupListener wakeupListener;
/**
* Converts the buffer timestamp (the player position, with renderer offset) to the composition
@@ -363,6 +365,10 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
// Ignore available frames while flushing
return;
}
+ if (wakeupListener != null) {
+ // Wake up the player when not playing to render the frame more promptly.
+ wakeupListener.onWakeup();
+ }
// The frame presentation time is relative to the start of the Composition and without the
// renderer offset
long bufferPresentationTimeUs = framePresentationTimeUs - bufferTimestampAdjustmentUs;
@@ -829,6 +835,11 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
PlaybackVideoGraphWrapper.this.render(positionUs, elapsedRealtimeUs);
}
+ @Override
+ public void setWakeupListener(Renderer.WakeupListener wakeupListener) {
+ PlaybackVideoGraphWrapper.this.wakeupListener = wakeupListener;
+ }
+
@Override
public void join(boolean renderNextFrameImmediately) {
defaultVideoSink.join(renderNextFrameImmediately);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java
index 54e237d9b8..4069c38b23 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoSink.java
@@ -294,6 +294,9 @@ public interface VideoSink {
*/
void render(long positionUs, long elapsedRealtimeUs) throws VideoSinkException;
+ /** Sets a {@link Renderer.WakeupListener} on the {@code VideoSink}. */
+ void setWakeupListener(Renderer.WakeupListener wakeupListener);
+
/**
* Joins the video sink to a new stream.
*
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java
index 678317816c..7df14902b2 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/BufferingVideoSink.java
@@ -23,6 +23,7 @@ import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.TimestampIterator;
+import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.video.PlaceholderSurface;
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
import androidx.media3.exoplayer.video.VideoSink;
@@ -229,6 +230,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
+ @Override
+ public void setWakeupListener(Renderer.WakeupListener wakeupListener) {
+ executeOrDelay(videoSink -> videoSink.setWakeupListener(wakeupListener));
+ }
+
@Override
public void join(boolean renderNextFrameImmediately) {
executeOrDelay(videoSink -> videoSink.join(renderNextFrameImmediately));
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java
index 126cf8f99c..626a11056a 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/CompositionPlayer.java
@@ -700,7 +700,10 @@ public final class CompositionPlayer extends SimpleBasePlayer
.setPlaybackLooper(playbackThread.getLooper())
.setRenderersFactory(sequenceRenderersFactory)
.setHandleAudioBecomingNoisy(true)
- .setClock(clock);
+ .setClock(clock)
+ // Use dynamic scheduling to show the first video/image frame more promptly when the
+ // player is paused (which is common in editing applications).
+ .experimentalSetDynamicSchedulingEnabled(true);
boolean disableVideoPlayback = false;
for (int j = 0; j < editedMediaItemSequence.editedMediaItems.size(); j++) {
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java
index a3ccd8d3d0..34bc035f79 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java
+++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SequenceRenderersFactory.java
@@ -476,6 +476,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return videoSink.handleInputBitmap(outputImage, checkStateNotNull(timestampIterator));
}
+ @Override
+ public void handleMessage(@MessageType int messageType, @Nullable Object message)
+ throws ExoPlaybackException {
+ switch (messageType) {
+ case MSG_SET_WAKEUP_LISTENER:
+ videoSink.setWakeupListener((WakeupListener) checkNotNull(message));
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ }
+ }
+
private ConstantRateTimestampIterator createTimestampIterator(long positionUs) {
long streamOffsetUs = getStreamOffsetUs();
long imageBaseTimestampUs = streamOffsetUs + offsetToCompositionTimeUs;