mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
DataSources: Remove position-out-of-range workarounds
PiperOrigin-RevId: 364871094
This commit is contained in:
parent
2b0995635e
commit
3ebf94cd45
@ -31,6 +31,12 @@
|
|||||||
* Analytics:
|
* Analytics:
|
||||||
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
|
* Add `onAudioCodecError` and `onVideoCodecError` to `AnalyticsListener`.
|
||||||
* Downloads and caching:
|
* Downloads and caching:
|
||||||
|
* Fix `CacheWriter` to correctly handle cases where the request `DataSpec`
|
||||||
|
extends beyond the end of the underlying resource. Caching will now
|
||||||
|
succeed in this case, with data up to the end of the resource being
|
||||||
|
cached. This behaviour is enabled by default, and so the
|
||||||
|
`allowShortContent` parameter has been removed
|
||||||
|
([#7326](https://github.com/google/ExoPlayer/issues/7326)).
|
||||||
* Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
|
* Fix `CacheWriter` to correctly handle `DataSource.close` failures, for
|
||||||
which it cannot be assumed that data was successfully written to the
|
which it cannot be assumed that data was successfully written to the
|
||||||
cache.
|
cache.
|
||||||
|
@ -0,0 +1,428 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,301 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -630,22 +630,6 @@ public final class CacheDataSource implements DataSource {
|
|||||||
return read(buffer, offset, readLength);
|
return read(buffer, offset, readLength);
|
||||||
}
|
}
|
||||||
return bytesRead;
|
return bytesRead;
|
||||||
} catch (IOException e) {
|
|
||||||
// TODO: This is not correct, because position-out-of-range exceptions should only be thrown
|
|
||||||
// if the requested position is more than one byte beyond the end of the resource. Conversely,
|
|
||||||
// this code is assuming that a position-out-of-range exception indicates the requested
|
|
||||||
// position is exactly one byte beyond the end of the resource, which is not a case for which
|
|
||||||
// this type of exception should be thrown. This exception handling may be required for
|
|
||||||
// interop with current HttpDataSource implementations that do (incorrectly) throw a
|
|
||||||
// position-out-of-range exception at this position. It should be removed when the
|
|
||||||
// HttpDataSource implementations are fixed.
|
|
||||||
if (currentDataSpec.length == C.LENGTH_UNSET
|
|
||||||
&& DataSourceException.isCausedByPositionOutOfRange(e)) {
|
|
||||||
setNoBytesRemainingAndMaybeStoreLength(castNonNull(requestDataSpec.key));
|
|
||||||
return C.RESULT_END_OF_INPUT;
|
|
||||||
}
|
|
||||||
handleBeforeThrow(e);
|
|
||||||
throw e;
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
handleBeforeThrow(e);
|
handleBeforeThrow(e);
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.upstream.DataSourceException;
|
|
||||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||||
import com.google.android.exoplayer2.util.PriorityTaskManager.PriorityTooLowException;
|
import com.google.android.exoplayer2.util.PriorityTaskManager.PriorityTooLowException;
|
||||||
@ -160,17 +159,6 @@ public final class CacheWriter {
|
|||||||
isDataSourceOpen = true;
|
isDataSourceOpen = true;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Util.closeQuietly(dataSource);
|
Util.closeQuietly(dataSource);
|
||||||
// TODO: This exception handling may be required for interop with current HttpDataSource
|
|
||||||
// implementations that (incorrectly) throw a position-out-of-range when opened exactly one
|
|
||||||
// byte beyond the end of the resource. It should be removed when the HttpDataSource
|
|
||||||
// implementations are fixed.
|
|
||||||
if (isLastBlock && DataSourceException.isCausedByPositionOutOfRange(e)) {
|
|
||||||
// The length of the request exceeds the length of the content. If we allow shorter
|
|
||||||
// content and are reading the last block, fall through and try again with an unbounded
|
|
||||||
// request to read up to the end of the content.
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
/*
|
||||||
|
* 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
@ -0,0 +1,260 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* 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;
|
Loading…
x
Reference in New Issue
Block a user