mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Read Bitmap in ExperimentalFrameExtractor
Add a MatrixTransformation GlEffect to flip between OpenGL and Bitmap coordinates PiperOrigin-RevId: 696029842
This commit is contained in:
parent
64e92cb8e1
commit
175dca41df
@ -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.test.utils.BitmapPixelTestUtil.maybeSaveTestBitmap;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
||||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
@ -23,6 +24,7 @@ import static org.junit.Assert.assertThrows;
|
|||||||
|
|
||||||
import android.app.Instrumentation;
|
import android.app.Instrumentation;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
import androidx.media3.common.util.NullableType;
|
import androidx.media3.common.util.NullableType;
|
||||||
@ -38,7 +40,10 @@ import java.util.concurrent.ExecutionException;
|
|||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TestName;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
/** End-to-end instrumentation test for {@link ExperimentalFrameExtractor}. */
|
/** End-to-end instrumentation test for {@link ExperimentalFrameExtractor}. */
|
||||||
@ -48,10 +53,18 @@ public class FrameExtractorTest {
|
|||||||
"asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4";
|
"asset:///media/mp4/sample_with_increasing_timestamps_360p.mp4";
|
||||||
private static final long TIMEOUT_SECONDS = 10;
|
private static final long TIMEOUT_SECONDS = 10;
|
||||||
|
|
||||||
|
@Rule public final TestName testName = new TestName();
|
||||||
|
|
||||||
private final Context context = ApplicationProvider.getApplicationContext();
|
private final Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
|
||||||
|
private String testId;
|
||||||
private @MonotonicNonNull ExperimentalFrameExtractor frameExtractor;
|
private @MonotonicNonNull ExperimentalFrameExtractor frameExtractor;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUpTestId() {
|
||||||
|
testId = testName.getMethodName();
|
||||||
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void tearDown() {
|
public void tearDown() {
|
||||||
if (frameExtractor != null) {
|
if (frameExtractor != null) {
|
||||||
@ -64,8 +77,15 @@ public class FrameExtractorTest {
|
|||||||
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
|
frameExtractor = new ExperimentalFrameExtractor(context, MediaItem.fromUri(FILE_PATH));
|
||||||
|
|
||||||
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
|
ListenableFuture<Frame> frameFuture = frameExtractor.getFrame(/* positionMs= */ 8_500);
|
||||||
|
Bitmap bitmap = frameFuture.get(TIMEOUT_SECONDS, SECONDS).bitmap;
|
||||||
|
|
||||||
|
maybeSaveTestBitmap(testId, /* bitmapLabel= */ "actual", bitmap, /* path= */ null);
|
||||||
assertThat(frameFuture.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_531);
|
assertThat(frameFuture.get(TIMEOUT_SECONDS, SECONDS).presentationTimeMs).isEqualTo(8_531);
|
||||||
|
// TODO: b/350498258 - Actually check Bitmap contents. Due to bugs in hardware decoders,
|
||||||
|
// such a test would require a too high tolerance.
|
||||||
|
assertThat(bitmap.getWidth()).isEqualTo(640);
|
||||||
|
assertThat(bitmap.getHeight()).isEqualTo(360);
|
||||||
|
assertThat(bitmap.getConfig()).isEqualTo(Bitmap.Config.ARGB_8888);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -24,6 +24,9 @@ import static androidx.media3.common.util.Util.usToMs;
|
|||||||
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;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.opengl.GLES20;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -34,9 +37,11 @@ import androidx.media3.common.MediaItem;
|
|||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.ConditionVariable;
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
|
import androidx.media3.common.util.GlUtil;
|
||||||
import androidx.media3.common.util.NullableType;
|
import androidx.media3.common.util.NullableType;
|
||||||
import androidx.media3.effect.GlEffect;
|
import androidx.media3.effect.GlEffect;
|
||||||
import androidx.media3.effect.GlShaderProgram;
|
import androidx.media3.effect.GlShaderProgram;
|
||||||
|
import androidx.media3.effect.MatrixTransformation;
|
||||||
import androidx.media3.effect.PassthroughShaderProgram;
|
import androidx.media3.effect.PassthroughShaderProgram;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
import androidx.media3.exoplayer.analytics.AnalyticsListener;
|
||||||
@ -45,6 +50,7 @@ import com.google.common.util.concurrent.FutureCallback;
|
|||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.checkerframework.checker.initialization.qual.Initialized;
|
import org.checkerframework.checker.initialization.qual.Initialized;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
@ -59,14 +65,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
/* package */ final class ExperimentalFrameExtractor implements AnalyticsListener {
|
/* package */ final class ExperimentalFrameExtractor implements AnalyticsListener {
|
||||||
|
|
||||||
/** Stores an extracted and decoded video frame. */
|
/** Stores an extracted and decoded video frame. */
|
||||||
// TODO: b/350498258 - Add a Bitmap field to Frame.
|
|
||||||
public static final class Frame {
|
public static final class Frame {
|
||||||
|
|
||||||
/** The presentation timestamp of the extracted frame, in milliseconds. */
|
/** The presentation timestamp of the extracted frame, in milliseconds. */
|
||||||
public final long presentationTimeMs;
|
public final long presentationTimeMs;
|
||||||
|
|
||||||
private Frame(long presentationTimeMs) {
|
/** The extracted frame contents. */
|
||||||
|
public final Bitmap bitmap;
|
||||||
|
|
||||||
|
private Frame(long presentationTimeMs, Bitmap bitmap) {
|
||||||
this.presentationTimeMs = presentationTimeMs;
|
this.presentationTimeMs = presentationTimeMs;
|
||||||
|
this.bitmap = bitmap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +231,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ImmutableList<Effect> buildVideoEffects() {
|
private ImmutableList<Effect> buildVideoEffects() {
|
||||||
return ImmutableList.of(new FrameReader());
|
return ImmutableList.of(
|
||||||
|
(MatrixTransformation)
|
||||||
|
presentationTimeUs -> {
|
||||||
|
Matrix mirrorY = new Matrix();
|
||||||
|
mirrorY.setScale(/* sx= */ 1, /* sy= */ -1);
|
||||||
|
return mirrorY;
|
||||||
|
},
|
||||||
|
new FrameReader());
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class FrameReader implements GlEffect {
|
private final class FrameReader implements GlEffect {
|
||||||
@ -234,13 +250,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final class FrameReadingGlShaderProgram extends PassthroughShaderProgram {
|
private final class FrameReadingGlShaderProgram extends PassthroughShaderProgram {
|
||||||
|
private static final int BYTES_PER_PIXEL = 4;
|
||||||
|
|
||||||
|
private ByteBuffer byteBuffer = ByteBuffer.allocateDirect(0);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueInputFrame(
|
public void queueInputFrame(
|
||||||
GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) {
|
GlObjectsProvider glObjectsProvider, GlTextureInfo inputTexture, long presentationTimeUs) {
|
||||||
|
int pixelBufferSize = inputTexture.width * inputTexture.height * BYTES_PER_PIXEL;
|
||||||
|
if (byteBuffer.capacity() != pixelBufferSize) {
|
||||||
|
byteBuffer = ByteBuffer.allocateDirect(pixelBufferSize);
|
||||||
|
}
|
||||||
|
byteBuffer.clear();
|
||||||
|
try {
|
||||||
|
GlUtil.focusFramebufferUsingCurrentContext(
|
||||||
|
inputTexture.fboId, inputTexture.width, inputTexture.height);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
GLES20.glReadPixels(
|
||||||
|
/* x= */ 0,
|
||||||
|
/* y= */ 0,
|
||||||
|
inputTexture.width,
|
||||||
|
inputTexture.height,
|
||||||
|
GLES20.GL_RGBA,
|
||||||
|
GLES20.GL_UNSIGNED_BYTE,
|
||||||
|
byteBuffer);
|
||||||
|
GlUtil.checkGlError();
|
||||||
|
} catch (GlUtil.GlException e) {
|
||||||
|
onError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// According to https://www.khronos.org/opengl/wiki/Pixel_Transfer#Endian_issues,
|
||||||
|
// the colors will have the order RGBA in client memory. This is what the bitmap expects:
|
||||||
|
// https://developer.android.com/reference/android/graphics/Bitmap.Config.
|
||||||
|
Bitmap bitmap =
|
||||||
|
Bitmap.createBitmap(inputTexture.width, inputTexture.height, Bitmap.Config.ARGB_8888);
|
||||||
|
bitmap.copyPixelsFromBuffer(byteBuffer);
|
||||||
|
|
||||||
SettableFuture<Frame> frameBeingExtractedFuture =
|
SettableFuture<Frame> frameBeingExtractedFuture =
|
||||||
checkNotNull(frameBeingExtractedFutureAtomicReference.getAndSet(null));
|
checkNotNull(frameBeingExtractedFutureAtomicReference.getAndSet(null));
|
||||||
// TODO: b/350498258 - Read the input texture contents into a Bitmap.
|
frameBeingExtractedFuture.set(new Frame(usToMs(presentationTimeUs), bitmap));
|
||||||
frameBeingExtractedFuture.set(new Frame(usToMs(presentationTimeUs)));
|
|
||||||
// Drop frame: do not call outputListener.onOutputFrameAvailable().
|
// Drop frame: do not call outputListener.onOutputFrameAvailable().
|
||||||
// Block effects pipeline: do not call inputListener.onReadyToAcceptInputFrame().
|
// Block effects pipeline: do not call inputListener.onReadyToAcceptInputFrame().
|
||||||
// The effects pipeline will unblock and receive new frames when flushed after a seek.
|
// The effects pipeline will unblock and receive new frames when flushed after a seek.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user