Frame extraction support changing MediaItem

Add FrameExtraction.setMediaItem() method
Prefer non-vendor software codecs by default because
vendor codecs sometimes fail when we attempt reuse.

PiperOrigin-RevId: 703486235
This commit is contained in:
dancho 2024-12-06 06:54:42 -08:00 committed by Copybara-Service
parent b816e2f284
commit 1d2ffcb165
4 changed files with 220 additions and 113 deletions

View File

@ -258,7 +258,8 @@ public final class MediaCodecUtil {
/**
* Returns a copy of the provided decoder list sorted such that software decoders are listed
* first.
* first. Break ties by listing non-{@link MediaCodecInfo#vendor} decoders first, due to issues
* with decoder reuse with some software vendor codecs. See b/382447848.
*
* <p>The returned list is not modifiable.
*/
@ -266,7 +267,9 @@ public final class MediaCodecUtil {
public static List<MediaCodecInfo> getDecoderInfosSortedBySoftwareOnly(
List<MediaCodecInfo> decoderInfos) {
decoderInfos = new ArrayList<>(decoderInfos);
sortByScore(decoderInfos, decoderInfo -> decoderInfo.softwareOnly ? 1 : 0);
sortByScore(
decoderInfos,
decoderInfo -> (decoderInfo.softwareOnly ? 2 : 0) + (decoderInfo.vendor ? 0 : 1));
return ImmutableList.copyOf(decoderInfos);
}

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.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED;
import static androidx.media3.exoplayer.SeekParameters.CLOSEST_SYNC;
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
@ -30,6 +31,7 @@ import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Bitmap;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.NullableType;
import androidx.media3.effect.Presentation;
@ -92,10 +94,8 @@ public class FrameExtractorTest {
public void extractFrame_oneFrame_returnsNearest() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -120,8 +120,8 @@ public class FrameExtractorTest {
public void extractFrame_oneFrameWithPresentationEffect_returnsScaledFrame() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of(Presentation.createForHeight(180)));
@ -148,10 +148,8 @@ public class FrameExtractorTest {
public void extractFrame_pastDuration_returnsLastFrame() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -177,10 +175,8 @@ public class FrameExtractorTest {
public void extractFrame_repeatedPositionMs_returnsTheSameFrame() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
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<>();
@ -217,9 +213,8 @@ public class FrameExtractorTest {
context,
new ExperimentalFrameExtractor.Configuration.Builder()
.setSeekParameters(CLOSEST_SYNC)
.build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
.build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 0L, 0L, 0L);
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
@ -253,10 +248,8 @@ public class FrameExtractorTest {
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -284,9 +277,8 @@ public class FrameExtractorTest {
context,
new ExperimentalFrameExtractor.Configuration.Builder()
.setSeekParameters(CLOSEST_SYNC)
.build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
.build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -314,10 +306,8 @@ public class FrameExtractorTest {
String filePath = "asset:///nonexistent";
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(filePath),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(filePath), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0);
@ -328,14 +318,27 @@ public class FrameExtractorTest {
.isEqualTo(ERROR_CODE_IO_FILE_NOT_FOUND);
}
@Test
public void getFrame_withoutMediaItem_throws() {
frameExtractor =
new ExperimentalFrameExtractor(
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
ExecutionException thrown =
assertThrows(ExecutionException.class, () -> frameFuture.get(TIMEOUT_SECONDS, SECONDS));
assertThat(thrown).hasCauseThat().isInstanceOf(PlaybackException.class);
assertThat(((PlaybackException) thrown.getCause()).errorCode)
.isEqualTo(ERROR_CODE_SETUP_REQUIRED);
}
@Test
public void extractFrame_oneFrame_completesViaCallback() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>();
AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>();
ConditionVariable frameReady = new ConditionVariable();
@ -373,10 +376,8 @@ public class FrameExtractorTest {
public void frameExtractor_releaseOnPlayerLooper_returns() {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
instrumentation.runOnMainSync(frameExtractor::release);
@ -387,10 +388,9 @@ public class FrameExtractorTest {
public void extractFrame_oneFrameRotated_returnsFrameInCorrectOrientation() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 0);
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
@ -415,10 +415,8 @@ public class FrameExtractorTest {
public void extractFrame_randomAccessWithCancellation_returnsCorrectFrames() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(FILE_PATH),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
@ -440,4 +438,39 @@ public class FrameExtractorTest {
.renderedOutputBufferCount)
.isEqualTo(4);
}
@Test
public void extractFrame_changeMediaItem_extractsFrameFromTheCorrectItem() throws Exception {
frameExtractor =
new ExperimentalFrameExtractor(
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFutureFirstItem = frameExtractor.getFrame(/* positionMs= */ 0);
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
ListenableFuture<Frame> frameFutureSecondItem =
frameExtractor.getFrame(/* positionMs= */ 8_500);
Frame frameFirstItem = frameFutureFirstItem.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmapFirstItem = frameFirstItem.bitmap;
Bitmap expectedBitmapFirstItem =
readBitmap(
/* assetString= */ GOLDEN_ASSET_FOLDER_PATH
+ "internal_emulator_transformer_output_180_rotated_0.000.png");
maybeSaveTestBitmap(
testId, /* bitmapLabel= */ "firstItem", actualBitmapFirstItem, /* path= */ null);
Frame frameSecondItem = frameFutureSecondItem.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmapSecondItem = frameSecondItem.bitmap;
Bitmap expectedBitmapSecondItem =
readBitmap(
/* assetString= */ GOLDEN_ASSET_FOLDER_PATH
+ "sample_with_increasing_timestamps_360p_8.531.png");
maybeSaveTestBitmap(
testId, /* bitmapLabel= */ "secondItem", actualBitmapSecondItem, /* path= */ null);
assertThat(frameFirstItem.presentationTimeMs).isEqualTo(0);
assertBitmapsAreSimilar(expectedBitmapFirstItem, actualBitmapFirstItem, PSNR_THRESHOLD);
assertThat(frameSecondItem.presentationTimeMs).isEqualTo(8_531);
assertBitmapsAreSimilar(expectedBitmapSecondItem, actualBitmapSecondItem, PSNR_THRESHOLD);
}
}

View File

@ -23,6 +23,7 @@ import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_COLOR_TEST_1080P_HLG10;
import static androidx.media3.transformer.AndroidTestUtil.MP4_TRIM_OPTIMIZATION_270;
import static androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -56,8 +57,8 @@ public class FrameExtractorHdrTest {
"test-generated-goldens/sample_mp4_first_frame/electrical_colors/tone_map_hlg_to_sdr.png";
// File names in test-generated-goldens/FrameExtractorTest end with the presentation time of the
// extracted frame in seconds and milliseconds (_0.000 for 0s ; _1.567 for 1.567 seconds).
private static final String EXTRACT_HLG_PNG_ASSET_PATH =
"test-generated-goldens/FrameExtractorTest/hlg10-color-test_0.000.png";
private static final String GOLDEN_ASSET_FOLDER_PATH =
"test-generated-goldens/FrameExtractorTest/";
private static final long TIMEOUT_SECONDS = 10;
private static final float PSNR_THRESHOLD = 25f;
@ -85,10 +86,9 @@ public class FrameExtractorHdrTest {
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
frameExtractor =
new ExperimentalFrameExtractor(
context,
new ExperimentalFrameExtractor.Configuration.Builder().build(),
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri),
/* effects= */ ImmutableList.of());
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
frameExtractor.getFrame(/* positionMs= */ 0);
@ -111,16 +111,17 @@ public class FrameExtractorHdrTest {
context,
new ExperimentalFrameExtractor.Configuration.Builder()
.setExtractHdrFrames(true)
.build(),
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri),
/* effects= */ ImmutableList.of());
.build());
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
frameExtractor.getFrame(/* positionMs= */ 0);
ExperimentalFrameExtractor.Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmap = frame.bitmap;
Bitmap actualBitmapDefaultColorSpace = removeColorSpace(actualBitmap);
Bitmap expectedBitmap = readBitmap(EXTRACT_HLG_PNG_ASSET_PATH);
Bitmap expectedBitmap =
readBitmap(/* assetString= */ GOLDEN_ASSET_FOLDER_PATH + "hlg10-color-test_0.000.png");
maybeSaveTestBitmap(
testId,
/* bitmapLabel= */ "actualBitmapDefaultColorSpace",
@ -133,6 +134,45 @@ public class FrameExtractorHdrTest {
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
}
@Test
public void
extractFrame_changeMediaItemFromHdrToSdrWithToneMapping_extractsFrameFromTheCorrectItem()
throws Exception {
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
frameExtractor =
new ExperimentalFrameExtractor(
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFutureFirstItem =
frameExtractor.getFrame(/* positionMs= */ 0);
frameExtractor.setMediaItem(
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri), /* effects= */ ImmutableList.of());
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFutureSecondItem =
frameExtractor.getFrame(/* positionMs= */ 0);
ExperimentalFrameExtractor.Frame frameFirstItem =
frameFutureFirstItem.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmapFirstItem = frameFirstItem.bitmap;
Bitmap expectedBitmapFirstItem = readBitmap(TONE_MAP_HLG_TO_SDR_PNG_ASSET_PATH);
maybeSaveTestBitmap(
testId, /* bitmapLabel= */ "firstItem", actualBitmapFirstItem, /* path= */ null);
ExperimentalFrameExtractor.Frame frameSecondItem =
frameFutureSecondItem.get(TIMEOUT_SECONDS, SECONDS);
Bitmap actualBitmapSecondItem = frameSecondItem.bitmap;
Bitmap expectedBitmapSecondItem =
readBitmap(
/* assetString= */ GOLDEN_ASSET_FOLDER_PATH
+ "internal_emulator_transformer_output_180_rotated_0.000.png");
maybeSaveTestBitmap(
testId, /* bitmapLabel= */ "secondItem", actualBitmapSecondItem, /* path= */ null);
assertThat(frameFirstItem.presentationTimeMs).isEqualTo(0);
assertBitmapsAreSimilar(expectedBitmapFirstItem, actualBitmapFirstItem, PSNR_THRESHOLD);
assertThat(frameSecondItem.presentationTimeMs).isEqualTo(0);
assertBitmapsAreSimilar(expectedBitmapSecondItem, actualBitmapSecondItem, PSNR_THRESHOLD);
}
/**
* Copy the contents of the input {@link Bitmap} into a {@link Bitmap} with {@link
* Bitmap.Config#RGBA_F16} config and default {@link ColorSpace}.

View File

@ -24,11 +24,13 @@ import static androidx.media3.common.ColorInfo.SDR_BT709_LIMITED;
import static androidx.media3.common.ColorInfo.isTransferHdr;
import static androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK;
import static androidx.media3.common.PlaybackException.ERROR_CODE_INVALID_STATE;
import static androidx.media3.common.PlaybackException.ERROR_CODE_SETUP_REQUIRED;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.GlUtil.createRgb10A2Texture;
import static androidx.media3.common.util.Util.SDK_INT;
import static androidx.media3.common.util.Util.usToMs;
import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import android.content.Context;
@ -77,6 +79,7 @@ import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import com.google.common.collect.ImmutableList;
@ -90,7 +93,6 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.checkerframework.checker.initialization.qual.Initialized;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
@ -120,7 +122,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* </ul>
*/
@UnstableApi
public final class ExperimentalFrameExtractor implements AnalyticsListener {
public final class ExperimentalFrameExtractor {
/** Configuration for the frame extractor. */
public static final class Configuration {
@ -261,20 +263,15 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
* The last {@link Frame} that was extracted successfully. Accessed on the {@linkplain
* ExoPlayer#getApplicationLooper() ExoPlayer application thread}.
*/
private @MonotonicNonNull Frame lastExtractedFrame;
@Nullable private Frame lastExtractedFrame;
/**
* Creates an instance.
*
* @param context {@link Context}.
* @param configuration The {@link Configuration} for this frame extractor.
* @param mediaItem The {@link MediaItem} from which frames are extracted.
* @param effects The {@link List} of {@linkplain Effect video effects} to apply to the extracted
* video frames.
*/
// TODO: b/350498258 - Support changing the MediaItem.
public ExperimentalFrameExtractor(
Context context, Configuration configuration, MediaItem mediaItem, List<Effect> effects) {
public ExperimentalFrameExtractor(Context context, Configuration configuration) {
player =
new ExoPlayer.Builder(
context,
@ -292,29 +289,41 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
})
.setSeekParameters(configuration.seekParameters)
.build();
player.addAnalyticsListener(new PlayerListener());
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
extractedFrameNeedsRendering = new AtomicBoolean();
frameBeingExtractedCompleterAtomicReference = new AtomicReference<>(null);
lastRequestedFrameFuture = immediateCancelledFuture();
}
/**
* Sets a new {@link MediaItem}.
*
* <p>Changing between SDR and HDR {@link MediaItem}s is not supported when {@link
* Configuration#extractHdrFrames} is true.
*
* @param mediaItem The {@link MediaItem} from which frames will be extracted.
* @param effects The {@link List} of {@linkplain Effect video effects} to apply to the extracted
* video frames.
*/
public void setMediaItem(MediaItem mediaItem, List<Effect> effects) {
ListenableFuture<Frame> previousRequestedFrame = lastRequestedFrameFuture;
// TODO: b/350498258 - Extracting the first frame is a workaround for ExoPlayer.setVideoEffects
// returning incorrect timestamps if we seek the player before rendering starts from zero.
frameBeingExtractedCompleterAtomicReference = new AtomicReference<>(null);
lastRequestedFrameFuture =
CallbackToFutureAdapter.getFuture(
completer -> {
frameBeingExtractedCompleterAtomicReference.set(completer);
// TODO: b/350498258 - Refactor this and remove declaring this reference as
// initialized to satisfy the nullness checker.
@SuppressWarnings("nullness:assignment")
@Initialized
ExperimentalFrameExtractor thisRef = this;
playerApplicationThreadHandler.post(
previousRequestedFrame.addListener(
() -> {
player.addAnalyticsListener(thisRef);
player.setVideoEffects(thisRef.buildVideoEffects(effects));
frameBeingExtractedCompleterAtomicReference.set(completer);
lastExtractedFrame = null;
player.setVideoEffects(buildVideoEffects(effects));
player.setMediaItem(mediaItem);
player.setPlayWhenReady(false);
player.prepare();
});
return "ExperimentalFrameExtractor constructor";
},
playerApplicationThreadHandler::post);
return "ExperimentalFrameExtractor.setMediaItem";
});
}
@ -369,6 +378,12 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
}
if (playerError != null) {
completer.setException(playerError);
} else if (player.getCurrentMediaItem() == null) {
completer.setException(
new PlaybackException(
"Player has no current item. Call setMediaItem before getFrame.",
null,
ERROR_CODE_SETUP_REQUIRED));
} else {
checkState(frameBeingExtractedCompleterAtomicReference.compareAndSet(null, completer));
extractedFrameNeedsRendering.set(false);
@ -395,8 +410,30 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
waitForRelease.blockUninterruptible();
}
// AnalyticsListener
@VisibleForTesting
/* package */ ListenableFuture<@NullableType DecoderCounters> getDecoderCounters() {
SettableFuture<@NullableType DecoderCounters> decoderCountersSettableFuture =
SettableFuture.create();
playerApplicationThreadHandler.post(
() -> decoderCountersSettableFuture.set(player.getVideoDecoderCounters()));
return decoderCountersSettableFuture;
}
private ImmutableList<Effect> buildVideoEffects(List<Effect> effects) {
ImmutableList.Builder<Effect> listBuilder = new ImmutableList.Builder<>();
listBuilder.addAll(effects);
listBuilder.add(
(MatrixTransformation)
presentationTimeUs -> {
Matrix mirrorY = new Matrix();
mirrorY.setScale(/* sx= */ 1, /* sy= */ -1);
return mirrorY;
});
listBuilder.add(new FrameReader());
return listBuilder.build();
}
private final class PlayerListener implements AnalyticsListener {
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
// Fail the next frame to be extracted. Errors will propagate to later pending requests via
@ -423,28 +460,6 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
frameBeingExtractedCompleter.set(checkNotNull(lastExtractedFrame));
}
}
@VisibleForTesting
/* package */ ListenableFuture<@NullableType DecoderCounters> getDecoderCounters() {
SettableFuture<@NullableType DecoderCounters> decoderCountersSettableFuture =
SettableFuture.create();
playerApplicationThreadHandler.post(
() -> decoderCountersSettableFuture.set(player.getVideoDecoderCounters()));
return decoderCountersSettableFuture;
}
private ImmutableList<Effect> buildVideoEffects(List<Effect> effects) {
ImmutableList.Builder<Effect> listBuilder = new ImmutableList.Builder<>();
listBuilder.addAll(effects);
listBuilder.add(
(MatrixTransformation)
presentationTimeUs -> {
Matrix mirrorY = new Matrix();
mirrorY.setScale(/* sx= */ 1, /* sy= */ -1);
return mirrorY;
});
listBuilder.add(new FrameReader());
return listBuilder.build();
}
private final class FrameReader implements GlEffect {
@ -609,7 +624,7 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
private boolean frameRenderedSinceLastPositionReset;
private List<Effect> effectsFromPlayer;
private @MonotonicNonNull Effect rotation;
@Nullable private Effect rotation;
public FrameExtractorRenderer(
Context context,
@ -627,6 +642,18 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
effectsFromPlayer = ImmutableList.of();
}
@Override
protected void onStreamChanged(
Format[] formats,
long startPositionUs,
long offsetUs,
MediaSource.MediaPeriodId mediaPeriodId)
throws ExoPlaybackException {
super.onStreamChanged(formats, startPositionUs, offsetUs, mediaPeriodId);
frameRenderedSinceLastPositionReset = false;
setRotation(null);
}
@Override
public void setVideoEffects(List<Effect> effects) {
effectsFromPlayer = effects;
@ -653,17 +680,21 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
// Some decoders do not apply rotation. It's no extra cost to rotate with a GL matrix
// transformation effect instead.
// https://developer.android.com/reference/android/media/MediaCodec#transformations-when-rendering-onto-surface
rotation =
setRotation(
new ScaleAndRotateTransformation.Builder()
.setRotationDegrees(360 - format.rotationDegrees)
.build();
setEffectsWithRotation();
.build());
formatHolder.format = format.buildUpon().setRotationDegrees(0).build();
}
}
return super.onInputFormatChanged(formatHolder);
}
private void setRotation(@Nullable Effect rotation) {
this.rotation = rotation;
setEffectsWithRotation();
}
private void setEffectsWithRotation() {
ImmutableList.Builder<Effect> effectBuilder = new ImmutableList.Builder<>();
if (rotation != null) {