Index: webrtc/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java |
diff --git a/webrtc/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java b/webrtc/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java |
index 5e8725777c5145ccdfce786a263b884278fe8653..d045276cc45225737529fe17090c1e31a3c60cb0 100644 |
--- a/webrtc/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java |
+++ b/webrtc/sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java |
@@ -11,10 +11,13 @@ |
package org.webrtc; |
import android.annotation.TargetApi; |
+import android.graphics.Matrix; |
import android.media.MediaCodec; |
import android.media.MediaCodecInfo; |
import android.media.MediaFormat; |
+import android.opengl.GLES20; |
import android.os.Bundle; |
+import android.view.Surface; |
import java.io.IOException; |
import java.nio.ByteBuffer; |
import java.util.Arrays; |
@@ -73,6 +76,12 @@ class HardwareVideoEncoder implements VideoEncoder { |
// value to send exceptions thrown during release back to the encoder thread. |
private volatile Exception shutdownException = null; |
+ // Surface objects for texture-mode encoding. |
pthatcher1
2017/07/17 22:50:19
Would it make sense to write a comment explaining
mellem
2017/07/17 23:22:00
Done.
|
+ private EglBase14.Context sharedContext; |
+ private EglBase14 eglBase; |
+ private Surface inputSurface; |
+ private GlRectDrawer drawer; |
pthatcher1
2017/07/17 22:50:19
It would be nice if these had a common name that m
mellem
2017/07/17 23:22:01
Done.
|
+ |
private MediaCodec codec; |
private Callback callback; |
@@ -97,15 +106,21 @@ class HardwareVideoEncoder implements VideoEncoder { |
* @throws IllegalArgumentException if colorFormat is unsupported |
*/ |
public HardwareVideoEncoder(String codecName, VideoCodecType codecType, int colorFormat, |
- int keyFrameIntervalSec, int forceKeyFrameIntervalMs, BitrateAdjuster bitrateAdjuster) { |
+ int keyFrameIntervalSec, int forceKeyFrameIntervalMs, BitrateAdjuster bitrateAdjuster, |
+ EglBase14.Context sharedContext) { |
this.codecName = codecName; |
this.codecType = codecType; |
this.colorFormat = colorFormat; |
- this.inputColorFormat = ColorFormat.valueOf(colorFormat); |
+ if (sharedContext == null) { |
+ this.inputColorFormat = ColorFormat.valueOf(colorFormat); |
+ } else { |
+ this.inputColorFormat = null; |
pthatcher1
2017/07/17 22:50:19
Can you write a comment explaining why the inputCo
mellem
2017/07/17 23:22:00
Done.
|
+ } |
this.keyFrameIntervalSec = keyFrameIntervalSec; |
this.forcedKeyFrameMs = forceKeyFrameIntervalMs; |
this.bitrateAdjuster = bitrateAdjuster; |
this.outputBuilders = new LinkedBlockingDeque<>(); |
+ this.sharedContext = sharedContext; |
} |
@Override |
@@ -144,6 +159,15 @@ class HardwareVideoEncoder implements VideoEncoder { |
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyFrameIntervalSec); |
Logging.d(TAG, "Format: " + format); |
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
+ |
+ if (sharedContext != null) { |
+ // Surface mode. |
pthatcher1
2017/07/17 22:50:19
Above it was called "texture mode", not it says "s
mellem
2017/07/17 23:22:00
Done.
|
+ eglBase = new EglBase14(sharedContext, EglBase.CONFIG_RECORDABLE); |
+ inputSurface = codec.createInputSurface(); |
+ eglBase.createSurface(inputSurface); |
+ drawer = new GlRectDrawer(); |
+ } |
+ |
codec.start(); |
} catch (IllegalStateException e) { |
Logging.e(TAG, "initEncode failed", e); |
@@ -161,6 +185,9 @@ class HardwareVideoEncoder implements VideoEncoder { |
@Override |
public VideoCodecStatus release() { |
try { |
+ if (outputThread == null) { |
+ return VideoCodecStatus.OK; |
+ } |
pthatcher1
2017/07/17 22:50:19
Is this only true when we're in texture mode? If
mellem
2017/07/17 23:22:00
No, this is just a fix for a bug that I whacked.
|
// The outputThread actually stops and releases the codec once running is false. |
running = false; |
if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) { |
@@ -176,6 +203,19 @@ class HardwareVideoEncoder implements VideoEncoder { |
codec = null; |
outputThread = null; |
outputBuilders.clear(); |
+ |
+ if (drawer != null) { |
+ drawer.release(); |
+ drawer = null; |
+ } |
+ if (eglBase != null) { |
+ eglBase.release(); |
+ eglBase = null; |
+ } |
+ if (inputSurface != null) { |
+ inputSurface.release(); |
+ inputSurface = null; |
+ } |
pthatcher1
2017/07/17 22:50:19
Would it make sense to put the 4 texture things in
mellem
2017/07/17 23:22:01
It's possible to fail init partway through allocat
|
} |
return VideoCodecStatus.OK; |
} |
@@ -196,37 +236,12 @@ class HardwareVideoEncoder implements VideoEncoder { |
} |
} |
- // No timeout. Don't block for an input buffer, drop frames if the encoder falls behind. |
- int index; |
- try { |
- index = codec.dequeueInputBuffer(0 /* timeout */); |
- } catch (IllegalStateException e) { |
- Logging.e(TAG, "dequeueInputBuffer failed", e); |
- return VideoCodecStatus.FALLBACK_SOFTWARE; |
- } |
- |
- if (index == -1) { |
- // Encoder is falling behind. No input buffers available. Drop the frame. |
- Logging.e(TAG, "Dropped frame, no input buffers available"); |
- return VideoCodecStatus.OK; // See webrtc bug 2887. |
- } |
if (outputBuilders.size() > MAX_ENCODER_Q_SIZE) { |
// Too many frames in the encoder. Drop this frame. |
Logging.e(TAG, "Dropped frame, encoder queue full"); |
return VideoCodecStatus.OK; // See webrtc bug 2887. |
} |
- // TODO(mellem): Add support for input surfaces and textures. |
- ByteBuffer buffer; |
- try { |
- buffer = codec.getInputBuffers()[index]; |
- } catch (IllegalStateException e) { |
- Logging.e(TAG, "getInputBuffers failed", e); |
- return VideoCodecStatus.FALLBACK_SOFTWARE; |
- } |
- VideoFrame.I420Buffer i420 = videoFrame.getBuffer().toI420(); |
- inputColorFormat.fillBufferFromI420(buffer, i420); |
- |
boolean requestedKeyFrame = false; |
for (EncodedImage.FrameType frameType : encodeInfo.frameTypes) { |
if (frameType == EncodedImage.FrameType.VideoFrameKey) { |
@@ -241,9 +256,10 @@ class HardwareVideoEncoder implements VideoEncoder { |
requestKeyFrame(presentationTimestampMs); |
} |
+ VideoFrame.Buffer videoFrameBuffer = videoFrame.getBuffer(); |
// Number of bytes in the video buffer. Y channel is sampled at one byte per pixel; U and V are |
// subsampled at one byte per four pixels. |
- int bufferSize = videoFrame.getBuffer().getHeight() * videoFrame.getBuffer().getWidth() * 3 / 2; |
+ int bufferSize = videoFrameBuffer.getHeight() * videoFrameBuffer.getWidth() * 3 / 2; |
EncodedImage.Builder builder = EncodedImage.builder() |
.setCaptureTimeMs(presentationTimestampMs) |
.setCompleteFrame(true) |
@@ -251,17 +267,89 @@ class HardwareVideoEncoder implements VideoEncoder { |
.setEncodedHeight(videoFrame.getHeight()) |
.setRotation(videoFrame.getRotation()); |
outputBuilders.offer(builder); |
- try { |
- codec.queueInputBuffer( |
- index, 0 /* offset */, bufferSize, presentationTimestampUs, 0 /* flags */); |
- } catch (IllegalStateException e) { |
- Logging.e(TAG, "queueInputBuffer failed", e); |
- // Keep the output builders in sync with buffers in the codec. |
- outputBuilders.pollLast(); |
- // IllegalStateException thrown when the codec is in the wrong state. |
- return VideoCodecStatus.FALLBACK_SOFTWARE; |
+ |
+ if (videoFrameBuffer instanceof VideoFrame.TextureBuffer) { |
sakal
2017/07/17 12:25:41
nit: I would prefer these cases in separate method
mellem
2017/07/17 17:49:29
Done.
|
+ VideoFrame.TextureBuffer textureBuffer = (VideoFrame.TextureBuffer) videoFrameBuffer; |
+ |
+ // TODO(mellem): Put this matrix manipulation in a helper. |
sakal
2017/07/17 12:25:41
I have implemented such helper here https://chromi
mellem
2017/07/17 17:49:30
Done. Thanks for writing that.
|
+ // The android.graphics.Matrix looks like this: |
+ // [x1 y1 w1] |
+ // [x2 y2 w2] |
+ // [x3 y3 w3] |
+ // We want to contruct a matrix that looks like this: |
+ // [x1 y1 0 w1] |
+ // [x2 y2 0 w2] |
+ // [ 0 0 1 0] |
+ // [x3 y3 0 w3] |
+ Matrix matrix = videoFrame.getTransformMatrix(); |
+ float[] matrix3x3 = new float[9]; |
+ matrix.getValues(matrix3x3); |
+ |
+ float[] transformationMatrix = new float[16]; |
+ transformationMatrix[0 * 4 + 0] = matrix3x3[0 * 3 + 0]; |
+ transformationMatrix[0 * 4 + 1] = matrix3x3[0 * 3 + 1]; |
+ transformationMatrix[0 * 4 + 3] = matrix3x3[0 * 3 + 2]; |
+ transformationMatrix[1 * 4 + 0] = matrix3x3[1 * 3 + 0]; |
+ transformationMatrix[1 * 4 + 1] = matrix3x3[1 * 3 + 1]; |
+ transformationMatrix[1 * 4 + 3] = matrix3x3[1 * 3 + 2]; |
+ transformationMatrix[2 * 4 + 2] = 1; // Z-scale should be 1. |
+ transformationMatrix[3 * 4 + 0] = matrix3x3[2 * 3 + 0]; |
+ transformationMatrix[3 * 4 + 1] = matrix3x3[2 * 3 + 1]; |
+ transformationMatrix[3 * 4 + 3] = matrix3x3[2 * 3 + 2]; |
+ |
+ try { |
+ eglBase.makeCurrent(); |
+ // TODO(perkj): glClear() shouldn't be necessary since every pixel is covered anyway, |
+ // but it's a workaround for bug webrtc:5147. |
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); |
+ drawer.drawOes( |
sakal
2017/07/17 12:25:42
nit: TextureFrame might be RGB frame in the future
mellem
2017/07/17 17:49:29
Done.
|
+ textureBuffer.getTextureId(), transformationMatrix, width, height, 0, 0, width, height); |
+ eglBase.swapBuffers(videoFrame.getTimestampNs()); |
+ } catch (RuntimeException e) { |
+ Logging.e(TAG, "encodeTexture failed", e); |
+ // Keep the output builders in sync with buffers in the codec. |
+ outputBuilders.pollLast(); |
+ return VideoCodecStatus.ERROR; |
+ } |
+ return VideoCodecStatus.OK; |
+ } else { |
+ // No timeout. Don't block for an input buffer, drop frames if the encoder falls behind. |
sakal
2017/07/17 12:25:42
Can we at least log a clear error if the mode does
mellem
2017/07/17 17:49:30
Logging added. We can actually process a texture
sakal
2017/07/18 08:46:13
We reconfigure the encoder before we hit that chec
mellem
2017/07/18 17:11:57
Acknowledged.
|
+ int index; |
+ try { |
+ index = codec.dequeueInputBuffer(0 /* timeout */); |
+ } catch (IllegalStateException e) { |
+ Logging.e(TAG, "dequeueInputBuffer failed", e); |
+ return VideoCodecStatus.FALLBACK_SOFTWARE; |
sakal
2017/07/17 12:25:42
Can you just replace all FALLBACK_SOTWAREs with ER
mellem
2017/07/17 17:49:29
Done.
|
+ } |
+ |
+ if (index == -1) { |
+ // Encoder is falling behind. No input buffers available. Drop the frame. |
+ Logging.e(TAG, "Dropped frame, no input buffers available"); |
+ return VideoCodecStatus.OK; // See webrtc bug 2887. |
+ } |
+ |
+ ByteBuffer buffer; |
+ try { |
+ buffer = codec.getInputBuffers()[index]; |
+ } catch (IllegalStateException e) { |
+ Logging.e(TAG, "getInputBuffers failed", e); |
+ return VideoCodecStatus.FALLBACK_SOFTWARE; |
+ } |
+ VideoFrame.I420Buffer i420 = videoFrameBuffer.toI420(); |
sakal
2017/07/17 12:25:41
toI420 will return "a new instance". Therefore, we
mellem
2017/07/17 17:49:29
Done.
|
+ inputColorFormat.fillBufferFromI420(buffer, i420); |
+ |
+ try { |
+ codec.queueInputBuffer( |
+ index, 0 /* offset */, bufferSize, presentationTimestampUs, 0 /* flags */); |
+ } catch (IllegalStateException e) { |
+ Logging.e(TAG, "queueInputBuffer failed", e); |
+ // Keep the output builders in sync with buffers in the codec. |
+ outputBuilders.pollLast(); |
+ // IllegalStateException thrown when the codec is in the wrong state. |
+ return VideoCodecStatus.FALLBACK_SOFTWARE; |
+ } |
+ return VideoCodecStatus.OK; |
} |
- return VideoCodecStatus.OK; |
} |
@Override |