Commit bec8b44b by Dustin

BitmapFactoryVideoRenderer Tests

parent df9e51de
...@@ -21,7 +21,11 @@ android { ...@@ -21,7 +21,11 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
multiDexEnabled true multiDexEnabled true
} }
testOptions{
unitTests.all {
jvmArgs '-noverify'
}
}
buildTypes { buildTypes {
debug { debug {
testCoverageEnabled = true testCoverageEnabled = true
......
...@@ -3,13 +3,15 @@ package com.google.android.exoplayer2.video; ...@@ -3,13 +3,15 @@ package com.google.android.exoplayer2.video;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; 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.BaseRenderer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
...@@ -23,11 +25,16 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -23,11 +25,16 @@ import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public class BitmapFactoryVideoRenderer extends BaseRenderer { 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 static int threadId;
private final Rect rect = new Rect(); private final Rect rect = new Rect();
private final Point lastSurface = new Point();
private final RenderRunnable renderRunnable = new RenderRunnable(); private final RenderRunnable renderRunnable = new RenderRunnable();
final VideoRendererEventListener.EventDispatcher eventDispatcher; final VideoRendererEventListener.EventDispatcher eventDispatcher;
...@@ -60,8 +67,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -60,8 +67,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
throws ExoPlaybackException { throws ExoPlaybackException {
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
if (mayRenderStartOfStream) {
thread.start(); thread.start();
} }
}
@Override
protected void onStarted() throws ExoPlaybackException {
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
@Override @Override
protected void onDisabled() { protected void onDisabled() {
...@@ -74,20 +90,12 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -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 @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
//Log.d(TAG, "Render: us=" + positionUs); //Log.d(TAG, "Render: us=" + positionUs);
synchronized (eventDispatcher) { synchronized (renderRunnable) {
currentTimeUs = positionUs; currentTimeUs = positionUs;
eventDispatcher.notify(); renderRunnable.notify();
} }
} }
...@@ -127,7 +135,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -127,7 +135,17 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE); 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 @Nullable
final Surface surface = this.surface; final Surface surface = this.surface;
if (surface == null) { if (surface == null) {
...@@ -136,30 +154,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -136,30 +154,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
//Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight());
final Canvas canvas = surface.lockCanvas(null); final Canvas canvas = surface.lockCanvas(null);
final Rect clipBounds = canvas.getClipBounds(); renderBitmap(bitmap, canvas);
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);
surface.unlockCanvasAndPost(canvas); surface.unlockCanvasAndPost(canvas);
@Nullable @Nullable
...@@ -173,12 +168,27 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -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<String, Boolean> {
final DecoderInputBuffer decoderInputBuffer = final DecoderInputBuffer decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
private volatile boolean running = true; private volatile boolean running = true;
@VisibleForTesting
Function<String, Boolean> sleepFunction = this;
void stop() { void stop() {
running = false; running = false;
thread.interrupt(); thread.interrupt();
...@@ -197,7 +207,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -197,7 +207,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(),
byteBuffer.arrayOffset() + byteBuffer.position()); byteBuffer.arrayOffset() + byteBuffer.position());
if (bitmap == null) { if (bitmap == null) {
eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); throw new NullPointerException("Decode bytes failed");
} else { } else {
return bitmap; return bitmap;
} }
...@@ -212,18 +222,21 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -212,18 +222,21 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
* *
* @return true if interrupted * @return true if interrupted
*/ */
private boolean sleep() { public synchronized Boolean apply(String why) {
synchronized (eventDispatcher) {
try { try {
eventDispatcher.wait(); wait();
return false; return false;
} catch (InterruptedException e) { } catch (InterruptedException e) {
//If we are interrupted, treat as a cancel //If we are interrupted, treat as a cancel
return true; return true;
} }
} }
private boolean sleep(String why) {
return sleepFunction.apply(why);
} }
@WorkerThread
public void run() { public void run() {
final FormatHolder formatHolder = getFormatHolder(); final FormatHolder formatHolder = getFormatHolder();
long start = SystemClock.uptimeMillis(); long start = SystemClock.uptimeMillis();
...@@ -232,10 +245,11 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -232,10 +245,11 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
decoderInputBuffer.clear(); decoderInputBuffer.clear();
final int result = readSource(formatHolder, decoderInputBuffer, final int result = readSource(formatHolder, decoderInputBuffer,
formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0); formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0);
if (result == C.RESULT_BUFFER_READ) { switch (result) {
case C.RESULT_BUFFER_READ: {
if (decoderInputBuffer.isEndOfStream()) { if (decoderInputBuffer.isEndOfStream()) {
//Wait for shutdown or stream to be changed //Wait for shutdown or stream to be changed
sleep(); sleep(STREAM_END);
continue; continue;
} }
final long leadUs = decoderInputBuffer.timeUs - currentTimeUs; final long leadUs = decoderInputBuffer.timeUs - currentTimeUs;
...@@ -254,7 +268,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -254,7 +268,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
} }
while (currentTimeUs < decoderInputBuffer.timeUs) { while (currentTimeUs < decoderInputBuffer.timeUs) {
//Log.d(TAG, "Sleep: us=" + currentTimeUs); //Log.d(TAG, "Sleep: us=" + currentTimeUs);
if (sleep()) { if (sleep(RENDER_WAIT)) {
//Sleep was interrupted, discard Bitmap //Sleep was interrupted, discard Bitmap
continue main; continue main;
} }
...@@ -262,10 +276,42 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -262,10 +276,42 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
if (running) { if (running) {
renderBitmap(bitmap); renderBitmap(bitmap);
} }
} else if (result == C.RESULT_FORMAT_READ) { }
break;
case C.RESULT_FORMAT_READ:
onFormatChanged(formatHolder); 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;
} }
} }
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;
}
}
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;
}
}
}
...@@ -32,7 +32,8 @@ import java.util.List; ...@@ -32,7 +32,8 @@ import java.util.List;
* A {@link SubtitleView.Output} that uses Android's native layout framework via {@link * A {@link SubtitleView.Output} that uses Android's native layout framework via {@link
* SubtitlePainter}. * SubtitlePainter}.
*/ */
/* package */ final class CanvasSubtitleOutput extends View implements SubtitleView.Output { /* package */ final class
CanvasSubtitleOutput extends View implements SubtitleView.Output {
private final List<SubtitlePainter> painters; private final List<SubtitlePainter> painters;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment