Delete unused surfacecapturer package

PiperOrigin-RevId: 366131005
This commit is contained in:
olly 2021-03-31 23:35:17 +01:00 committed by Oliver Woodman
parent af926deb7a
commit ae47a138ee
7 changed files with 0 additions and 1307 deletions

View File

@ -1,428 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import static com.google.android.exoplayer2.testutil.TestUtil.getBitmap;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Looper;
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ConditionVariable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link PixelCopySurfaceCapturerV24}. */
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 24)
public final class PixelCopySurfaceCapturerV24Test {
private PixelCopySurfaceCapturerV24 pixelCopySurfaceCapturer;
private DummyMainThread testThread;
private List<Bitmap> resultBitmaps;
private List<Throwable> resultExceptions;
private ConditionVariable callbackCalledCondition;
private final SurfaceCapturer.Callback defaultCallback =
new SurfaceCapturer.Callback() {
@Override
public void onSurfaceCaptured(Bitmap bitmap) {
resultBitmaps.add(bitmap);
callbackCalledCondition.open();
}
@Override
public void onSurfaceCaptureError(Exception e) {
resultExceptions.add(e);
callbackCalledCondition.open();
}
};
@Before
public void setUp() {
resultBitmaps = new ArrayList<>();
resultExceptions = new ArrayList<>();
testThread = new DummyMainThread();
callbackCalledCondition = new ConditionVariable();
}
@After
public void tearDown() {
testThread.runOnMainThread(
() -> {
if (pixelCopySurfaceCapturer != null) {
pixelCopySurfaceCapturer.release();
}
});
testThread.release();
}
@Test
public void getSurface_notNull() {
int outputWidth = 80;
int outputHeight = 60;
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
Surface surface = pixelCopySurfaceCapturer.getSurface();
assertThat(surface).isNotNull();
});
}
@Test
public void captureSurface_bmpFile_originalSize() throws IOException, InterruptedException {
int outputWidth = 80;
int outputHeight = 60;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(originalBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_bmpFile_largerSize_sameRatio()
throws IOException, InterruptedException {
int outputWidth = 160;
int outputHeight = 120;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_bmpFile_largerSize_notSameRatio()
throws IOException, InterruptedException {
int outputWidth = 89;
int outputHeight = 67;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_bmpFile_smallerSize_sameRatio()
throws IOException, InterruptedException {
int outputWidth = 40;
int outputHeight = 30;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_bmpFile_smallerSize_notSameRatio()
throws IOException, InterruptedException {
int outputWidth = 32;
int outputHeight = 12;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_pngFile_originalSize() throws IOException, InterruptedException {
int outputWidth = 256;
int outputHeight = 256;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(originalBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_pngFile_largerSize_sameRatio()
throws IOException, InterruptedException {
int outputWidth = 512;
int outputHeight = 512;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_pngFile_largerSize_notSameRatio()
throws IOException, InterruptedException {
int outputWidth = 567;
int outputHeight = 890;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_pngFile_smallerSize_sameRatio()
throws IOException, InterruptedException {
int outputWidth = 128;
int outputHeight = 128;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_pngFile_smallerSize_notSameRatio()
throws IOException, InterruptedException {
int outputWidth = 210;
int outputHeight = 123;
Bitmap originalBitmap =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(originalBitmap, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap);
});
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(1);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(0));
}
@Test
public void captureSurface_multipleTimes() throws IOException, InterruptedException {
int outputWidth = 500;
int outputHeight = 400;
Bitmap originalBitmap1 =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_80_60.bmp");
Bitmap originalBitmap2 =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
"media/bitmap/image_256_256.png");
Bitmap expectedBitmap1 =
Bitmap.createScaledBitmap(originalBitmap1, outputWidth, outputHeight, /* filter= */ true);
Bitmap expectedBitmap2 =
Bitmap.createScaledBitmap(originalBitmap2, outputWidth, outputHeight, /* filter= */ true);
testThread.runOnMainThread(
() -> {
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper()));
drawBitmapOnSurface(originalBitmap1);
});
// Wait for the first bitmap to finish draw on the pixelCopySurfaceCapturer's surface.
callbackCalledCondition.block();
callbackCalledCondition.close();
testThread.runOnMainThread(() -> drawBitmapOnSurface(originalBitmap2));
callbackCalledCondition.block();
assertThat(resultExceptions).isEmpty();
assertThat(resultBitmaps).hasSize(2);
assertBitmapsAreSimilar(expectedBitmap1, resultBitmaps.get(0));
assertBitmapsAreSimilar(expectedBitmap2, resultBitmaps.get(1));
}
@Test
public void getOutputWidth() {
int outputWidth = 500;
int outputHeight = 400;
testThread.runOnMainThread(
() ->
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper())));
assertThat(pixelCopySurfaceCapturer.getOutputWidth()).isEqualTo(outputWidth);
}
@Test
public void getOutputHeight() {
int outputWidth = 500;
int outputHeight = 400;
testThread.runOnMainThread(
() ->
pixelCopySurfaceCapturer =
new PixelCopySurfaceCapturerV24(
defaultCallback, outputWidth, outputHeight, new Handler(Looper.myLooper())));
assertThat(pixelCopySurfaceCapturer.getOutputHeight()).isEqualTo(outputHeight);
}
// Internal methods
private void drawBitmapOnSurface(Bitmap bitmap) {
pixelCopySurfaceCapturer.setDefaultSurfaceTextureBufferSize(
bitmap.getWidth(), bitmap.getHeight());
Surface surface = pixelCopySurfaceCapturer.getSurface();
Canvas canvas = surface.lockCanvas(/* inOutDirty= */ null);
canvas.drawBitmap(bitmap, /* left= */ 0, /* top= */ 0, new Paint());
surface.unlockCanvasAndPost(canvas);
}
/**
* Asserts whether actual bitmap is very similar to the expected bitmap.
*
* <p>This is defined as their PSNR value is greater than or equal to 30.
*
* @param expectedBitmap The expected bitmap.
* @param actualBitmap The actual bitmap.
*/
private static void assertBitmapsAreSimilar(Bitmap expectedBitmap, Bitmap actualBitmap) {
// TODO: Default PSNR threshold of 35 is quite low. Try to increase this without breaking tests.
TestUtil.assertBitmapsAreSimilar(expectedBitmap, actualBitmap, /* psnrThresholdDb= */ 35);
}
}

View File

@ -1,301 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import static com.google.android.exoplayer2.testutil.TestUtil.assertBitmapsAreSimilar;
import static com.google.android.exoplayer2.testutil.TestUtil.getBitmap;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.testutil.DummyMainThread;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.ConditionVariable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link VideoRendererOutputCapturer}. */
@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 24)
public final class VideoRendererOutputCapturerTest {
private static final String TEST_VIDEO_URI = "asset:///media/mp4/testvid_1022ms.mp4";
private static final List<Integer> FRAMES_TO_CAPTURE =
Collections.unmodifiableList(Arrays.asList(0, 14, 15, 16, 29));
private static final List<Integer> CAPTURE_FRAMES_TIME_MS =
Collections.unmodifiableList(Arrays.asList(0, 467, 501, 534, 969));
// TODO: PSNR threshold of 20 is really low. This is partly due to a bug with Texture rendering.
// To be updated when the internal bug has been resolved. See [Internal: b/80516628].
private static final double PSNR_THRESHOLD = 20;
private DummyMainThread testThread;
private List<Pair<Integer, Integer>> resultOutputSizes;
private List<Bitmap> resultBitmaps;
private AtomicReference<Exception> testException;
private ExoPlayer exoPlayer;
private VideoRendererOutputCapturer videoRendererOutputCapturer;
private SingleFrameMediaCodecVideoRenderer mediaCodecVideoRenderer;
private TestRunner testRunner;
@Before
public void setUp() {
testThread = new DummyMainThread();
resultOutputSizes = new ArrayList<>();
resultBitmaps = new ArrayList<>();
testException = new AtomicReference<>();
testRunner = new TestRunner();
testThread.runOnMainThread(
() -> {
Context context = ApplicationProvider.getApplicationContext();
mediaCodecVideoRenderer =
new SingleFrameMediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT);
exoPlayer = new ExoPlayer.Builder(context, mediaCodecVideoRenderer).build();
exoPlayer.setMediaSource(getMediaSource(context, TEST_VIDEO_URI));
exoPlayer.prepare();
videoRendererOutputCapturer =
new VideoRendererOutputCapturer(
testRunner, new Handler(Looper.myLooper()), mediaCodecVideoRenderer, exoPlayer);
});
}
@After
public void tearDown() {
testThread.runOnMainThread(
() -> {
if (exoPlayer != null) {
exoPlayer.release();
}
if (videoRendererOutputCapturer != null) {
videoRendererOutputCapturer.release();
}
});
testThread.release();
}
@Test
public void setOutputSize() throws InterruptedException {
testThread.runOnMainThread(
() -> {
videoRendererOutputCapturer =
new VideoRendererOutputCapturer(
testRunner, new Handler(Looper.myLooper()), mediaCodecVideoRenderer, exoPlayer);
videoRendererOutputCapturer.setOutputSize(800, 600);
});
testRunner.outputSetCondition.block();
assertNoExceptionOccurred();
assertThat(resultOutputSizes).containsExactly(new Pair<>(800, 600));
}
@Test
public void getFrame_getAllFramesCorrectly_originalSize() throws Exception {
int outputWidth = 480;
int outputHeight = 360;
startFramesCaptureProcess(outputWidth, outputHeight, CAPTURE_FRAMES_TIME_MS);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, outputWidth, outputHeight, resultBitmaps);
}
@Test
public void getFrame_getAllFramesCorrectly_largerSize_SameRatio() throws Exception {
int outputWidth = 720;
int outputHeight = 540;
startFramesCaptureProcess(outputWidth, outputHeight, CAPTURE_FRAMES_TIME_MS);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, outputWidth, outputHeight, resultBitmaps);
}
@Test
public void getFrame_getAllFramesCorrectly_largerSize_NotSameRatio() throws Exception {
int outputWidth = 987;
int outputHeight = 654;
startFramesCaptureProcess(outputWidth, outputHeight, CAPTURE_FRAMES_TIME_MS);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, outputWidth, outputHeight, resultBitmaps);
}
@Test
public void getFrame_getAllFramesCorrectly_smallerSize_SameRatio() throws Exception {
int outputWidth = 320;
int outputHeight = 240;
startFramesCaptureProcess(outputWidth, outputHeight, CAPTURE_FRAMES_TIME_MS);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, outputWidth, outputHeight, resultBitmaps);
}
@Test
public void getFrame_getAllFramesCorrectly_smallerSize_NotSameRatio() throws Exception {
int outputWidth = 432;
int outputHeight = 321;
startFramesCaptureProcess(outputWidth, outputHeight, CAPTURE_FRAMES_TIME_MS);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, outputWidth, outputHeight, resultBitmaps);
}
@Ignore // [Internal ref: b/111542655]
@Test
public void getFrame_getAllFramesCorrectly_setSurfaceMultipleTimes() throws Exception {
int firstOutputWidth = 480;
int firstOutputHeight = 360;
startFramesCaptureProcess(firstOutputWidth, firstOutputHeight, CAPTURE_FRAMES_TIME_MS);
int secondOutputWidth = 432;
int secondOutputHeight = 321;
startFramesCaptureProcess(secondOutputWidth, secondOutputHeight, CAPTURE_FRAMES_TIME_MS);
List<Bitmap> firstHalfResult =
new ArrayList<>(resultBitmaps.subList(0, FRAMES_TO_CAPTURE.size()));
List<Bitmap> secondHalfResult =
new ArrayList<>(
resultBitmaps.subList(FRAMES_TO_CAPTURE.size(), 2 * FRAMES_TO_CAPTURE.size()));
assertThat(resultBitmaps).hasSize(FRAMES_TO_CAPTURE.size() * 2);
assertNoExceptionOccurred();
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, firstOutputWidth, firstOutputHeight, firstHalfResult);
assertExtractedFramesMatchExpectation(
FRAMES_TO_CAPTURE, secondOutputWidth, secondOutputHeight, secondHalfResult);
}
private void startFramesCaptureProcess(
int outputWidth, int outputHeight, List<Integer> listFrameToCaptureMs)
throws InterruptedException {
testRunner.captureFinishedCondition.close();
testThread.runOnMainThread(
() -> {
videoRendererOutputCapturer.setOutputSize(outputWidth, outputHeight);
testRunner.setListFramesToCapture(listFrameToCaptureMs);
testRunner.startCapturingProcess();
});
testRunner.captureFinishedCondition.block();
}
private MediaSource getMediaSource(Context context, String testVideoUri) {
return new ProgressiveMediaSource.Factory(new DefaultDataSourceFactory(context))
.createMediaSource(MediaItem.fromUri(testVideoUri));
}
private void assertNoExceptionOccurred() {
if (testException.get() != null) {
throw new AssertionError("Unexpected exception", testException.get());
}
}
private void assertExtractedFramesMatchExpectation(
List<Integer> framesToExtract, int outputWidth, int outputHeight, List<Bitmap> resultBitmaps)
throws IOException {
assertThat(resultBitmaps).hasSize(framesToExtract.size());
for (int i = 0; i < framesToExtract.size(); i++) {
int frameIndex = framesToExtract.get(i);
String expectedBitmapFileName =
String.format("media/mp4/testvid_1022ms_%03d.png", frameIndex);
Bitmap referenceFrame =
getBitmap(
InstrumentationRegistry.getInstrumentation().getTargetContext(),
expectedBitmapFileName);
Bitmap expectedBitmap =
Bitmap.createScaledBitmap(referenceFrame, outputWidth, outputHeight, /* filter= */ true);
assertBitmapsAreSimilar(expectedBitmap, resultBitmaps.get(i), PSNR_THRESHOLD);
}
}
/** A {@link VideoRendererOutputCapturer.Callback} implementation that facilities testing. */
private final class TestRunner implements VideoRendererOutputCapturer.Callback {
private final ConditionVariable outputSetCondition;
private final ConditionVariable captureFinishedCondition;
private final List<Integer> captureFrameTimeMs;
private final AtomicInteger currentFrameIndex;
TestRunner() {
captureFrameTimeMs = new ArrayList<>();
currentFrameIndex = new AtomicInteger();
outputSetCondition = new ConditionVariable();
captureFinishedCondition = new ConditionVariable();
}
public void setListFramesToCapture(List<Integer> listFrameToCaptureMs) {
captureFrameTimeMs.clear();
captureFrameTimeMs.addAll(listFrameToCaptureMs);
}
public void startCapturingProcess() {
currentFrameIndex.set(0);
exoPlayer.seekTo(captureFrameTimeMs.get(currentFrameIndex.get()));
}
@Override
public void onOutputSizeSet(int width, int height) {
resultOutputSizes.add(new Pair<>(width, height));
outputSetCondition.open();
}
@Override
public void onSurfaceCaptured(Bitmap bitmap) {
resultBitmaps.add(bitmap);
int frameIndex = currentFrameIndex.incrementAndGet();
if (frameIndex == captureFrameTimeMs.size()) {
captureFinishedCondition.open();
} else {
exoPlayer.seekTo(captureFrameTimeMs.get(frameIndex));
}
}
@Override
public void onSurfaceCaptureError(Exception exception) {
testException.set(exception);
// By default, if there is any thrown exception, we will finish the test.
captureFinishedCondition.open();
}
}
}

View File

@ -1,108 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.view.PixelCopy;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.util.EGLSurfaceTexture;
/**
* A {@link SurfaceCapturer} implementation that uses {@link PixelCopy} APIs to perform image copy
* from a {@link SurfaceTexture} into a {@link Bitmap}.
*/
@RequiresApi(24)
/* package */ final class PixelCopySurfaceCapturerV24 extends SurfaceCapturer
implements EGLSurfaceTexture.TextureImageListener, PixelCopy.OnPixelCopyFinishedListener {
/** Exception to be thrown if there is some problem capturing images from the surface. */
public static final class SurfaceCapturerException extends Exception {
/**
* One of the {@link PixelCopy} {@code ERROR_*} values return from the {@link
* PixelCopy#request(Surface, Bitmap, PixelCopy.OnPixelCopyFinishedListener, Handler)}
*/
public final int errorCode;
/**
* Constructs a new instance.
*
* @param message The error message.
* @param errorCode The error code.
*/
public SurfaceCapturerException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
}
private final EGLSurfaceTexture eglSurfaceTexture;
private final Handler handler;
private final Surface decoderSurface;
@Nullable private Bitmap bitmap;
@SuppressWarnings("nullness")
/* package */ PixelCopySurfaceCapturerV24(
Callback callback, int outputWidth, int outputHeight, Handler imageRenderingHandler) {
super(callback, outputWidth, outputHeight);
this.handler = imageRenderingHandler;
eglSurfaceTexture = new EGLSurfaceTexture(imageRenderingHandler, /* callback= */ this);
eglSurfaceTexture.init(EGLSurfaceTexture.SECURE_MODE_NONE);
decoderSurface = new Surface(eglSurfaceTexture.getSurfaceTexture());
}
@Override
public Surface getSurface() {
return decoderSurface;
}
@Override
public void release() {
eglSurfaceTexture.release();
decoderSurface.release();
}
/** @see SurfaceTexture#setDefaultBufferSize(int, int) */
public void setDefaultSurfaceTextureBufferSize(int width, int height) {
eglSurfaceTexture.getSurfaceTexture().setDefaultBufferSize(width, height);
}
// TextureImageListener
@Override
public void onFrameAvailable() {
bitmap = Bitmap.createBitmap(getOutputWidth(), getOutputHeight(), Bitmap.Config.ARGB_8888);
PixelCopy.request(decoderSurface, bitmap, this, handler);
}
// OnPixelCopyFinishedListener
@Override
public void onPixelCopyFinished(int copyResult) {
Callback callback = getCallback();
if (copyResult == PixelCopy.SUCCESS && bitmap != null) {
callback.onSurfaceCaptured(bitmap);
} else {
callback.onSurfaceCaptureError(
new SurfaceCapturerException("Couldn't copy image from surface", copyResult));
}
}
}

