mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
b816e2f284
commit
1d2ffcb165
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,10 +120,10 @@ public class FrameExtractorTest {
|
||||
public void extractFrame_oneFrameWithPresentationEffect_returnsScaledFrame() throws Exception {
|
||||
frameExtractor =
|
||||
new ExperimentalFrameExtractor(
|
||||
context,
|
||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
||||
MediaItem.fromUri(FILE_PATH),
|
||||
/* effects= */ ImmutableList.of(Presentation.createForHeight(180)));
|
||||
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||
frameExtractor.setMediaItem(
|
||||
MediaItem.fromUri(FILE_PATH),
|
||||
/* effects= */ ImmutableList.of(Presentation.createForHeight(180)));
|
||||
|
||||
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
|
||||
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}.
|
||||
|
@ -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();
|
||||
// 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 = 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.
|
||||
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,35 +410,6 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
||||
waitForRelease.blockUninterruptible();
|
||||
}
|
||||
|
||||
// AnalyticsListener
|
||||
|
||||
@Override
|
||||
public void onPlayerError(EventTime eventTime, PlaybackException error) {
|
||||
// Fail the next frame to be extracted. Errors will propagate to later pending requests via
|
||||
// Future callbacks.
|
||||
@Nullable
|
||||
CallbackToFutureAdapter.Completer<Frame> frameBeingExtractedCompleter =
|
||||
frameBeingExtractedCompleterAtomicReference.getAndSet(null);
|
||||
if (frameBeingExtractedCompleter != null) {
|
||||
frameBeingExtractedCompleter.setException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
|
||||
// The player enters STATE_BUFFERING at the start of a seek.
|
||||
// At the end of a seek, the player enters STATE_READY after the video renderer position has
|
||||
// been reset, and the renderer reports that it's ready.
|
||||
if (state == Player.STATE_READY && !extractedFrameNeedsRendering.get()) {
|
||||
// If the seek resolves to the current position, the renderer position will not be reset
|
||||
// and extractedFrameNeedsRendering remains false. No frames are rendered. Repeat the
|
||||
// previously returned frame.
|
||||
CallbackToFutureAdapter.Completer<Frame> frameBeingExtractedCompleter =
|
||||
checkNotNull(frameBeingExtractedCompleterAtomicReference.getAndSet(null));
|
||||
frameBeingExtractedCompleter.set(checkNotNull(lastExtractedFrame));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ ListenableFuture<@NullableType DecoderCounters> getDecoderCounters() {
|
||||
SettableFuture<@NullableType DecoderCounters> decoderCountersSettableFuture =
|
||||
@ -447,6 +433,35 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
||||
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
|
||||
// Future callbacks.
|
||||
@Nullable
|
||||
CallbackToFutureAdapter.Completer<Frame> frameBeingExtractedCompleter =
|
||||
frameBeingExtractedCompleterAtomicReference.getAndSet(null);
|
||||
if (frameBeingExtractedCompleter != null) {
|
||||
frameBeingExtractedCompleter.setException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
|
||||
// The player enters STATE_BUFFERING at the start of a seek.
|
||||
// At the end of a seek, the player enters STATE_READY after the video renderer position has
|
||||
// been reset, and the renderer reports that it's ready.
|
||||
if (state == Player.STATE_READY && !extractedFrameNeedsRendering.get()) {
|
||||
// If the seek resolves to the current position, the renderer position will not be reset
|
||||
// and extractedFrameNeedsRendering remains false. No frames are rendered. Repeat the
|
||||
// previously returned frame.
|
||||
CallbackToFutureAdapter.Completer<Frame> frameBeingExtractedCompleter =
|
||||
checkNotNull(frameBeingExtractedCompleterAtomicReference.getAndSet(null));
|
||||
frameBeingExtractedCompleter.set(checkNotNull(lastExtractedFrame));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class FrameReader implements GlEffect {
|
||||
@Override
|
||||
public GlShaderProgram toGlShaderProgram(Context context, boolean useHdr)
|
||||
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user