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
|
* 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.
|
* <p>The returned list is not modifiable.
|
||||||
*/
|
*/
|
||||||
@ -266,7 +267,9 @@ public final class MediaCodecUtil {
|
|||||||
public static List<MediaCodecInfo> getDecoderInfosSortedBySoftwareOnly(
|
public static List<MediaCodecInfo> getDecoderInfosSortedBySoftwareOnly(
|
||||||
List<MediaCodecInfo> decoderInfos) {
|
List<MediaCodecInfo> decoderInfos) {
|
||||||
decoderInfos = new ArrayList<>(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);
|
return ImmutableList.copyOf(decoderInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package androidx.media3.transformer;
|
package androidx.media3.transformer;
|
||||||
|
|
||||||
import static androidx.media3.common.PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND;
|
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.exoplayer.SeekParameters.CLOSEST_SYNC;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
||||||
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
import static androidx.media3.test.utils.BitmapPixelTestUtil.readBitmap;
|
||||||
@ -30,6 +31,7 @@ import android.app.Instrumentation;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
import androidx.media3.common.util.NullableType;
|
import androidx.media3.common.util.NullableType;
|
||||||
import androidx.media3.effect.Presentation;
|
import androidx.media3.effect.Presentation;
|
||||||
@ -92,10 +94,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_oneFrame_returnsNearest() throws Exception {
|
public void extractFrame_oneFrame_returnsNearest() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
|
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
|
||||||
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
@ -120,8 +120,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_oneFrameWithPresentationEffect_returnsScaledFrame() throws Exception {
|
public void extractFrame_oneFrameWithPresentationEffect_returnsScaledFrame() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(
|
||||||
MediaItem.fromUri(FILE_PATH),
|
MediaItem.fromUri(FILE_PATH),
|
||||||
/* effects= */ ImmutableList.of(Presentation.createForHeight(180)));
|
/* effects= */ ImmutableList.of(Presentation.createForHeight(180)));
|
||||||
|
|
||||||
@ -148,10 +148,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_pastDuration_returnsLastFrame() throws Exception {
|
public void extractFrame_pastDuration_returnsLastFrame() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000);
|
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 200_000);
|
||||||
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
@ -177,10 +175,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_repeatedPositionMs_returnsTheSameFrame() throws Exception {
|
public void extractFrame_repeatedPositionMs_returnsTheSameFrame() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
|
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
|
||||||
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L);
|
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 66L, 66L);
|
||||||
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
|
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
|
||||||
@ -217,9 +213,8 @@ public class FrameExtractorTest {
|
|||||||
context,
|
context,
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder()
|
new ExperimentalFrameExtractor.Configuration.Builder()
|
||||||
.setSeekParameters(CLOSEST_SYNC)
|
.setSeekParameters(CLOSEST_SYNC)
|
||||||
.build(),
|
.build());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
|
ImmutableList<Long> requestedFramePositionsMs = ImmutableList.of(0L, 0L, 33L, 34L, 34L);
|
||||||
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 0L, 0L, 0L);
|
ImmutableList<Long> expectedFramePositionsMs = ImmutableList.of(0L, 0L, 0L, 0L, 0L);
|
||||||
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
|
List<ListenableFuture<Frame>> frameFutures = new ArrayList<>();
|
||||||
@ -253,10 +248,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
|
public void extractFrame_randomAccess_returnsCorrectFrames() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
||||||
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
||||||
@ -284,9 +277,8 @@ public class FrameExtractorTest {
|
|||||||
context,
|
context,
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder()
|
new ExperimentalFrameExtractor.Configuration.Builder()
|
||||||
.setSeekParameters(CLOSEST_SYNC)
|
.setSeekParameters(CLOSEST_SYNC)
|
||||||
.build(),
|
.build());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
||||||
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
||||||
@ -314,10 +306,8 @@ public class FrameExtractorTest {
|
|||||||
String filePath = "asset:///nonexistent";
|
String filePath = "asset:///nonexistent";
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(filePath), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(filePath),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0);
|
ListenableFuture<Frame> frame0 = frameExtractor.getFrame(/* positionMs= */ 0);
|
||||||
|
|
||||||
@ -328,14 +318,27 @@ public class FrameExtractorTest {
|
|||||||
.isEqualTo(ERROR_CODE_IO_FILE_NOT_FOUND);
|
.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
|
@Test
|
||||||
public void extractFrame_oneFrame_completesViaCallback() throws Exception {
|
public void extractFrame_oneFrame_completesViaCallback() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>();
|
AtomicReference<@NullableType Frame> frameAtomicReference = new AtomicReference<>();
|
||||||
AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>();
|
AtomicReference<@NullableType Throwable> throwableAtomicReference = new AtomicReference<>();
|
||||||
ConditionVariable frameReady = new ConditionVariable();
|
ConditionVariable frameReady = new ConditionVariable();
|
||||||
@ -373,10 +376,8 @@ public class FrameExtractorTest {
|
|||||||
public void frameExtractor_releaseOnPlayerLooper_returns() {
|
public void frameExtractor_releaseOnPlayerLooper_returns() {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
|
||||||
instrumentation.runOnMainSync(frameExtractor::release);
|
instrumentation.runOnMainSync(frameExtractor::release);
|
||||||
@ -387,10 +388,9 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_oneFrameRotated_returnsFrameInCorrectOrientation() throws Exception {
|
public void extractFrame_oneFrameRotated_returnsFrameInCorrectOrientation() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(
|
||||||
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri),
|
MediaItem.fromUri(MP4_TRIM_OPTIMIZATION_270.uri), /* effects= */ ImmutableList.of());
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 0);
|
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 0);
|
||||||
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
@ -415,10 +415,8 @@ public class FrameExtractorTest {
|
|||||||
public void extractFrame_randomAccessWithCancellation_returnsCorrectFrames() throws Exception {
|
public void extractFrame_randomAccessWithCancellation_returnsCorrectFrames() throws Exception {
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(MediaItem.fromUri(FILE_PATH), /* effects= */ ImmutableList.of());
|
||||||
MediaItem.fromUri(FILE_PATH),
|
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
ListenableFuture<Frame> frame5 = frameExtractor.getFrame(/* positionMs= */ 5_000);
|
||||||
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
ListenableFuture<Frame> frame3 = frameExtractor.getFrame(/* positionMs= */ 3_000);
|
||||||
@ -440,4 +438,39 @@ public class FrameExtractorTest {
|
|||||||
.renderedOutputBufferCount)
|
.renderedOutputBufferCount)
|
||||||
.isEqualTo(4);
|
.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.BitmapPixelTestUtil.readBitmap;
|
||||||
import static androidx.media3.test.utils.TestUtil.assertBitmapsAreSimilar;
|
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_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 androidx.media3.transformer.mh.HdrCapabilitiesUtil.assumeDeviceSupportsOpenGlToneMapping;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
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";
|
"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
|
// 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).
|
// 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 =
|
private static final String GOLDEN_ASSET_FOLDER_PATH =
|
||||||
"test-generated-goldens/FrameExtractorTest/hlg10-color-test_0.000.png";
|
"test-generated-goldens/FrameExtractorTest/";
|
||||||
private static final long TIMEOUT_SECONDS = 10;
|
private static final long TIMEOUT_SECONDS = 10;
|
||||||
private static final float PSNR_THRESHOLD = 25f;
|
private static final float PSNR_THRESHOLD = 25f;
|
||||||
|
|
||||||
@ -85,10 +86,9 @@ public class FrameExtractorHdrTest {
|
|||||||
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
assumeDeviceSupportsOpenGlToneMapping(testId, MP4_ASSET_COLOR_TEST_1080P_HLG10.videoFormat);
|
||||||
frameExtractor =
|
frameExtractor =
|
||||||
new ExperimentalFrameExtractor(
|
new ExperimentalFrameExtractor(
|
||||||
context,
|
context, new ExperimentalFrameExtractor.Configuration.Builder().build());
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder().build(),
|
frameExtractor.setMediaItem(
|
||||||
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri),
|
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
|
||||||
/* effects= */ ImmutableList.of());
|
|
||||||
|
|
||||||
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
|
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
|
||||||
frameExtractor.getFrame(/* positionMs= */ 0);
|
frameExtractor.getFrame(/* positionMs= */ 0);
|
||||||
@ -111,16 +111,17 @@ public class FrameExtractorHdrTest {
|
|||||||
context,
|
context,
|
||||||
new ExperimentalFrameExtractor.Configuration.Builder()
|
new ExperimentalFrameExtractor.Configuration.Builder()
|
||||||
.setExtractHdrFrames(true)
|
.setExtractHdrFrames(true)
|
||||||
.build(),
|
.build());
|
||||||
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri),
|
frameExtractor.setMediaItem(
|
||||||
/* effects= */ ImmutableList.of());
|
MediaItem.fromUri(MP4_ASSET_COLOR_TEST_1080P_HLG10.uri), /* effects= */ ImmutableList.of());
|
||||||
|
|
||||||
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
|
ListenableFuture<ExperimentalFrameExtractor.Frame> frameFuture =
|
||||||
frameExtractor.getFrame(/* positionMs= */ 0);
|
frameExtractor.getFrame(/* positionMs= */ 0);
|
||||||
ExperimentalFrameExtractor.Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
ExperimentalFrameExtractor.Frame frame = frameFuture.get(TIMEOUT_SECONDS, SECONDS);
|
||||||
Bitmap actualBitmap = frame.bitmap;
|
Bitmap actualBitmap = frame.bitmap;
|
||||||
Bitmap actualBitmapDefaultColorSpace = removeColorSpace(actualBitmap);
|
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(
|
maybeSaveTestBitmap(
|
||||||
testId,
|
testId,
|
||||||
/* bitmapLabel= */ "actualBitmapDefaultColorSpace",
|
/* bitmapLabel= */ "actualBitmapDefaultColorSpace",
|
||||||
@ -133,6 +134,45 @@ public class FrameExtractorHdrTest {
|
|||||||
assertBitmapsAreSimilar(expectedBitmap, actualBitmap, PSNR_THRESHOLD);
|
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
|
* Copy the contents of the input {@link Bitmap} into a {@link Bitmap} with {@link
|
||||||
* Bitmap.Config#RGBA_F16} config and default {@link ColorSpace}.
|
* 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.ColorInfo.isTransferHdr;
|
||||||
import static androidx.media3.common.PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK;
|
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_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.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.common.util.GlUtil.createRgb10A2Texture;
|
import static androidx.media3.common.util.GlUtil.createRgb10A2Texture;
|
||||||
import static androidx.media3.common.util.Util.SDK_INT;
|
import static androidx.media3.common.util.Util.SDK_INT;
|
||||||
import static androidx.media3.common.util.Util.usToMs;
|
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 static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -77,6 +79,7 @@ import androidx.media3.exoplayer.SeekParameters;
|
|||||||
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
|
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -90,7 +93,6 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,7 +122,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
public final class ExperimentalFrameExtractor {
|
||||||
|
|
||||||
/** Configuration for the frame extractor. */
|
/** Configuration for the frame extractor. */
|
||||||
public static final class Configuration {
|
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
|
* The last {@link Frame} that was extracted successfully. Accessed on the {@linkplain
|
||||||
* ExoPlayer#getApplicationLooper() ExoPlayer application thread}.
|
* ExoPlayer#getApplicationLooper() ExoPlayer application thread}.
|
||||||
*/
|
*/
|
||||||
private @MonotonicNonNull Frame lastExtractedFrame;
|
@Nullable private Frame lastExtractedFrame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
*
|
*
|
||||||
* @param context {@link Context}.
|
* @param context {@link Context}.
|
||||||
* @param configuration The {@link Configuration} for this frame extractor.
|
* @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) {
|
||||||
public ExperimentalFrameExtractor(
|
|
||||||
Context context, Configuration configuration, MediaItem mediaItem, List<Effect> effects) {
|
|
||||||
player =
|
player =
|
||||||
new ExoPlayer.Builder(
|
new ExoPlayer.Builder(
|
||||||
context,
|
context,
|
||||||
@ -292,29 +289,41 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
})
|
})
|
||||||
.setSeekParameters(configuration.seekParameters)
|
.setSeekParameters(configuration.seekParameters)
|
||||||
.build();
|
.build();
|
||||||
|
player.addAnalyticsListener(new PlayerListener());
|
||||||
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
|
playerApplicationThreadHandler = new Handler(player.getApplicationLooper());
|
||||||
extractedFrameNeedsRendering = new AtomicBoolean();
|
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
|
// 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.
|
// returning incorrect timestamps if we seek the player before rendering starts from zero.
|
||||||
frameBeingExtractedCompleterAtomicReference = new AtomicReference<>(null);
|
|
||||||
lastRequestedFrameFuture =
|
lastRequestedFrameFuture =
|
||||||
CallbackToFutureAdapter.getFuture(
|
CallbackToFutureAdapter.getFuture(
|
||||||
completer -> {
|
completer -> {
|
||||||
frameBeingExtractedCompleterAtomicReference.set(completer);
|
previousRequestedFrame.addListener(
|
||||||
// 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(
|
|
||||||
() -> {
|
() -> {
|
||||||
player.addAnalyticsListener(thisRef);
|
frameBeingExtractedCompleterAtomicReference.set(completer);
|
||||||
player.setVideoEffects(thisRef.buildVideoEffects(effects));
|
lastExtractedFrame = null;
|
||||||
|
player.setVideoEffects(buildVideoEffects(effects));
|
||||||
player.setMediaItem(mediaItem);
|
player.setMediaItem(mediaItem);
|
||||||
player.setPlayWhenReady(false);
|
player.setPlayWhenReady(false);
|
||||||
player.prepare();
|
player.prepare();
|
||||||
});
|
},
|
||||||
return "ExperimentalFrameExtractor constructor";
|
playerApplicationThreadHandler::post);
|
||||||
|
return "ExperimentalFrameExtractor.setMediaItem";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,6 +378,12 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
}
|
}
|
||||||
if (playerError != null) {
|
if (playerError != null) {
|
||||||
completer.setException(playerError);
|
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 {
|
} else {
|
||||||
checkState(frameBeingExtractedCompleterAtomicReference.compareAndSet(null, completer));
|
checkState(frameBeingExtractedCompleterAtomicReference.compareAndSet(null, completer));
|
||||||
extractedFrameNeedsRendering.set(false);
|
extractedFrameNeedsRendering.set(false);
|
||||||
@ -395,8 +410,30 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
waitForRelease.blockUninterruptible();
|
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
|
@Override
|
||||||
public void onPlayerError(EventTime eventTime, PlaybackException error) {
|
public void onPlayerError(EventTime eventTime, PlaybackException error) {
|
||||||
// Fail the next frame to be extracted. Errors will propagate to later pending requests via
|
// 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));
|
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 {
|
private final class FrameReader implements GlEffect {
|
||||||
@ -609,7 +624,7 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
|
|
||||||
private boolean frameRenderedSinceLastPositionReset;
|
private boolean frameRenderedSinceLastPositionReset;
|
||||||
private List<Effect> effectsFromPlayer;
|
private List<Effect> effectsFromPlayer;
|
||||||
private @MonotonicNonNull Effect rotation;
|
@Nullable private Effect rotation;
|
||||||
|
|
||||||
public FrameExtractorRenderer(
|
public FrameExtractorRenderer(
|
||||||
Context context,
|
Context context,
|
||||||
@ -627,6 +642,18 @@ public final class ExperimentalFrameExtractor implements AnalyticsListener {
|
|||||||
effectsFromPlayer = ImmutableList.of();
|
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
|
@Override
|
||||||
public void setVideoEffects(List<Effect> effects) {
|
public void setVideoEffects(List<Effect> effects) {
|
||||||
effectsFromPlayer = 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
|
// Some decoders do not apply rotation. It's no extra cost to rotate with a GL matrix
|
||||||
// transformation effect instead.
|
// transformation effect instead.
|
||||||
// https://developer.android.com/reference/android/media/MediaCodec#transformations-when-rendering-onto-surface
|
// https://developer.android.com/reference/android/media/MediaCodec#transformations-when-rendering-onto-surface
|
||||||
rotation =
|
setRotation(
|
||||||
new ScaleAndRotateTransformation.Builder()
|
new ScaleAndRotateTransformation.Builder()
|
||||||
.setRotationDegrees(360 - format.rotationDegrees)
|
.setRotationDegrees(360 - format.rotationDegrees)
|
||||||
.build();
|
.build());
|
||||||
setEffectsWithRotation();
|
|
||||||
formatHolder.format = format.buildUpon().setRotationDegrees(0).build();
|
formatHolder.format = format.buildUpon().setRotationDegrees(0).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.onInputFormatChanged(formatHolder);
|
return super.onInputFormatChanged(formatHolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setRotation(@Nullable Effect rotation) {
|
||||||
|
this.rotation = rotation;
|
||||||
|
setEffectsWithRotation();
|
||||||
|
}
|
||||||
|
|
||||||
private void setEffectsWithRotation() {
|
private void setEffectsWithRotation() {
|
||||||
ImmutableList.Builder<Effect> effectBuilder = new ImmutableList.Builder<>();
|
ImmutableList.Builder<Effect> effectBuilder = new ImmutableList.Builder<>();
|
||||||
if (rotation != null) {
|
if (rotation != null) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user