View File

@ -1,107 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import android.content.Context;
import android.media.MediaCodec;
import android.view.Surface;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import java.nio.ByteBuffer;
/**
* Decodes and renders video using {@link MediaCodec}.
*
* <p>This video renderer will only render the first frame after position reset (seeking), or after
* being re-enabled.
*/
public class SingleFrameMediaCodecVideoRenderer extends MediaCodecVideoRenderer {
private static final String TAG = "SingleFrameMediaCodecVideoRenderer";
private boolean hasRenderedFirstFrame;
@Nullable private Surface surface;
public SingleFrameMediaCodecVideoRenderer(
Context context, MediaCodecSelector mediaCodecSelector) {
super(context, mediaCodecSelector);
}
@Override
public String getName() {
return TAG;
}
@Override
public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
if (messageType == MSG_SET_SURFACE) {
this.surface = (Surface) message;
}
super.handleMessage(messageType, message);
}
@Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException {
hasRenderedFirstFrame = false;
super.onEnabled(joining, mayRenderStartOfStream);
}
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
hasRenderedFirstFrame = false;
super.onPositionReset(positionUs, joining);
}
@Override
protected boolean processOutputBuffer(
long positionUs,
long elapsedRealtimeUs,
@Nullable MediaCodecAdapter codec,
@Nullable ByteBuffer buffer,
int bufferIndex,
int bufferFlags,
int sampleCount,
long bufferPresentationTimeUs,
boolean isDecodeOnlyBuffer,
boolean isLastBuffer,
Format format)
throws ExoPlaybackException {
Assertions.checkNotNull(codec); // Can not render video without codec
long presentationTimeUs = bufferPresentationTimeUs - getOutputStreamOffsetUs();
if (isDecodeOnlyBuffer && !isLastBuffer) {
skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
return true;
}
if (surface == null || hasRenderedFirstFrame) {
return false;
}
hasRenderedFirstFrame = true;
if (Util.SDK_INT >= 21) {
renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime());
} else {
renderOutputBuffer(codec, bufferIndex, presentationTimeUs);
}
return true;
}
}

