From 9cfe5fcf44337e3eeee52ba9326cfd3273aa722a Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 25 Sep 2014 20:29:44 +0100 Subject: [PATCH] API level 21 enhancements for ExoPlayer playbacks. - Use native frame release timing in video renderer for smoother video playback. - Avoid unnecessary memory copy steps in audio renderer. - Use non-blocking AudioTrack API. --- demo/build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 2 +- demo/src/main/project.properties | 2 +- library/build.gradle | 2 +- library/src/main/AndroidManifest.xml | 2 +- .../MediaCodecAudioTrackRenderer.java | 63 ++++---- .../MediaCodecVideoTrackRenderer.java | 151 +++++++++++------- library/src/main/project.properties | 2 +- 8 files changed, 133 insertions(+), 93 deletions(-) diff --git a/demo/build.gradle b/demo/build.gradle index a91c1ab2ca..c8db67dba5 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -19,7 +19,7 @@ android { defaultConfig { minSdkVersion 16 - targetSdkVersion 19 + targetSdkVersion 21 } buildTypes { release { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 8812dbc014..42075d9d8c 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -25,7 +25,7 @@ - + - + diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 8a75331465..cd4ed45292 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -563,12 +563,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onDisabled() { + super.onDisabled(); + releaseAudioTrack(); audioSessionId = 0; - try { - releaseAudioTrack(); - } finally { - super.onDisabled(); - } } @Override @@ -620,42 +617,52 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer { } } - // Copy {@code buffer} into {@code temporaryBuffer}. - // TODO: Bypass this copy step on versions of Android where [redacted] is implemented. - if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) { - temporaryBuffer = new byte[bufferInfo.size]; - } - buffer.position(bufferInfo.offset); - buffer.get(temporaryBuffer, 0, bufferInfo.size); - temporaryBufferOffset = 0; temporaryBufferSize = bufferInfo.size; + buffer.position(bufferInfo.offset); + if (Util.SDK_INT < 21) { + // Copy {@code buffer} into {@code temporaryBuffer}. + if (temporaryBuffer == null || temporaryBuffer.length < bufferInfo.size) { + temporaryBuffer = new byte[bufferInfo.size]; + } + buffer.get(temporaryBuffer, 0, bufferInfo.size); + temporaryBufferOffset = 0; + } } if (audioTrack == null) { initAudioTrack(); } - // TODO: Don't bother doing this once [redacted] is fixed. - // Work out how many bytes we can write without the risk of blocking. - int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize); - int bytesToWrite = bufferSize - bytesPending; - - if (bytesToWrite > 0) { - bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); - audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); - temporaryBufferOffset += bytesToWrite; - temporaryBufferSize -= bytesToWrite; - submittedBytes += bytesToWrite; - if (temporaryBufferSize == 0) { - codec.releaseOutputBuffer(bufferIndex, false); - codecCounters.renderedOutputBufferCount++; - return true; + int bytesWritten = 0; + if (Util.SDK_INT < 21) { + // Work out how many bytes we can write without the risk of blocking. + int bytesPending = (int) (submittedBytes - getPlaybackHeadPosition() * frameSize); + int bytesToWrite = bufferSize - bytesPending; + if (bytesToWrite > 0) { + bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite); + bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite); + temporaryBufferOffset += bytesWritten; } + } else { + bytesWritten = writeNonBlockingV21(audioTrack, buffer, temporaryBufferSize); + } + + temporaryBufferSize -= bytesWritten; + submittedBytes += bytesWritten; + if (temporaryBufferSize == 0) { + codec.releaseOutputBuffer(bufferIndex, false); + codecCounters.renderedOutputBufferCount++; + return true; } return false; } + @TargetApi(21) + private int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { + return audioTrack.write(buffer, size, AudioTrack.WRITE_NON_BLOCKING); + } + /** * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as * an unsigned 32 bit integer, which also wraps around periodically. This method returns the diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java index 565ab41723..3c9473585c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.drm.DrmSessionManager; import com.google.android.exoplayer.util.MimeTypes; import com.google.android.exoplayer.util.TraceUtil; +import com.google.android.exoplayer.util.Util; import android.annotation.TargetApi; import android.media.MediaCodec; @@ -93,7 +94,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { private final int maxDroppedFrameCountToNotify; private Surface surface; - private boolean drawnToSurface; + private boolean reportedDrawnToSurface; private boolean renderedFirstFrame; private long joiningDeadlineUs; private long droppedFrameAccumulationStartTimeMs; @@ -270,7 +271,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { @Override protected void onStopped() { joiningDeadlineUs = -1; - notifyAndResetDroppedFrameCount(); + maybeNotifyDroppedFrameCount(); super.onStopped(); } @@ -303,7 +304,7 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { return; } this.surface = surface; - this.drawnToSurface = false; + this.reportedDrawnToSurface = false; int state = getState(); if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) { releaseCodec(); @@ -369,24 +370,37 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { } if (!renderedFirstFrame) { - renderOutputBuffer(codec, bufferIndex); + renderOutputBufferImmediate(codec, bufferIndex); renderedFirstFrame = true; return true; } if (getState() == TrackRenderer.STATE_STARTED && earlyUs < 30000) { - if (earlyUs > 11000) { - // We're a little too early to render the frame. Sleep until the frame can be rendered. - // Note: The 11ms threshold was chosen fairly arbitrarily. - try { - // Subtracting 10000 rather than 11000 ensures that the sleep time will be at least 1ms. - Thread.sleep((earlyUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + if (Util.SDK_INT >= 21) { + // Let the underlying framework time the release. + if (earlyUs < 50000) { + renderOutputBufferTimedV21(codec, bufferIndex, System.nanoTime() + (earlyUs * 1000L)); + return true; } + return false; + } else { + // We need to time the release ourselves. + if (earlyUs < 30000) { + if (earlyUs > 11000) { + // We're a little too early to render the frame. Sleep until the frame can be rendered. + // Note: The 11ms threshold was chosen fairly arbitrarily. + try { + // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms. + Thread.sleep((earlyUs - 10000) / 1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + renderOutputBufferImmediate(codec, bufferIndex); + return true; + } + return false; } - renderOutputBuffer(codec, bufferIndex); - return true; } // We're either not playing, or it's not time to render the frame yet. @@ -407,65 +421,84 @@ public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { codecCounters.droppedOutputBufferCount++; droppedFrameCount++; if (droppedFrameCount == maxDroppedFrameCountToNotify) { - notifyAndResetDroppedFrameCount(); + maybeNotifyDroppedFrameCount(); } } - private void renderOutputBuffer(MediaCodec codec, int bufferIndex) { - if (lastReportedWidth != currentWidth || lastReportedHeight != currentHeight - || lastReportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; - notifyVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); - } - TraceUtil.beginSection("renderVideoBuffer"); + private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("renderVideoBufferImmediate"); codec.releaseOutputBuffer(bufferIndex, true); TraceUtil.endSection(); codecCounters.renderedOutputBufferCount++; - if (!drawnToSurface) { - drawnToSurface = true; - notifyDrawnToSurface(surface); - } + maybeNotifyDrawnToSurface(); } - private void notifyVideoSizeChanged(final int width, final int height, - final float pixelWidthHeightRatio) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); - } - }); - } + @TargetApi(21) + private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long nanoTime) { + maybeNotifyVideoSizeChanged(); + TraceUtil.beginSection("releaseOutputBufferTimed"); + codec.releaseOutputBuffer(bufferIndex, nanoTime); + TraceUtil.endSection(); + codecCounters.renderedOutputBufferCount++; + maybeNotifyDrawnToSurface(); } - private void notifyDrawnToSurface(final Surface surface) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrawnToSurface(surface); - } - }); + private void maybeNotifyVideoSizeChanged() { + if (eventHandler == null || eventListener == null + || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight + && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { + return; } + // Make final copies to ensure the runnable reports the correct values. + final int currentWidth = this.currentWidth; + final int currentHeight = this.currentHeight; + final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio); + } + }); + // Update the last reported values. + lastReportedWidth = currentWidth; + lastReportedHeight = currentHeight; + lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; } - private void notifyAndResetDroppedFrameCount() { - if (eventHandler != null && eventListener != null && droppedFrameCount > 0) { - long now = SystemClock.elapsedRealtime(); - final int countToNotify = droppedFrameCount; - final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = now; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDroppedFrames(countToNotify, elapsedToNotify); - } - }); + private void maybeNotifyDrawnToSurface() { + if (eventHandler == null || eventListener == null || reportedDrawnToSurface) { + return; } + // Make a final copy to ensure the runnable reports the correct surface. + final Surface surface = this.surface; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrawnToSurface(surface); + } + }); + // Record that we have reported that the surface has been drawn to. + reportedDrawnToSurface = true; + } + + private void maybeNotifyDroppedFrameCount() { + if (eventHandler == null || eventListener == null || droppedFrameCount == 0) { + return; + } + long now = SystemClock.elapsedRealtime(); + // Make final copies to ensure the runnable reports the correct values. + final int countToNotify = droppedFrameCount; + final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDroppedFrames(countToNotify, elapsedToNotify); + } + }); + // Reset the dropped frame tracking. + droppedFrameCount = 0; + droppedFrameAccumulationStartTimeMs = now; } } diff --git a/library/src/main/project.properties b/library/src/main/project.properties index 8e4bc5fdce..b756f4487f 100644 --- a/library/src/main/project.properties +++ b/library/src/main/project.properties @@ -8,5 +8,5 @@ # project structure. # Project target. -target=android-19 +target=android-21 android.library=true