mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add Configration for Frame Extraction for specific SeekParameters
Expose ExoPlayer seek parameters via FrameExtractor API PiperOrigin-RevId: 696449874
This commit is contained in:
parent
301ef207f2
commit
16a15b94ca
@ -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);
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user