View File

@ -1,84 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import android.graphics.Bitmap;
import android.view.Surface;
/**
* A surface capturer, which captures image drawn into its surface as bitmaps.
*
* <p>It constructs a {@link Surface}, which can be used as the output surface for an image producer
* to draw images to. As images are being drawn into this surface, this capturer will capture these
* images, and return them via {@link Callback}. The output images will have a fixed frame size of
* (width, height), and any image drawn into the surface will be stretched to fit this frame size.
*/
public abstract class SurfaceCapturer {
/** The callback to be notified of the image capturing result. */
public interface Callback {
/**
* Called when the surface capturer has been able to capture its surface into a {@link Bitmap}.
* This will happen whenever the producer updates the image on the wrapped surface.
*/
void onSurfaceCaptured(Bitmap bitmap);
/** Called when the surface capturer couldn't capture its surface due to an error. */
void onSurfaceCaptureError(Exception e);
}
/** The callback to be notified of the image capturing result. */
private final Callback callback;
/** The width of the output images. */
private final int outputWidth;
/** The height of the output images. */
private final int outputHeight;
/**
* Constructs a new instance.
*
* @param callback See {@link #callback}.
* @param outputWidth See {@link #outputWidth}.
* @param outputHeight See {@link #outputHeight}.
*/
protected SurfaceCapturer(Callback callback, int outputWidth, int outputHeight) {
this.callback = callback;
this.outputWidth = outputWidth;
this.outputHeight = outputHeight;
}
/** Returns the callback to be notified of the image capturing result. */
protected Callback getCallback() {
return callback;
}
/** Returns the width of the output images. */
public int getOutputWidth() {
return outputWidth;
}
/** Returns the height of the output images. */
public int getOutputHeight() {
return outputHeight;
}
/** Returns a {@link Surface} that image producers (camera, video codec etc...) can draw to. */
public abstract Surface getSurface();
/** Releases all kept resources. This instance cannot be used after this call. */
public abstract void release();
}

