mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Allow replays with CompositionPlayer
PiperOrigin-RevId: 745536073
This commit is contained in:
parent
71fb3ad5a5
commit
4bfa154acd
@ -366,7 +366,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public VideoSink getSink(int inputIndex) {
|
public VideoSink getSink(int inputIndex) {
|
||||||
checkState(!contains(inputVideoSinks, inputIndex));
|
if (contains(inputVideoSinks, inputIndex)) {
|
||||||
|
return inputVideoSinks.get(inputIndex);
|
||||||
|
}
|
||||||
InputVideoSink inputVideoSink = new InputVideoSink(context, inputIndex);
|
InputVideoSink inputVideoSink = new InputVideoSink(context, inputIndex);
|
||||||
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
if (inputIndex == PRIMARY_SEQUENCE_INDEX) {
|
||||||
addListener(inputVideoSink);
|
addListener(inputVideoSink);
|
||||||
@ -730,7 +732,9 @@ public final class PlaybackVideoGraphWrapper implements VideoSinkProvider, Video
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void redraw() {
|
public void redraw() {
|
||||||
checkState(isInitialized());
|
if (!isInitialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Resignal EOS only for the last item.
|
// Resignal EOS only for the last item.
|
||||||
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
|
boolean needsResignalEndOfCurrentInputStream = signaledEndOfStream;
|
||||||
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs;
|
long replayedPresentationTimeUs = lastOutputBufferPresentationTimeUs;
|
||||||
|
@ -481,6 +481,7 @@ public final class AndroidTestUtil {
|
|||||||
.setFrameRate(30.00f)
|
.setFrameRate(30.00f)
|
||||||
.setCodecs("avc1.42C033")
|
.setCodecs("avc1.42C033")
|
||||||
.build())
|
.build())
|
||||||
|
.setVideoDurationUs(1_000_000L)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public static final AssetInfo MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS =
|
public static final AssetInfo MP4_LONG_ASSET_WITH_INCREASING_TIMESTAMPS =
|
||||||
|
@ -32,9 +32,11 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.VideoFrameProcessor;
|
import androidx.media3.common.VideoFrameProcessor;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.effect.Contrast;
|
import androidx.media3.effect.Contrast;
|
||||||
|
import androidx.media3.effect.GlEffect;
|
||||||
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
import androidx.media3.exoplayer.DefaultRenderersFactory;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.Renderer;
|
import androidx.media3.exoplayer.Renderer;
|
||||||
@ -43,6 +45,12 @@ import androidx.media3.exoplayer.util.EventLogger;
|
|||||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||||
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
import androidx.media3.transformer.AndroidTestUtil.ReplayVideoRenderer;
|
||||||
|
import androidx.media3.transformer.Composition;
|
||||||
|
import androidx.media3.transformer.CompositionPlayer;
|
||||||
|
import androidx.media3.transformer.EditedMediaItem;
|
||||||
|
import androidx.media3.transformer.EditedMediaItemSequence;
|
||||||
|
import androidx.media3.transformer.Effects;
|
||||||
|
import androidx.media3.transformer.InputTimestampRecordingShaderProgram;
|
||||||
import androidx.media3.transformer.PlayerTestListener;
|
import androidx.media3.transformer.PlayerTestListener;
|
||||||
import androidx.media3.transformer.SurfaceTestActivity;
|
import androidx.media3.transformer.SurfaceTestActivity;
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||||
@ -64,8 +72,11 @@ public class ReplayCacheTest {
|
|||||||
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000;
|
private static final long TEST_TIMEOUT_MS = isRunningOnEmulator() ? 20_000 : 10_000;
|
||||||
|
|
||||||
private static final MediaItem VIDEO_MEDIA_ITEM_1 = MediaItem.fromUri(MP4_ASSET.uri);
|
private static final MediaItem VIDEO_MEDIA_ITEM_1 = MediaItem.fromUri(MP4_ASSET.uri);
|
||||||
|
private static final long VIDEO_MEDIA_ITEM_1_DURATION_US = MP4_ASSET.videoDurationUs;
|
||||||
private static final MediaItem VIDEO_MEDIA_ITEM_2 =
|
private static final MediaItem VIDEO_MEDIA_ITEM_2 =
|
||||||
MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri);
|
MediaItem.fromUri(MP4_ASSET_WITH_INCREASING_TIMESTAMPS.uri);
|
||||||
|
private static final long VIDEO_MEDIA_ITEM_2_DURATION_US =
|
||||||
|
MP4_ASSET_WITH_INCREASING_TIMESTAMPS.videoDurationUs;
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
public ActivityScenarioRule<SurfaceTestActivity> rule =
|
public ActivityScenarioRule<SurfaceTestActivity> rule =
|
||||||
@ -74,6 +85,7 @@ public class ReplayCacheTest {
|
|||||||
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
private final Context context = getInstrumentation().getContext().getApplicationContext();
|
||||||
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||||
|
|
||||||
|
private CompositionPlayer compositionPlayer;
|
||||||
private ExoPlayer exoPlayer;
|
private ExoPlayer exoPlayer;
|
||||||
private SurfaceView surfaceView;
|
private SurfaceView surfaceView;
|
||||||
|
|
||||||
@ -88,6 +100,9 @@ public class ReplayCacheTest {
|
|||||||
getInstrumentation()
|
getInstrumentation()
|
||||||
.runOnMainSync(
|
.runOnMainSync(
|
||||||
() -> {
|
() -> {
|
||||||
|
if (compositionPlayer != null) {
|
||||||
|
compositionPlayer.release();
|
||||||
|
}
|
||||||
if (exoPlayer != null) {
|
if (exoPlayer != null) {
|
||||||
exoPlayer.release();
|
exoPlayer.release();
|
||||||
}
|
}
|
||||||
@ -159,4 +174,108 @@ public class ReplayCacheTest {
|
|||||||
// we don't currently replay (the frame at media item transition).
|
// we don't currently replay (the frame at media item transition).
|
||||||
assertThat(playedFrameTimestampsUs).hasSize(119);
|
assertThat(playedFrameTimestampsUs).hasSize(119);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void enableReplay_withCompositionPlayerSingleSequence_playsSequence() throws Exception {
|
||||||
|
assumeTrue(
|
||||||
|
"The MediaCodec decoder's output surface is sometimes dropping frames on emulator despite"
|
||||||
|
+ " using MediaFormat.KEY_ALLOW_FRAME_DROP.",
|
||||||
|
!Util.isRunningOnEmulator());
|
||||||
|
PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS * 1000);
|
||||||
|
InputTimestampRecordingShaderProgram inputTimestampRecordingShaderProgram =
|
||||||
|
new InputTimestampRecordingShaderProgram();
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
compositionPlayer =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.experimentalSetEnableReplayableCache(true)
|
||||||
|
.build();
|
||||||
|
compositionPlayer.setVideoSurfaceView(surfaceView);
|
||||||
|
compositionPlayer.addListener(playerTestListener);
|
||||||
|
compositionPlayer.addListener(
|
||||||
|
new Player.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onRenderedFirstFrame() {
|
||||||
|
compositionPlayer.experimentalRedrawLastFrame();
|
||||||
|
compositionPlayer.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
compositionPlayer.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_1)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_1_DURATION_US)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_2)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_2_DURATION_US)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(
|
||||||
|
new Contrast(0.5f),
|
||||||
|
(GlEffect)
|
||||||
|
(context, useHdr) -> inputTimestampRecordingShaderProgram)))
|
||||||
|
.build());
|
||||||
|
compositionPlayer.prepare();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerEnded();
|
||||||
|
|
||||||
|
int countOfFirstFrameRendered = 0;
|
||||||
|
for (long timestampUs : inputTimestampRecordingShaderProgram.getInputTimestampsUs()) {
|
||||||
|
if (timestampUs == 0) {
|
||||||
|
countOfFirstFrameRendered++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertThat(countOfFirstFrameRendered).isEqualTo(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rapidReplay_withCompositionPlayerSingleSequence_playsSequence() throws Exception {
|
||||||
|
assumeTrue(
|
||||||
|
"The MediaCodec decoder's output surface is sometimes dropping frames on emulator despite"
|
||||||
|
+ " using MediaFormat.KEY_ALLOW_FRAME_DROP.",
|
||||||
|
!Util.isRunningOnEmulator());
|
||||||
|
PlayerTestListener playerTestListener = new PlayerTestListener(TEST_TIMEOUT_MS);
|
||||||
|
Handler mainHandler = new Handler(instrumentation.getTargetContext().getMainLooper());
|
||||||
|
|
||||||
|
instrumentation.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
compositionPlayer =
|
||||||
|
new CompositionPlayer.Builder(context)
|
||||||
|
.experimentalSetEnableReplayableCache(true)
|
||||||
|
.build();
|
||||||
|
compositionPlayer.setVideoSurfaceView(surfaceView);
|
||||||
|
compositionPlayer.addListener(playerTestListener);
|
||||||
|
compositionPlayer.setComposition(
|
||||||
|
new Composition.Builder(
|
||||||
|
new EditedMediaItemSequence.Builder(
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_1)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_1_DURATION_US)
|
||||||
|
.build(),
|
||||||
|
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM_2)
|
||||||
|
.setDurationUs(VIDEO_MEDIA_ITEM_2_DURATION_US)
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
.setEffects(
|
||||||
|
new Effects(
|
||||||
|
/* audioProcessors= */ ImmutableList.of(),
|
||||||
|
/* videoEffects= */ ImmutableList.of(new Contrast(0.5f))))
|
||||||
|
.build());
|
||||||
|
compositionPlayer.prepare();
|
||||||
|
compositionPlayer.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerReady();
|
||||||
|
for (int i = 0; i < 180; i++) {
|
||||||
|
// Replaying every 10 ms.
|
||||||
|
mainHandler.postDelayed(
|
||||||
|
compositionPlayer::experimentalRedrawLastFrame, /* delayMillis= */ 10 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
playerTestListener.waitUntilPlayerEnded();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ import androidx.media3.common.util.Log;
|
|||||||
import androidx.media3.common.util.Size;
|
import androidx.media3.common.util.Size;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
import androidx.media3.common.util.Util;
|
||||||
|
import androidx.media3.effect.DefaultVideoFrameProcessor;
|
||||||
import androidx.media3.effect.SingleInputVideoGraph;
|
import androidx.media3.effect.SingleInputVideoGraph;
|
||||||
import androidx.media3.effect.TimestampAdjustment;
|
import androidx.media3.effect.TimestampAdjustment;
|
||||||
import androidx.media3.exoplayer.ExoPlaybackException;
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
@ -125,6 +126,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
private boolean videoPrewarmingEnabled;
|
private boolean videoPrewarmingEnabled;
|
||||||
private Clock clock;
|
private Clock clock;
|
||||||
private VideoGraph.@MonotonicNonNull Factory videoGraphFactory;
|
private VideoGraph.@MonotonicNonNull Factory videoGraphFactory;
|
||||||
|
private boolean enableReplayableCache;
|
||||||
private boolean built;
|
private boolean built;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,6 +248,21 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether to enable replayable cache.
|
||||||
|
*
|
||||||
|
* <p>By default, the replayable cache is not enabled. Enable it to achieve accurate effect
|
||||||
|
* update, at the cost of using more power and computing resources.
|
||||||
|
*
|
||||||
|
* @param enableReplayableCache Whether replayable cache is enabled.
|
||||||
|
* @return This builder, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
public Builder experimentalSetEnableReplayableCache(boolean enableReplayableCache) {
|
||||||
|
this.enableReplayableCache = enableReplayableCache;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the {@link CompositionPlayer} instance. Must be called at most once.
|
* Builds the {@link CompositionPlayer} instance. Must be called at most once.
|
||||||
*
|
*
|
||||||
@ -262,7 +279,11 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
audioSink = new DefaultAudioSink.Builder(context).build();
|
audioSink = new DefaultAudioSink.Builder(context).build();
|
||||||
}
|
}
|
||||||
if (videoGraphFactory == null) {
|
if (videoGraphFactory == null) {
|
||||||
videoGraphFactory = new SingleInputVideoGraph.Factory();
|
videoGraphFactory =
|
||||||
|
new SingleInputVideoGraph.Factory(
|
||||||
|
new DefaultVideoFrameProcessor.Factory.Builder()
|
||||||
|
.setEnableReplayableCache(enableReplayableCache)
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
CompositionPlayer compositionPlayer = new CompositionPlayer(this);
|
CompositionPlayer compositionPlayer = new CompositionPlayer(this);
|
||||||
built = true;
|
built = true;
|
||||||
@ -311,11 +332,13 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
private final VideoGraph.Factory videoGraphFactory;
|
private final VideoGraph.Factory videoGraphFactory;
|
||||||
private final boolean videoPrewarmingEnabled;
|
private final boolean videoPrewarmingEnabled;
|
||||||
private final HandlerWrapper compositionInternalListenerHandler;
|
private final HandlerWrapper compositionInternalListenerHandler;
|
||||||
|
private final boolean enableReplayableCache;
|
||||||
|
|
||||||
/** Maps from input index to whether the video track is selected in that sequence. */
|
/** Maps from input index to whether the video track is selected in that sequence. */
|
||||||
private final SparseBooleanArray videoTracksSelected;
|
private final SparseBooleanArray videoTracksSelected;
|
||||||
|
|
||||||
private @MonotonicNonNull HandlerThread playbackThread;
|
private @MonotonicNonNull HandlerThread playbackThread;
|
||||||
|
private @MonotonicNonNull HandlerWrapper playbackThreadHandler;
|
||||||
private @MonotonicNonNull CompositionPlayerInternal compositionPlayerInternal;
|
private @MonotonicNonNull CompositionPlayerInternal compositionPlayerInternal;
|
||||||
private @MonotonicNonNull ImmutableList<MediaItemData> playlist;
|
private @MonotonicNonNull ImmutableList<MediaItemData> playlist;
|
||||||
private @MonotonicNonNull Composition composition;
|
private @MonotonicNonNull Composition composition;
|
||||||
@ -352,6 +375,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
videoGraphFactory = checkNotNull(builder.videoGraphFactory);
|
videoGraphFactory = checkNotNull(builder.videoGraphFactory);
|
||||||
videoPrewarmingEnabled = builder.videoPrewarmingEnabled;
|
videoPrewarmingEnabled = builder.videoPrewarmingEnabled;
|
||||||
compositionInternalListenerHandler = clock.createHandler(builder.looper, /* callback= */ null);
|
compositionInternalListenerHandler = clock.createHandler(builder.looper, /* callback= */ null);
|
||||||
|
this.enableReplayableCache = builder.enableReplayableCache;
|
||||||
videoTracksSelected = new SparseBooleanArray();
|
videoTracksSelected = new SparseBooleanArray();
|
||||||
players = new ArrayList<>();
|
players = new ArrayList<>();
|
||||||
compositionDurationUs = C.TIME_UNSET;
|
compositionDurationUs = C.TIME_UNSET;
|
||||||
@ -398,6 +422,21 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
this.composition = composition;
|
this.composition = composition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the effect pipeline to redraw the effects immediately.
|
||||||
|
*
|
||||||
|
* <p>The player must be {@linkplain Builder#experimentalSetEnableReplayableCache built with
|
||||||
|
* replayable cache support}.
|
||||||
|
*/
|
||||||
|
public void experimentalRedrawLastFrame() {
|
||||||
|
checkState(enableReplayableCache);
|
||||||
|
if (playbackThreadHandler == null || playbackVideoGraphWrapper == null) {
|
||||||
|
// Ignore replays before setting a composition.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playbackThreadHandler.post(() -> checkNotNull(playbackVideoGraphWrapper).getSink(0).redraw());
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets the {@link Surface} and {@link Size} to render to. */
|
/** Sets the {@link Surface} and {@link Size} to render to. */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void setVideoSurface(Surface surface, Size videoOutputSize) {
|
public void setVideoSurface(Surface surface, Size videoOutputSize) {
|
||||||
@ -714,6 +753,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
compositionDurationUs = getCompositionDurationUs(composition);
|
compositionDurationUs = getCompositionDurationUs(composition);
|
||||||
playbackThread = new HandlerThread("CompositionPlaybackThread", Process.THREAD_PRIORITY_AUDIO);
|
playbackThread = new HandlerThread("CompositionPlaybackThread", Process.THREAD_PRIORITY_AUDIO);
|
||||||
playbackThread.start();
|
playbackThread.start();
|
||||||
|
playbackThreadHandler = clock.createHandler(playbackThread.getLooper(), /* callback= */ null);
|
||||||
// Create the audio and video composition components now in order to setup the audio and video
|
// Create the audio and video composition components now in order to setup the audio and video
|
||||||
// pipelines. Once this method returns, further access to the audio and video graph wrappers
|
// pipelines. Once this method returns, further access to the audio and video graph wrappers
|
||||||
// must done on the playback thread only, to ensure related components are accessed from one
|
// must done on the playback thread only, to ensure related components are accessed from one
|
||||||
@ -734,6 +774,7 @@ public final class CompositionPlayer extends SimpleBasePlayer
|
|||||||
.setClock(clock)
|
.setClock(clock)
|
||||||
.setRequestOpenGlToneMapping(
|
.setRequestOpenGlToneMapping(
|
||||||
composition.hdrMode == Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
|
composition.hdrMode == Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL)
|
||||||
|
.setEnableReplayableCache(enableReplayableCache)
|
||||||
.build();
|
.build();
|
||||||
playbackVideoGraphWrapper.addListener(this);
|
playbackVideoGraphWrapper.addListener(this);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user