Fix composition player repeat mode

Some checks in SingleInputVideoGraph were causing CompositionPlayer to
throw for a single media item sequence when repeat mode was enabled. The
reason was that, in this case, no new input stream is registered to the
VideoFrameProcessor.

PiperOrigin-RevId: 715409509
This commit is contained in:
kimvde 2025-01-14 09:17:08 -08:00 committed by Copybara-Service
parent 2361624222
commit fbf9be2f00
2 changed files with 63 additions and 28 deletions

View File

@ -25,8 +25,6 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.ColorInfo;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.SurfaceInfo;
import androidx.media3.common.VideoCompositorSettings;
import androidx.media3.common.VideoFrameProcessingException;
@ -34,7 +32,6 @@ import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoGraph;
import androidx.media3.common.util.UnstableApi;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
import java.util.concurrent.Executor;
/** A {@link VideoGraph} that handles one input stream. */
@ -111,17 +108,6 @@ public abstract class SingleInputVideoGraph implements VideoGraph {
/* listenerExecutor= */ MoreExecutors.directExecutor(),
new VideoFrameProcessor.Listener() {
private long lastProcessedFramePresentationTimeUs;
private boolean isEnded;
@Override
public void onInputStreamRegistered(
@VideoFrameProcessor.InputType int inputType,
Format format,
List<Effect> effects) {
// An input stream could be registered after VideoFrameProcessor ends, following
// a flush() for example.
isEnded = false;
}
@Override
public void onOutputSizeChanged(int width, int height) {
@ -135,12 +121,6 @@ public abstract class SingleInputVideoGraph implements VideoGraph {
@Override
public void onOutputFrameAvailableForRendering(long presentationTimeUs) {
if (isEnded) {
onError(
new VideoFrameProcessingException(
"onOutputFrameAvailableForRendering() received after onEnded()"));
return;
}
// Frames are rendered automatically.
if (presentationTimeUs == 0) {
hasProducedFrameWithTimestampZero = true;
@ -157,11 +137,6 @@ public abstract class SingleInputVideoGraph implements VideoGraph {
@Override
public void onEnded() {
if (isEnded) {
onError(new VideoFrameProcessingException("onEnded() received multiple times"));
return;
}
isEnded = true;
listenerExecutor.execute(
() -> listener.onEnded(lastProcessedFramePresentationTimeUs));
}

View File

@ -16,19 +16,28 @@
package androidx.media3.transformer;
import static androidx.media3.common.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
import static androidx.media3.common.Player.REPEAT_MODE_ALL;
import static androidx.media3.common.Player.REPEAT_MODE_OFF;
import static androidx.media3.common.util.Util.isRunningOnEmulator;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET;
import static androidx.media3.transformer.AndroidTestUtil.PNG_ASSET;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context;
import androidx.media3.common.Effect;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.util.NullableType;
import androidx.media3.effect.GlEffect;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.After;
import org.junit.Test;
@ -241,8 +250,9 @@ public class CompositionPlaybackTest {
}
@Test
public void playback_sequenceOfThreeVideosWithRemovingFirstAndLastAudio_succeeds()
throws Exception {
public void
playback_sequenceOfThreeVideosWithRemovingFirstAndLastAudio_effectsReceiveCorrectTimestamps()
throws Exception {
InputTimestampRecordingShaderProgram inputTimestampRecordingShaderProgram =
new InputTimestampRecordingShaderProgram();
EditedMediaItem videoEditedMediaItem =
@ -291,7 +301,9 @@ public class CompositionPlaybackTest {
}
@Test
public void playback_sequenceOfThreeVideosWithRemovingMiddleAudio_succeeds() throws Exception {
public void
playback_sequenceOfThreeVideosWithRemovingMiddleAudio_effectsReceiveCorrectTimestamps()
throws Exception {
InputTimestampRecordingShaderProgram inputTimestampRecordingShaderProgram =
new InputTimestampRecordingShaderProgram();
EditedMediaItem videoEditedMediaItem =
@ -450,4 +462,52 @@ public class CompositionPlaybackTest {
assertThat(inputTimestampRecordingShaderProgram.getInputTimestampsUs())
.isEqualTo(expectedTimestampsUs);
}
@Test
public void playback_withRepeatModeSet_succeeds() throws Exception {
EditedMediaItem editedMediaItem =
new EditedMediaItem.Builder(VIDEO_MEDIA_ITEM).setDurationUs(VIDEO_DURATION_US).build();
Composition composition =
new Composition.Builder(new EditedMediaItemSequence.Builder(editedMediaItem).build())
.build();
CountDownLatch repetitionEndedLatch = new CountDownLatch(2);
AtomicReference<@NullableType PlaybackException> playbackException = new AtomicReference<>();
getInstrumentation()
.runOnMainSync(
() -> {
player = new CompositionPlayer.Builder(context).build();
player.addListener(playerTestListener);
player.addListener(
new Player.Listener() {
@Override
public void onPositionDiscontinuity(
Player.PositionInfo oldPosition,
Player.PositionInfo newPosition,
int reason) {
if (reason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
repetitionEndedLatch.countDown();
}
}
@Override
public void onPlayerError(PlaybackException error) {
playbackException.set(error);
while (repetitionEndedLatch.getCount() > 0) {
repetitionEndedLatch.countDown();
}
}
});
player.setComposition(composition);
player.setRepeatMode(REPEAT_MODE_ALL);
player.prepare();
player.play();
});
boolean latchTimedOut = !repetitionEndedLatch.await(TEST_TIMEOUT_MS, MILLISECONDS);
assertThat(playbackException.get()).isNull();
assertThat(latchTimedOut).isFalse();
getInstrumentation().runOnMainSync(() -> player.setRepeatMode(REPEAT_MODE_OFF));
playerTestListener.waitUntilPlayerEnded();
}
}