View File

@ -1,260 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.video.surfacecapturer;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* A capturer that can capture the output of a video {@link SingleFrameMediaCodecVideoRenderer} into
* bitmaps.
*
* <p>Start by setting the output size via {@link #setOutputSize(int, int)}. The capturer will
* create a surface and set this up as the output for the video renderer.
*
* <p>Once the surface setup is done, the capturer will call {@link Callback#onOutputSizeSet(int,
* int)}. After this call, the capturer will capture all images rendered by the {@link Renderer},
* and deliver the captured bitmaps via {@link Callback#onSurfaceCaptured(Bitmap)}, or failure via
* {@link Callback#onSurfaceCaptureError(Exception)}. You can change the output image size at any
* time by calling {@link #setOutputSize(int, int)}.
*
* <p>When this capturer is no longer needed, you need to call {@link #release()} to release all
* resources it is holding. After this call returns, no callback will be called anymore.
*/
public final class VideoRendererOutputCapturer implements Handler.Callback {
/** The callback to be notified of the video image capturing result. */
public interface Callback extends SurfaceCapturer.Callback {
/** Called when output surface has been set properly. */
void onOutputSizeSet(int width, int height);
}
private static final int MSG_SET_OUTPUT = 1;
private static final int MSG_RELEASE = 2;
private final HandlerThread handlerThread;
private final Handler handler;
private final ExoPlayer exoPlayer;
private final EventDispatcher eventDispatcher;
private final Renderer renderer;
@Nullable private SurfaceCapturer surfaceCapturer;
private volatile boolean released;
/**
* Constructs a new instance.
*
* @param callback The callback to be notified of image capturing result.
* @param callbackHandler The {@link Handler} that the callback will be called on.
* @param videoRenderer A {@link SingleFrameMediaCodecVideoRenderer} that will be used to render
* video frames, which this capturer will capture.
* @param exoPlayer The {@link ExoPlayer} instance that is using the video renderer.
*/
public VideoRendererOutputCapturer(
Callback callback,
Handler callbackHandler,
SingleFrameMediaCodecVideoRenderer videoRenderer,
ExoPlayer exoPlayer) {
this.renderer = Assertions.checkNotNull(videoRenderer);
this.exoPlayer = Assertions.checkNotNull(exoPlayer);
this.eventDispatcher = new EventDispatcher(callbackHandler, callback);
// Use a separate thread to handle all operations in this class, because bitmap copying may take
// time and should not be handled on callbackHandler (which maybe run on main thread).
handlerThread = new HandlerThread("ExoPlayer:VideoRendererOutputCapturer");
handlerThread.start();
handler = Util.createHandler(handlerThread.getLooper(), /* callback= */ this);
}
/**
* Sets the size of the video renderer surface's with and height.
*
* <p>This call is performed asynchronously. Only after the {@code callback} receives a call to
* {@link Callback#onOutputSizeSet(int, int)}, the output frames will conform to the new size.
* Output frames before the callback will still conform to previous size.
*
* @param width The target width of the output frame.
* @param height The target height of the output frame.
*/
public void setOutputSize(int width, int height) {
handler.obtainMessage(MSG_SET_OUTPUT, width, height).sendToTarget();
}
/** Releases all kept resources. This instance cannot be used after this call. */
public synchronized void release() {
if (released) {
return;
}
// Some long running or waiting operations may run on the handler thread, so we try to
// interrupt the thread to end these operations quickly.
handlerThread.interrupt();
handler.removeCallbacksAndMessages(null);
handler.sendEmptyMessage(MSG_RELEASE);
boolean wasInterrupted = false;
while (!released) {
try {
wait();
} catch (InterruptedException e) {
wasInterrupted = true;
}
}
if (wasInterrupted) {
// Restore the interrupted status.
Thread.currentThread().interrupt();
}
}
// Handler.Callback
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SET_OUTPUT:
handleSetOutput(/* width= */ message.arg1, /* height= */ message.arg2);
return true;
case MSG_RELEASE:
handleRelease();
return true;
default:
return false;
}
}
// Internal methods
private void handleSetOutput(int width, int height) {
if (surfaceCapturer == null
|| surfaceCapturer.getOutputWidth() != width
|| surfaceCapturer.getOutputHeight() != height) {
updateSurfaceCapturer(width, height);
}
eventDispatcher.onOutputSizeSet(width, height);
}
private void updateSurfaceCapturer(int width, int height) {
SurfaceCapturer oldSurfaceCapturer = surfaceCapturer;
if (oldSurfaceCapturer != null) {
blockingSetRendererSurface(/* surface= */ null);
oldSurfaceCapturer.release();
}
surfaceCapturer = createSurfaceCapturer(width, height);
blockingSetRendererSurface(surfaceCapturer.getSurface());
}
private SurfaceCapturer createSurfaceCapturer(int width, int height) {
if (Util.SDK_INT >= 24) {
return createSurfaceCapturerV24(width, height);
} else {
// TODO: Use different SurfaceCapturer based on API level, flags etc...
throw new UnsupportedOperationException(
"Creating Surface Capturer is not supported for API < 24 yet");
}
}
@RequiresApi(24)
private SurfaceCapturer createSurfaceCapturerV24(int width, int height) {
return new PixelCopySurfaceCapturerV24(eventDispatcher, width, height, handler);
}
private void blockingSetRendererSurface(@Nullable Surface surface) {
try {
exoPlayer
.createMessage(renderer)
.setType(Renderer.MSG_SET_SURFACE)
.setPayload(surface)
.send()
.blockUntilDelivered();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void handleRelease() {
eventDispatcher.release();
handler.removeCallbacksAndMessages(null);
if (surfaceCapturer != null) {
surfaceCapturer.release();
}
handlerThread.quit();
synchronized (this) {
released = true;
notifyAll();
}
}
/** Dispatches {@link Callback} events using a callback handler. */
private static final class EventDispatcher implements Callback {
private final Handler callbackHandler;
private final Callback callback;
private volatile boolean released;
private EventDispatcher(Handler callbackHandler, Callback callback) {
this.callbackHandler = callbackHandler;
this.callback = callback;
}
@Override
public void onOutputSizeSet(int width, int height) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onOutputSizeSet(width, height);
});
}
@Override
public void onSurfaceCaptured(Bitmap bitmap) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onSurfaceCaptured(bitmap);
});
}
@Override
public void onSurfaceCaptureError(Exception exception) {
callbackHandler.post(
() -> {
if (released) {
return;
}
callback.onSurfaceCaptureError(exception);
});
}
/** Releases this event dispatcher. No event will be dispatched after this call. */
public void release() {
released = true;
}
}
}

View File

@ -1,19 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NonNullApi
package com.google.android.exoplayer2.video.surfacecapturer;
import com.google.android.exoplayer2.util.NonNullApi;