Commit 1ff78292 by Dustin

Fix BitmapFactoryVideoRenderer sync issues

parent 1d85bf24
...@@ -6,6 +6,7 @@ import android.graphics.Canvas; ...@@ -6,6 +6,7 @@ import android.graphics.Canvas;
import android.graphics.Point; 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.view.Surface; import android.view.Surface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -20,27 +21,21 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; ...@@ -20,27 +21,21 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BitmapFactoryVideoRenderer extends BaseRenderer { public class BitmapFactoryVideoRenderer extends BaseRenderer {
private static final String TAG = "BitmapFactoryRenderer"; private static final String TAG = "BitmapFactoryRenderer";
final VideoRendererEventListener.EventDispatcher eventDispatcher; final VideoRendererEventListener.EventDispatcher eventDispatcher;
@Nullable @Nullable
Surface surface; volatile Surface surface;
private boolean firstFrameRendered;
private final Rect rect = new Rect(); private final Rect rect = new Rect();
private final Point lastSurface = new Point(); private final Point lastSurface = new Point();
private final RenderRunnable renderRunnable = new RenderRunnable();
private final Thread thread = new Thread(renderRunnable, "BitmapFactoryVideoRenderer");
private VideoSize lastVideoSize = VideoSize.UNKNOWN; private VideoSize lastVideoSize = VideoSize.UNKNOWN;
@Nullable
private ThreadPoolExecutor renderExecutor;
@Nullable
private Thread thread;
private long currentTimeUs; private long currentTimeUs;
private long nextFrameUs; private long frameUs;
private long frameUs = Long.MIN_VALUE; boolean ended;
private boolean ended; @Nullable
private DecoderCounters decoderCounters; private DecoderCounters decoderCounters;
public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler, public BitmapFactoryVideoRenderer(@Nullable Handler eventHandler,
...@@ -58,68 +53,43 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -58,68 +53,43 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
@Override @Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)
throws ExoPlaybackException { throws ExoPlaybackException {
firstFrameRendered = ended = false;
renderExecutor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
thread.start();
} }
@Override @Override
protected void onDisabled() { protected void onDisabled() {
renderExecutor.shutdownNow(); renderRunnable.running = false;
eventDispatcher.disabled(decoderCounters); thread.interrupt();
@Nullable
final DecoderCounters decoderCounters = this.decoderCounters;
if (decoderCounters != null) {
eventDispatcher.disabled(decoderCounters);
}
} }
private void onFormatChanged(@NonNull FormatHolder formatHolder) { private void onFormatChanged(@NonNull FormatHolder formatHolder) {
@Nullable final Format format = formatHolder.format; @Nullable final Format format = formatHolder.format;
if (format != null) { if (format != null) {
eventDispatcher.inputFormatChanged(format, null);
frameUs = (long)(1_000_000L / format.frameRate); 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);
synchronized (eventDispatcher) { synchronized (eventDispatcher) {
currentTimeUs = positionUs; currentTimeUs = positionUs;
eventDispatcher.notify(); eventDispatcher.notify();
} }
if (renderExecutor.getActiveCount() > 0) {
//Handle decoder overrun
if (positionUs > nextFrameUs) {
long us = (positionUs - nextFrameUs) + frameUs;
long dropped = us / frameUs;
eventDispatcher.droppedFrames((int)dropped, us);
nextFrameUs += frameUs * dropped;
}
return;
}
final FormatHolder formatHolder = getFormatHolder();
final DecoderInputBuffer decoderInputBuffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
final int result = readSource(formatHolder, decoderInputBuffer,
frameUs == Long.MIN_VALUE ? SampleStream.FLAG_REQUIRE_FORMAT : 0);
if (result == C.RESULT_BUFFER_READ) {
renderExecutor.execute(new RenderRunnable(decoderInputBuffer, nextFrameUs));
if (decoderInputBuffer.isEndOfStream()) {
ended = true;
} else {
nextFrameUs += frameUs;
}
} else if (result == C.RESULT_FORMAT_READ) {
onFormatChanged(formatHolder);
}
} }
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
nextFrameUs = positionUs; thread.interrupt();
@Nullable
final Thread thread = this.thread;
if (thread != null) {
thread.interrupt();
}
} }
@Override @Override
...@@ -141,7 +111,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -141,7 +111,7 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return ended && renderExecutor.getActiveCount() == 0; return renderRunnable.ended;
} }
@Override @Override
...@@ -154,96 +124,139 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer { ...@@ -154,96 +124,139 @@ public class BitmapFactoryVideoRenderer extends BaseRenderer {
} }
class RenderRunnable implements Runnable { class RenderRunnable implements Runnable {
@Nullable private volatile boolean ended;
private DecoderInputBuffer decoderInputBuffer; private boolean firstFrameRendered;
private final long renderUs; private volatile boolean running = true;
RenderRunnable(@NonNull final DecoderInputBuffer decoderInputBuffer, long renderUs) {
this.decoderInputBuffer = decoderInputBuffer;
this.renderUs = renderUs;
}
private boolean maybeDropFrame(long frameUs) { @Nullable
if (Math.abs(frameUs - currentTimeUs) > frameUs) { private Bitmap decodeInputBuffer(final DecoderInputBuffer decoderInputBuffer) {
eventDispatcher.droppedFrames(1, frameUs); @Nullable final ByteBuffer byteBuffer = decoderInputBuffer.data;
return true; if (byteBuffer != null) {
final Bitmap bitmap;
try {
bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(),
byteBuffer.arrayOffset() + byteBuffer.position());
if (bitmap == null) {
eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed"));
} else {
return bitmap;
}
} catch (Exception e) {
eventDispatcher.videoCodecError(e);
}
} }
return false; return null;
} }
public void run() { private void renderBitmap(final Bitmap bitmap, @NonNull final Surface surface) {
if (maybeDropFrame(renderUs)) { //Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight());
return; 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);
surface.unlockCanvasAndPost(canvas);
@Nullable @Nullable
final ByteBuffer byteBuffer = decoderInputBuffer.data; final DecoderCounters decoderCounters = BitmapFactoryVideoRenderer.this.decoderCounters;
@Nullable if (decoderCounters != null) {
final Surface surface = BitmapFactoryVideoRenderer.this.surface; decoderCounters.renderedOutputBufferCount++;
if (byteBuffer != null && surface != null) { }
final Bitmap bitmap; if (!firstFrameRendered) {
firstFrameRendered = true;
eventDispatcher.renderedFirstFrame(surface);
}
}
/**
*
* @return true if interrupted
*/
private boolean sleep() {
synchronized (eventDispatcher) {
try { try {
bitmap = BitmapFactory.decodeByteArray(byteBuffer.array(), byteBuffer.arrayOffset(), byteBuffer.arrayOffset() + byteBuffer.position()); eventDispatcher.wait();
} catch (Exception e) { return false;
eventDispatcher.videoCodecError(e); } catch (InterruptedException e) {
return; //If we are interrupted, treat as a cancel
return true;
} }
if (bitmap == null) { }
eventDispatcher.videoCodecError(new NullPointerException("Decode bytes failed")); }
return;
} public void run() {
decoderInputBuffer = null; final FormatHolder formatHolder = getFormatHolder();
//Wait for time to advance to display the Bitmap @NonNull
synchronized (eventDispatcher) { final DecoderInputBuffer decoderInputBuffer =
while (currentTimeUs < renderUs) { new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
try { long start = SystemClock.uptimeMillis();
thread = Thread.currentThread(); main:
eventDispatcher.wait(); while (running) {
} catch (InterruptedException e) { decoderInputBuffer.clear();
//If we are interrupted, treat as a cancel final int result = readSource(formatHolder, decoderInputBuffer,
return; formatHolder.format == null ? SampleStream.FLAG_REQUIRE_FORMAT : 0);
} finally { if (result == C.RESULT_BUFFER_READ) {
thread = null; if (decoderInputBuffer.isEndOfStream()) {
ended = true;
if (!sleep()) {
ended = false;
} }
continue;
} }
} final long leadUs = decoderInputBuffer.timeUs - currentTimeUs;
if (maybeDropFrame(renderUs)) { //If we are more than 1/2 a frame behind, skip the next frame
return; if (leadUs < -frameUs / 2) {
} eventDispatcher.droppedFrames(1, SystemClock.uptimeMillis() - start);
//Log.d(TAG, "Drawing: " + bitmap.getWidth() + "x" + bitmap.getHeight()); start = SystemClock.uptimeMillis();
final Canvas canvas = surface.lockCanvas(null); continue;
}
final Rect clipBounds = canvas.getClipBounds(); start = SystemClock.uptimeMillis();
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); @Nullable
decoderCounters.renderedOutputBufferCount++; final Bitmap bitmap = decodeInputBuffer(decoderInputBuffer);
if (!firstFrameRendered) { if (bitmap == null) {
firstFrameRendered = true; continue;
eventDispatcher.renderedFirstFrame(surface); }
while (currentTimeUs < decoderInputBuffer.timeUs) {
//Log.d(TAG, "Sleep: us=" + currentTimeUs);
if (sleep()) {
continue main;
}
if (!running) {
break main;
}
}
@Nullable
final Surface surface = BitmapFactoryVideoRenderer.this.surface;
if (surface != null) {
renderBitmap(bitmap, surface);
}
} else if (result == C.RESULT_FORMAT_READ) {
onFormatChanged(formatHolder);
} }
} }
ended = true;
} }
} }
} }
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