Add Configration for Frame Extraction for specific SeekParameters

Expose ExoPlayer seek parameters via FrameExtractor API

PiperOrigin-RevId: 696449874
This commit is contained in:
dancho 2024-11-14 02:44:50 -08:00 committed by Copybara-Service
parent 301ef207f2
commit 16a15b94ca
2 changed files with 107 additions and 11 deletions

View File

@ -16,6 +16,7 @@
package androidx.media3.transformer;
import static androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND;
import static androidx.media3.exoplayer.SeekParameters.CLOSEST_SYNC;
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
@ -86,7 +87,11 @@ public class FrameExtractorTest {
@Test
public void extractFrame_oneFrame_returnsNearest() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -103,7 +108,11 @@ public class FrameExtractorTest {
@Test
public void extractFrame_pastDuration_returnsLastFrame() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -121,7 +130,11 @@ public class FrameExtractorTest {
@Test
public void extractFrame_repeatedPositionMs_returnsTheSameFrame() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L);
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
@ -147,7 +160,11 @@ public class FrameExtractorTest {
@Test
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -162,10 +179,39 @@ public class FrameExtractorTest {
assertThat(frame8.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_031);
}
@Test
public void extractFrame_closestSyncRandomAccess_returnsCorrectFrames() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder()
.setSeekParameters(CLOSEST_SYNC)
.build(),
MediaItem.fromUri(FILE_PATH));
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
ListenableFuture<Frame> frame7 = frameExtractor.getFrame(/* positionMs= */ 7_000);
ListenableFuture<Frame> frame2 = frameExtractor.getFrame(/* positionMs= */ 2_000);
ListenableFuture<Frame> frame8 = frameExtractor.getFrame(/* positionMs= */ 8_000);
// The input video has sync points at 0s, 8.331s, and 9.198s. Verify with:
// ffprobe IN -select_streams v -show_entries frame=pict_type,pts_time -of csv -skip_frame nokey
assertThat(frame5.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_331);
assertThat(frame3.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(0);
assertThat(frame7.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_331);
assertThat(frame2.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(0);
assertThat(frame8.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_331);
}
@Test
public void extractFrame_invalidInput_reportsErrorViaFuture() {
String filePath = "asset:///nonexistent";
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(filePath));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(filePath));
ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0);
@ -178,7 +224,11 @@ public class FrameExtractorTest {
@Test
public void extractFrame_oneFrame_completesViaCallback() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>();
AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>();
ConditionVariable frameReady = new ConditionVariable();
@ -207,8 +257,12 @@ public class FrameExtractorTest {
}
@Test
public void frameExtractor_releaseOnPlayerLooper_returns() throws Exception {
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
public void frameExtractor_releaseOnPlayerLooper_returns() {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH));
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
instrumentation.runOnMainSync(frameExtractor::release);

View File

@ -44,12 +44,14 @@ import androidx.media3.effect.GlShaderProgram;
import androidx.media3.effect.MatrixTransformation;
import androidx.media3.effect.PassthroughShaderProgram;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.initialization.qual.Initialized;
@ -64,6 +66,46 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
/* package */ final class ExperimentalFrameExtractor implements AnalyticsListener {
/** Configuration for the frame extractor. */
// TODO: b/350498258 - Add configuration for decoder selection.
public static final class Configuration {
/** A builder for {@link Configuration} instances. */
public static final class Builder {
private SeekParameters seekParameters;
/** Creates a new instance with default values. */
public Builder() {
seekParameters = SeekParameters.DEFAULT;
}
/**
* Sets the parameters that control how seek operations are performed. Defaults to {@link
* SeekParameters#DEFAULT}.
*
* @param seekParameters The {@link SeekParameters}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setSeekParameters(SeekParameters seekParameters) {
this.seekParameters = seekParameters;
return this;
}
/** Builds a new {@link Configuration} instance. */
public Configuration build() {
return new Configuration(seekParameters);
}
}
/** The {@link SeekParameters}. */
public final SeekParameters seekParameters;
private Configuration(SeekParameters seekParameters) {
this.seekParameters = seekParameters;
}
}
/** Stores an extracted and decoded video frame. */
public static final class Frame {
@ -109,10 +151,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* @param mediaItem The {@link MediaItem} from which frames are extracted.
*/
// TODO: b/350498258 - Support changing the MediaItem.
// TODO: b/350498258 - Add configuration options such as SeekParameters.
// TODO: b/350498258 - Support video effects.
public ExperimentalFrameExtractor(Context context, MediaItem mediaItem) {
player = new ExoPlayer.Builder(context).build();
public ExperimentalFrameExtractor(
Context context, Configuration configuration, MediaItem mediaItem) {
player = new ExoPlayer.Builder(context).setSeekParameters(configuration.seekParameters).build();
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
lastRequestedFrameFuture = SettableFuture.create();
// TODO: b/350498258 - Extracting the first frame is a workaround for ExoPlayer.setVideoEffects