Make ExoPlayer.setVideoEffects() timestamp start from 0

This is consistent with `Transformer` and `CompositionPlayer`

Issue: androidx/media#1098
PiperOrigin-RevId: 646446824
This commit is contained in:
claincly 2024-06-25 05:56:37 -07:00 committed by Copybara-Service
parent 867410fece
commit 73bf852405
4 changed files with 89 additions and 13 deletions

View File

@ -12,6 +12,9 @@
* Fix potential `IndexOutOfBoundsException` caused by extractors reporting * Fix potential `IndexOutOfBoundsException` caused by extractors reporting
additional tracks after the initial preparation step additional tracks after the initial preparation step
([#1476](https://github.com/androidx/media/issues/1476)). ([#1476](https://github.com/androidx/media/issues/1476)).
* `Effects` in `ExoPlayer.setVideoEffect()` will receive the timestamps
with the renderer offset removed
([#1098](https://github.com/androidx/media/issues/1098)).
* Transformer: * Transformer:
* Track Selection: * Track Selection:
* Extractors: * Extractors:

View File

@ -75,6 +75,7 @@ import androidx.media3.exoplayer.mediacodec.MediaCodecRenderer;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil.DecoderQueryException;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.video.VideoRendererEventListener.EventDispatcher; import androidx.media3.exoplayer.video.VideoRendererEventListener.EventDispatcher;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -177,6 +178,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
private int tunnelingAudioSessionId; private int tunnelingAudioSessionId;
/* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener; /* package */ @Nullable OnFrameRenderedListenerV23 tunnelingOnFrameRenderedListener;
@Nullable private VideoFrameMetadataListener frameMetadataListener; @Nullable private VideoFrameMetadataListener frameMetadataListener;
private long startPositionUs;
/** /**
* @param context A context. * @param context A context.
@ -414,6 +416,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET; tunnelingAudioSessionId = C.AUDIO_SESSION_ID_UNSET;
reportedVideoSize = null; reportedVideoSize = null;
rendererPriority = C.PRIORITY_PLAYBACK; rendererPriority = C.PRIORITY_PLAYBACK;
startPositionUs = C.TIME_UNSET;
} }
// FrameTimingEvaluator methods // FrameTimingEvaluator methods
@ -714,6 +717,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
} }
} }
@Override
protected void onStreamChanged(
Format[] formats,
long startPositionUs,
long offsetUs,
MediaSource.MediaPeriodId mediaPeriodId)
throws ExoPlaybackException {
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
if (this.startPositionUs == C.TIME_UNSET) {
this.startPositionUs = startPositionUs;
}
}
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
if (videoSink != null) { if (videoSink != null) {
@ -814,6 +830,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
super.onReset(); super.onReset();
} finally { } finally {
hasSetVideoSink = false; hasSetVideoSink = false;
startPositionUs = C.TIME_UNSET;
if (placeholderSurface != null) { if (placeholderSurface != null) {
releasePlaceholderSurface(); releasePlaceholderSurface();
} }
@ -1446,8 +1463,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer
* position) to the frame presentation time, in microseconds. * position) to the frame presentation time, in microseconds.
*/ */
protected long getBufferTimestampAdjustmentUs() { protected long getBufferTimestampAdjustmentUs() {
// TODO - b/333514379: Make effect-enabled effect timestamp start from zero. return -startPositionUs;
return 0;
} }
private boolean maybeReleaseFrame( private boolean maybeReleaseFrame(

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.transformer; package androidx.media3.transformer.mh;
import static androidx.media3.common.util.Util.usToMs; import static androidx.media3.common.util.Util.usToMs;
import static androidx.media3.transformer.AndroidTestUtil.JPG_SINGLE_PIXEL_ASSET; import static androidx.media3.transformer.AndroidTestUtil.JPG_SINGLE_PIXEL_ASSET;
@ -27,6 +27,18 @@ import android.view.SurfaceView;
import androidx.media3.common.Effect; import androidx.media3.common.Effect;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.effect.GlEffect; import androidx.media3.effect.GlEffect;
import androidx.media3.exoplayer.ExoPlayer;
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.ExportTestResult;
import androidx.media3.transformer.InputTimestampRecordingShaderProgram;
import androidx.media3.transformer.PlayerTestListener;
import androidx.media3.transformer.SurfaceTestActivity;
import androidx.media3.transformer.Transformer;
import androidx.media3.transformer.TransformerAndroidTestRunner;
import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.platform.app.InstrumentationRegistry;
@ -68,6 +80,7 @@ public class VideoTimestampConsistencyTest {
private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); private final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
private final Context applicationContext = instrumentation.getContext().getApplicationContext(); private final Context applicationContext = instrumentation.getContext().getApplicationContext();
private ExoPlayer exoplayer;
private CompositionPlayer compositionPlayer; private CompositionPlayer compositionPlayer;
private SurfaceView surfaceView; private SurfaceView surfaceView;
@ -95,7 +108,8 @@ public class VideoTimestampConsistencyTest {
.setFrameRate(30) .setFrameRate(30)
.build(); .build();
compareTimestamps(ImmutableList.of(image), IMAGE_TIMESTAMPS_US_500_MS_30_FPS); compareTimestamps(
ImmutableList.of(image), IMAGE_TIMESTAMPS_US_500_MS_30_FPS, /* containsImage= */ true);
} }
@Test @Test
@ -105,7 +119,8 @@ public class VideoTimestampConsistencyTest {
.setDurationUs(MP4_ASSET.videoDurationUs) .setDurationUs(MP4_ASSET.videoDurationUs)
.build(); .build();
compareTimestamps(ImmutableList.of(video), MP4_ASSET_FRAME_TIMESTAMPS_US); compareTimestamps(
ImmutableList.of(video), MP4_ASSET_FRAME_TIMESTAMPS_US, /* containsImage= */ false);
} }
@Test @Test
@ -138,7 +153,8 @@ public class VideoTimestampConsistencyTest {
timestampUs -> ((MP4_ASSET.videoDurationUs - clippedStartUs) + timestampUs))) timestampUs -> ((MP4_ASSET.videoDurationUs - clippedStartUs) + timestampUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(video1, video2), expectedTimestamps); compareTimestamps(
ImmutableList.of(video1, video2), expectedTimestamps, /* containsImage= */ false);
} }
@Test @Test
@ -162,7 +178,8 @@ public class VideoTimestampConsistencyTest {
timestampUs -> (MP4_ASSET.videoDurationUs + timestampUs))) timestampUs -> (MP4_ASSET.videoDurationUs + timestampUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(video1, video2), expectedTimestamps); compareTimestamps(
ImmutableList.of(video1, video2), expectedTimestamps, /* containsImage= */ false);
} }
@Test @Test
@ -198,7 +215,8 @@ public class VideoTimestampConsistencyTest {
timestampUs -> (timestampUs + imageDurationUs))) timestampUs -> (timestampUs + imageDurationUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(image1, image2), expectedTimestamps); compareTimestamps(
ImmutableList.of(image1, image2), expectedTimestamps, /* containsImage= */ true);
} }
@Test @Test
@ -227,7 +245,8 @@ public class VideoTimestampConsistencyTest {
MP4_ASSET_FRAME_TIMESTAMPS_US, timestampUs -> (timestampUs + imageDurationUs))) MP4_ASSET_FRAME_TIMESTAMPS_US, timestampUs -> (timestampUs + imageDurationUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(image, video), expectedTimestamps); compareTimestamps(
ImmutableList.of(image, video), expectedTimestamps, /* containsImage= */ true);
} }
@Test @Test
@ -257,7 +276,8 @@ public class VideoTimestampConsistencyTest {
timestampUs -> (MP4_ASSET.videoDurationUs + timestampUs))) timestampUs -> (MP4_ASSET.videoDurationUs + timestampUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(video, image), expectedTimestamps); compareTimestamps(
ImmutableList.of(video, image), expectedTimestamps, /* containsImage= */ true);
} }
@Test @Test
@ -295,16 +315,26 @@ public class VideoTimestampConsistencyTest {
timestampUs -> ((MP4_ASSET.videoDurationUs - clippedStartUs) + timestampUs))) timestampUs -> ((MP4_ASSET.videoDurationUs - clippedStartUs) + timestampUs)))
.build(); .build();
compareTimestamps(ImmutableList.of(video, image), expectedTimestamps); compareTimestamps(
ImmutableList.of(video, image), expectedTimestamps, /* containsImage= */ true);
} }
private void compareTimestamps(List<EditedMediaItem> mediaItems, List<Long> expectedTimestamps) private void compareTimestamps(
List<EditedMediaItem> mediaItems, List<Long> expectedTimestamps, boolean containsImage)
throws Exception { throws Exception {
ImmutableList<Long> timestampsFromCompositionPlayer = ImmutableList<Long> timestampsFromCompositionPlayer =
getTimestampsFromCompositionPlayer(mediaItems); getTimestampsFromCompositionPlayer(mediaItems);
ImmutableList<Long> timestampsFromTransformer = getTimestampsFromTransformer(mediaItems); ImmutableList<Long> timestampsFromTransformer = getTimestampsFromTransformer(mediaItems);
assertThat(timestampsFromCompositionPlayer).isEqualTo(timestampsFromTransformer); assertThat(timestampsFromCompositionPlayer).isEqualTo(timestampsFromTransformer);
if (!containsImage) {
// ExoPlayer doesn't support image playback with effects.
ImmutableList<Long> timestampsFromExoPlayer =
getTimestampsFromExoPlayer(
Lists.transform(mediaItems, editedMediaItem -> editedMediaItem.mediaItem));
assertThat(timestampsFromCompositionPlayer).isEqualTo(timestampsFromExoPlayer);
}
assertThat(timestampsFromTransformer).isEqualTo(expectedTimestamps); assertThat(timestampsFromTransformer).isEqualTo(expectedTimestamps);
} }
@ -318,6 +348,7 @@ public class VideoTimestampConsistencyTest {
/* effects= */ ImmutableList.of( /* effects= */ ImmutableList.of(
(GlEffect) (context, useHdr) -> timestampRecordingShaderProgram)); (GlEffect) (context, useHdr) -> timestampRecordingShaderProgram));
@SuppressWarnings("unused")
ExportTestResult result = ExportTestResult result =
new TransformerAndroidTestRunner.Builder( new TransformerAndroidTestRunner.Builder(
applicationContext, new Transformer.Builder(applicationContext).build()) applicationContext, new Transformer.Builder(applicationContext).build())
@ -365,6 +396,32 @@ public class VideoTimestampConsistencyTest {
return timestampRecordingShaderProgram.getInputTimestampsUs(); return timestampRecordingShaderProgram.getInputTimestampsUs();
} }
private ImmutableList<Long> getTimestampsFromExoPlayer(List<MediaItem> mediaItems)
throws Exception {
PlayerTestListener playerListener = new PlayerTestListener(TEST_TIMEOUT_MS);
InputTimestampRecordingShaderProgram timestampRecordingShaderProgram =
new InputTimestampRecordingShaderProgram();
instrumentation.runOnMainSync(
() -> {
exoplayer = new ExoPlayer.Builder(applicationContext).build();
// Set a surface on the player even though there is no UI on this test. We need a surface
// otherwise the player will skip/drop video frames.
exoplayer.setVideoSurfaceView(surfaceView);
exoplayer.addListener(playerListener);
exoplayer.setMediaItems(mediaItems);
exoplayer.setVideoEffects(
ImmutableList.of((GlEffect) (context, useHdr) -> timestampRecordingShaderProgram));
exoplayer.prepare();
exoplayer.play();
});
playerListener.waitUntilPlayerEnded();
instrumentation.runOnMainSync(() -> exoplayer.release());
return timestampRecordingShaderProgram.getInputTimestampsUs();
}
private static ImmutableList<EditedMediaItem> prependVideoEffects( private static ImmutableList<EditedMediaItem> prependVideoEffects(
List<EditedMediaItem> editedMediaItems, List<Effect> effects) { List<EditedMediaItem> editedMediaItems, List<Effect> effects) {
ImmutableList.Builder<EditedMediaItem> prependedItems = new ImmutableList.Builder<>(); ImmutableList.Builder<EditedMediaItem> prependedItems = new ImmutableList.Builder<>();

View File

@ -293,7 +293,7 @@ public final class EditedMediaItem {
} }
/** Returns a {@link Builder} initialized with the values of this instance. */ /** Returns a {@link Builder} initialized with the values of this instance. */
/* package */ Builder buildUpon() { public Builder buildUpon() {
return new Builder(this); return new Builder(this);
} }