diff --git a/library/core/build.gradle b/library/core/build.gradle index 5bf70ad151..3196735378 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -21,7 +21,11 @@ android { testInstrumentationRunnerArguments clearPackageData: 'true' multiDexEnabled true } - + testOptions{ + unitTests.all { + jvmArgs '-noverify' + } + } buildTypes { debug { testCoverageEnabled = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java index 08c2e51e49..31dba8ef3e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRenderer.java @@ -3,13 +3,15 @@ package com.google.android.exoplayer2.video; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; -import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; import android.os.SystemClock; import android.view.Surface; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; +import androidx.arch.core.util.Function; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -23,11 +25,16 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.nio.ByteBuffer; public class BitmapFactoryVideoRenderer extends BaseRenderer { - private static final String TAG = "BitmapFactoryRenderer"; + static final String TAG = "BitmapFactoryRenderer"; + + //Sleep Reasons + static final String STREAM_END = "Stream End"; + static final String STREAM_EMPTY = "Stream Empty"; + static final String RENDER_WAIT = "Render Wait"; + private static int threadId; private final Rect rect = new Rect(); - private final Point lastSurface = new Point(); private final RenderRunnable renderRunnable = new RenderRunnable(); final VideoRendererEventListener.EventDispatcher eventDispatcher; @@ -60,7 +67,16 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { throws ExoPlaybackException { decoderCounters = new DecoderCounters(); eventDispatcher.enabled(decoderCounters); - thread.start(); + if (mayRenderStartOfStream) { + thread.start(); + } + } + + @Override + protected void onStarted() throws ExoPlaybackException { + if (thread.getState() == Thread.State.NEW) { + thread.start(); + } } @Override @@ -74,20 +90,12 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } } - private void onFormatChanged(@NonNull FormatHolder formatHolder) { - @Nullable final Format format = formatHolder.format; - if (format != null) { - frameUs = (long)(1_000_000L / format.frameRate); - eventDispatcher.inputFormatChanged(format, null); - } - } - @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { //Log.d(TAG, "Render: us=" + positionUs); - synchronized (eventDispatcher) { + synchronized (renderRunnable) { currentTimeUs = positionUs; - eventDispatcher.notify(); + renderRunnable.notify(); } } @@ -127,7 +135,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); } - void renderBitmap(final Bitmap bitmap) { + @WorkerThread + private void onFormatChanged(@NonNull FormatHolder formatHolder) { + @Nullable final Format format = formatHolder.format; + if (format != null) { + frameUs = (long)(1_000_000L / format.frameRate); + eventDispatcher.inputFormatChanged(format, null); + } + } + + @WorkerThread + void renderBitmap(@NonNull final Bitmap bitmap) { @Nullable final Surface surface = this.surface; if (surface == null) { @@ -136,30 +154,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); final Canvas canvas = surface.lockCanvas(null); - final Rect clipBounds = canvas.getClipBounds(); - final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); - final boolean videoSizeChanged; - if (videoSize.equals(lastVideoSize)) { - videoSizeChanged = false; - } else { - lastVideoSize = videoSize; - eventDispatcher.videoSizeChanged(videoSize); - videoSizeChanged = true; - } - if (lastSurface.x != clipBounds.width() || lastSurface.y != clipBounds.height() || - videoSizeChanged) { - lastSurface.x = clipBounds.width(); - lastSurface.y = clipBounds.height(); - final float scaleX = lastSurface.x / (float)videoSize.width; - final float scaleY = lastSurface.y / (float)videoSize.height; - final float scale = Math.min(scaleX, scaleY); - final float width = videoSize.width * scale; - final float height = videoSize.height * scale; - final int x = (int)(lastSurface.x - width) / 2; - final int y = (int)(lastSurface.y - height) / 2; - rect.set(x, y, x + (int)width, y + (int) height); - } - canvas.drawBitmap(bitmap, null, rect, null); + renderBitmap(bitmap, canvas); surface.unlockCanvasAndPost(canvas); @Nullable @@ -173,12 +168,27 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { } } - class RenderRunnable implements Runnable { + @WorkerThread + @VisibleForTesting + void renderBitmap(Bitmap bitmap, Canvas canvas) { + final VideoSize videoSize = new VideoSize(bitmap.getWidth(), bitmap.getHeight()); + if (!videoSize.equals(lastVideoSize)) { + lastVideoSize = videoSize; + eventDispatcher.videoSizeChanged(videoSize); + } + rect.set(0,0,canvas.getWidth(), canvas.getHeight()); + canvas.drawBitmap(bitmap, null, rect, null); + } + + class RenderRunnable implements Runnable, Function { final DecoderInputBuffer decoderInputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); private volatile boolean running = true; + @VisibleForTesting + Function sleepFunction = this; + void stop() { running = false; thread.interrupt(); @@ -197,7 +207,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.arrayOffset() + byteBuffer.position()); if (bitmap == null) { - eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); + throw new NullPointerException("Decode bytes failed"); } else { return bitmap; } @@ -212,18 +222,21 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { * * @return true if interrupted */ - private boolean sleep() { - synchronized (eventDispatcher) { - try { - eventDispatcher.wait(); - return false; - } catch (InterruptedException e) { - //If we are interrupted, treat as a cancel - return true; - } + public synchronized Boolean apply(String why) { + try { + wait(); + return false; + } catch (InterruptedException e) { + //If we are interrupted, treat as a cancel + return true; } } + private boolean sleep(String why) { + return sleepFunction.apply(why); + } + + @WorkerThread public void run() { final FormatHolder formatHolder = getFormatHolder(); long start = SystemClock.uptimeMillis(); @@ -232,40 +245,73 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { decoderInputBuffer.clear(); final int result = readSource(formatHolder, decoderInputBuffer, formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); - if (result == C.RESULT_BUFFER_READ) { - if (decoderInputBuffer.isEndOfStream()) { - //Wait for shutdown or stream to be changed - sleep(); - continue; - } - final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; - //If we are more than 1/2 a frame behind, skip the next frame - if (leadUs < -frameUs / 2) { - eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); + switch (result) { + case C.RESULT_BUFFER_READ: { + if (decoderInputBuffer.isEndOfStream()) { + //Wait for shutdown or stream to be changed + sleep(STREAM_END); + continue; + } + final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; + //If we are more than 1/2 a frame behind, skip the next frame + if (leadUs < -frameUs / 2) { + eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start); + start = SystemClock.uptimeMillis(); + continue; + } start = SystemClock.uptimeMillis(); - continue; - } - start = SystemClock.uptimeMillis(); - @Nullable - final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); - if (bitmap == null) { - continue; - } - while (currentTimeUs < decoderInputBuffer.timeUs) { - //Log.d(TAG, "Sleep: us=" + currentTimeUs); - if (sleep()) { - //Sleep was interrupted, discard Bitmap - continue main; + @Nullable + final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer); + if (bitmap == null) { + continue; + } + while (currentTimeUs < decoderInputBuffer.timeUs) { + //Log.d(TAG, "Sleep: us=" + currentTimeUs); + if (sleep(RENDER_WAIT)) { + //Sleep was interrupted, discard Bitmap + continue main; + } + } + if (running) { + renderBitmap(bitmap); } } - if (running) { - renderBitmap(bitmap); - } - } else if (result == C.RESULT_FORMAT_READ) { + break; + case C.RESULT_FORMAT_READ: onFormatChanged(formatHolder); + break; + case C.RESULT_NOTHING_READ: + sleep(STREAM_EMPTY); + break; } } } } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Rect getRect() { + return rect; + } + + @Nullable + @VisibleForTesting + DecoderCounters getDecoderCounters() { + return decoderCounters; + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Thread getThread() { + return thread; + } + + @Nullable + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + Surface getSurface() { + return surface; + } + + RenderRunnable getRenderRunnable() { + return renderRunnable; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java new file mode 100644 index 0000000000..914f59786b --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/BitmapFactoryVideoRendererTest.java @@ -0,0 +1,237 @@ +package com.google.android.exoplayer2.video; + +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; +import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.sample; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; +import androidx.arch.core.util.Function; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.PlaybackException; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.drm.DrmSessionEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DefaultAllocator; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowBitmapFactory; +import org.robolectric.shadows.ShadowLooper; + +@RunWith(AndroidJUnit4.class) +@Config(shadows = {ShadowSurfaceExtended.class}) +public class BitmapFactoryVideoRendererTest { + private final static Format FORMAT_MJPEG = new Format.Builder(). + setSampleMimeType(MimeTypes.VIDEO_MJPEG). + setWidth(320).setHeight(240). + setFrameRate(15f).build(); + + FakeEventListener fakeEventListener = new FakeEventListener(); + BitmapFactoryVideoRenderer bitmapFactoryVideoRenderer; + + @Before + public void before() { + fakeEventListener = new FakeEventListener(); + final Handler handler = new Handler(Looper.getMainLooper()); + bitmapFactoryVideoRenderer = new BitmapFactoryVideoRenderer(handler, fakeEventListener); + } + + @After + public void after() { + //Kill the Thread + bitmapFactoryVideoRenderer.onDisabled(); + } + + @Test + public void getName() { + Assert.assertEquals(BitmapFactoryVideoRenderer.TAG, bitmapFactoryVideoRenderer.getName()); + } + + @Test + public void onEnabled_givenMayRenderStartOfStream() throws PlaybackException { + bitmapFactoryVideoRenderer.onEnabled(false, true); + ShadowLooper.idleMainLooper(); + Assert.assertNotNull(bitmapFactoryVideoRenderer.getDecoderCounters()); + Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); + Assert.assertTrue(fakeEventListener.isVideoEnabled()); + } + + @Test + public void onStarted_givenThreadNotStarted() throws PlaybackException { + bitmapFactoryVideoRenderer.onStarted(); + ShadowLooper.idleMainLooper(); + Assert.assertEquals(Thread.State.RUNNABLE, bitmapFactoryVideoRenderer.getThread().getState()); + } + + @Test + public void onDisabled_givenOnEnabled() throws PlaybackException, InterruptedException { + onEnabled_givenMayRenderStartOfStream(); + bitmapFactoryVideoRenderer.onDisabled(); + ShadowLooper.idleMainLooper(); + Assert.assertFalse(fakeEventListener.isVideoEnabled()); + //Ensure Thread is shutdown + bitmapFactoryVideoRenderer.getThread().join(500L); + Assert.assertTrue(bitmapFactoryVideoRenderer.isEnded()); + } + + private FakeSampleStream getSampleStream() throws IOException { + final Context context = ApplicationProvider.getApplicationContext(); + final byte[] bytes = TestUtil.getByteArray(context, "media/jpeg/image-320-240.jpg"); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ FORMAT_MJPEG, + ImmutableList.of( + sample(0L, C.BUFFER_FLAG_KEY_FRAME, bytes), + END_OF_STREAM_ITEM)); + return fakeSampleStream; + } + + private Surface setSurface() throws ExoPlaybackException { + final Surface surface = ShadowSurfaceExtended.newInstance(); + final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); + shadowSurfaceExtended.setSize(1080, 1920); + bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + return surface; + } + + @Test + public void handleMessage_givenSurface() throws ExoPlaybackException { + final Surface surface = setSurface(); + Assert.assertSame(surface, bitmapFactoryVideoRenderer.getSurface()); + bitmapFactoryVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, null); + Assert.assertNull(bitmapFactoryVideoRenderer.getSurface()); + } + + @Test + public void isReady_givenSurface() throws ExoPlaybackException { + Assert.assertFalse(bitmapFactoryVideoRenderer.isReady()); + setSurface(); + Assert.assertTrue(bitmapFactoryVideoRenderer.isReady()); + } + + @Test + public void render_givenJpegAndSurface() throws IOException, ExoPlaybackException { + final Surface surface = setSurface(); + final ShadowSurfaceExtended shadowSurfaceExtended = Shadow.extract(surface); + + FakeSampleStream fakeSampleStream = getSampleStream(); + fakeSampleStream.writeData(0L); + bitmapFactoryVideoRenderer.enable(RendererConfiguration.DEFAULT, new Format[]{FORMAT_MJPEG}, + fakeSampleStream, 0L, false, true, 0L, 0L); + bitmapFactoryVideoRenderer.render(0L, 0L); + // This test actually decodes the JPEG (very cool!), + // May need to bump up timers for slow machines + Assert.assertTrue(shadowSurfaceExtended.waitForPost(500L)); + } + + @Test + public void supportsFormat_givenMjpegFormat() throws ExoPlaybackException{ + Assert.assertEquals(C.FORMAT_HANDLED, + bitmapFactoryVideoRenderer.supportsFormat(FORMAT_MJPEG) & C.FORMAT_HANDLED); + } + + @Test + public void supportsFormat_givenMp4vFormat() throws ExoPlaybackException{ + final Format format = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_MP4V).build(); + Assert.assertEquals(0, + bitmapFactoryVideoRenderer.supportsFormat(format) & C.FORMAT_HANDLED); + } + + @Test + public void renderBitmap_given4by3BitmapAnd16by9Canvas() { + final Bitmap bitmap = Bitmap.createBitmap(FORMAT_MJPEG.width, FORMAT_MJPEG.height, Bitmap.Config.ARGB_8888); + final Bitmap canvasBitmap = Bitmap.createBitmap(1080, 1920, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(canvasBitmap); + bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); + ShadowLooper.idleMainLooper(); + + final Rect rect = bitmapFactoryVideoRenderer.getRect(); + Assert.assertEquals(canvas.getWidth(), rect.width()); + Assert.assertEquals(canvas.getHeight(), rect.height()); + final VideoSize videoSize = fakeEventListener.videoSize; + Assert.assertEquals(bitmap.getWidth(), videoSize.width); + + bitmapFactoryVideoRenderer.renderBitmap(bitmap, canvas); + ShadowLooper.idleMainLooper(); + Assert.assertSame(videoSize, fakeEventListener.videoSize); + } + + @Test + public void RenderRunnable_run_givenLateFrame() throws IOException, ExoPlaybackException { + final Function sleep = why -> {throw new RuntimeException(why);}; + + FakeSampleStream fakeSampleStream = getSampleStream(); + fakeSampleStream.writeData(0L); + //Don't enable so the Thread is not running + bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); + BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = + bitmapFactoryVideoRenderer.getRenderRunnable(); + renderRunnable.sleepFunction = sleep; + bitmapFactoryVideoRenderer.render(1_000_000L, 0L); + try { + renderRunnable.run(); + } catch (RuntimeException e) { + Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); + } + ShadowLooper.idleMainLooper(); + Assert.assertEquals(1, fakeEventListener.getDroppedFrames()); + } + + @Test + public void RenderRunnable_run_givenBadJpeg() throws IOException, ExoPlaybackException { + final Function sleep = why -> {throw new RuntimeException(why);}; + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ FORMAT_MJPEG, + ImmutableList.of( + oneByteSample(0L, C.BUFFER_FLAG_KEY_FRAME), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(0L); + + //Don't enable so the Thread is not running + bitmapFactoryVideoRenderer.replaceStream(new Format[]{FORMAT_MJPEG}, fakeSampleStream, 0L, 0L); + BitmapFactoryVideoRenderer.RenderRunnable renderRunnable = + bitmapFactoryVideoRenderer.getRenderRunnable(); + renderRunnable.sleepFunction = sleep; + bitmapFactoryVideoRenderer.render(0L, 0L); + // There is a bug in Robolectric where it doesn't handle null images, + // so we won't get our Exception + ShadowBitmapFactory.setAllowInvalidImageData(false); + try { + renderRunnable.run(); + } catch (RuntimeException e) { + Assert.assertEquals(BitmapFactoryVideoRenderer.STREAM_EMPTY, e.getMessage()); + } + ShadowLooper.idleMainLooper(); + Assert.assertTrue(fakeEventListener.getVideoCodecError() instanceof NullPointerException); + + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java b/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java new file mode 100644 index 0000000000..ebc70e3f94 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/FakeEventListener.java @@ -0,0 +1,64 @@ +package com.google.android.exoplayer2.video; + +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.decoder.DecoderCounters; + +public class FakeEventListener implements VideoRendererEventListener { + @Nullable + VideoSize videoSize; + @Nullable + DecoderCounters decoderCounters; + + private long firstFrameRenderMs = Long.MIN_VALUE; + + private int droppedFrames; + + private Exception videoCodecError; + + @Override + public void onVideoSizeChanged(VideoSize videoSize) { + this.videoSize = videoSize; + } + + public boolean isVideoEnabled() { + return decoderCounters != null; + } + + @Override + public void onVideoEnabled(DecoderCounters counters) { + decoderCounters = counters; + } + + @Override + public void onVideoDisabled(DecoderCounters counters) { + decoderCounters = null; + } + + public long getFirstFrameRenderMs() { + return firstFrameRenderMs; + } + + @Override + public void onRenderedFirstFrame(Object output, long renderTimeMs) { + firstFrameRenderMs = renderTimeMs; + } + + public int getDroppedFrames() { + return droppedFrames; + } + + @Override + public void onDroppedFrames(int count, long elapsedMs) { + droppedFrames+=count; + } + + public Exception getVideoCodecError() { + return videoCodecError; + } + + @Override + public void onVideoCodecError(Exception videoCodecError) { + this.videoCodecError = videoCodecError; + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java b/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java new file mode 100644 index 0000000000..c107832310 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/ShadowSurfaceExtended.java @@ -0,0 +1,43 @@ +package com.google.android.exoplayer2.video; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.view.Surface; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import org.robolectric.annotation.Implements; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowSurface; + +@Implements(Surface.class) +public class ShadowSurfaceExtended extends ShadowSurface { + private final Semaphore postSemaphore = new Semaphore(0); + private int width; + private int height; + + public static Surface newInstance() { + return Shadow.newInstanceOf(Surface.class); + } + + public void setSize(final int width, final int height) { + this.width = width; + this.height = height; + } + + public Canvas lockCanvas(Rect canvas) { + return new Canvas(Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)); + } + + public void unlockCanvasAndPost(Canvas canvas) { + postSemaphore.release(); + } + + public boolean waitForPost(long millis) { + try { + return postSemaphore.tryAcquire(millis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java index f0826ce35b..3d92566d3c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/CanvasSubtitleOutput.java @@ -32,7 +32,8 @@ import java.util.List; * A {@link SubtitleView.Output} that uses Android's native layout framework via {@link * SubtitlePainter}. */ -/* package */ final class CanvasSubtitleOutput extends View implements SubtitleView.Output { +/* package */ final class +CanvasSubtitleOutput extends View implements SubtitleView.Output { private final List painters; diff --git a/testdata/src/test/assets/media/jpeg/image-320-240.jpg b/testdata/src/test/assets/media/jpeg/image-320-240.jpg new file mode 100644 index 0000000000..d1796c66fd Binary files /dev/null and b/testdata/src/test/assets/media/jpeg/image-320-240.jpg differ