Android MediaCodecVideoDecoder: Manage lifetime of texture frames

This CL should be the last one in a series to finally unblock camera texture capture.

The SurfaceTexture.updateTexImage() calls are moved from the video renderers into MediaCodecVideoDecoder, and the destructor of the texture frames will signal MediaCodecVideoDecoder that the frame has returned. This CL also removes the SurfaceTexture from the native handle and only exposes the texture matrix instead, because only the video source should access the SurfaceTexture.

BUG=webrtc:4993
R=glaznev@webrtc.org, perkj@webrtc.org

Review URL: https://codereview.webrtc.org/1378033003 .

Cr-Commit-Position: refs/heads/master@{#10203}
This commit is contained in:
Magnus Jedvert
2015-10-07 22:57:06 +02:00
parent 87962a9787
commit 91b348c702
8 changed files with 163 additions and 166 deletions

View File

@ -389,25 +389,10 @@ public class SurfaceViewRenderer extends SurfaceView
} }
final long startTimeNs = System.nanoTime(); final long startTimeNs = System.nanoTime();
final float[] samplingMatrix;
if (frame.yuvFrame) {
// The convention in WebRTC is that the first element in a ByteBuffer corresponds to the
// top-left corner of the image, but in glTexImage2D() the first element corresponds to the
// bottom-left corner. We correct this discrepancy by setting a vertical flip as sampling
// matrix.
samplingMatrix = RendererCommon.verticalFlipMatrix();
} else {
// TODO(magjed): Move updateTexImage() to the video source instead.
SurfaceTexture surfaceTexture = (SurfaceTexture) frame.textureObject;
surfaceTexture.updateTexImage();
samplingMatrix = new float[16];
surfaceTexture.getTransformMatrix(samplingMatrix);
}
final float[] texMatrix; final float[] texMatrix;
synchronized (layoutLock) { synchronized (layoutLock) {
final float[] rotatedSamplingMatrix = final float[] rotatedSamplingMatrix =
RendererCommon.rotateTextureMatrix(samplingMatrix, frame.rotationDegree); RendererCommon.rotateTextureMatrix(frame.samplingMatrix, frame.rotationDegree);
final float[] layoutMatrix = RendererCommon.getLayoutMatrix( final float[] layoutMatrix = RendererCommon.getLayoutMatrix(
mirror, frameAspectRatio(), (float) layoutWidth / layoutHeight); mirror, frameAspectRatio(), (float) layoutWidth / layoutHeight);
texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutMatrix); texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutMatrix);

View File

@ -244,29 +244,15 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
} }
if (isNewFrame) { if (isNewFrame) {
rotatedSamplingMatrix = RendererCommon.rotateTextureMatrix(
pendingFrame.samplingMatrix, pendingFrame.rotationDegree);
if (pendingFrame.yuvFrame) { if (pendingFrame.yuvFrame) {
rendererType = RendererType.RENDERER_YUV; rendererType = RendererType.RENDERER_YUV;
drawer.uploadYuvData(yuvTextures, pendingFrame.width, pendingFrame.height, drawer.uploadYuvData(yuvTextures, pendingFrame.width, pendingFrame.height,
pendingFrame.yuvStrides, pendingFrame.yuvPlanes); pendingFrame.yuvStrides, pendingFrame.yuvPlanes);
// The convention in WebRTC is that the first element in a ByteBuffer corresponds to the
// top-left corner of the image, but in glTexImage2D() the first element corresponds to
// the bottom-left corner. We correct this discrepancy by setting a vertical flip as
// sampling matrix.
final float[] samplingMatrix = RendererCommon.verticalFlipMatrix();
rotatedSamplingMatrix =
RendererCommon.rotateTextureMatrix(samplingMatrix, pendingFrame.rotationDegree);
} else { } else {
rendererType = RendererType.RENDERER_TEXTURE; rendererType = RendererType.RENDERER_TEXTURE;
// External texture rendering. Update texture image to latest and make a deep copy of // External texture rendering. Make a deep copy of the external texture.
// the external texture.
// TODO(magjed): Move updateTexImage() to the video source instead.
final SurfaceTexture surfaceTexture = (SurfaceTexture) pendingFrame.textureObject;
surfaceTexture.updateTexImage();
final float[] samplingMatrix = new float[16];
surfaceTexture.getTransformMatrix(samplingMatrix);
rotatedSamplingMatrix =
RendererCommon.rotateTextureMatrix(samplingMatrix, pendingFrame.rotationDegree);
// Reallocate offscreen texture if necessary. // Reallocate offscreen texture if necessary.
textureCopy.setSize(pendingFrame.rotatedWidth(), pendingFrame.rotatedHeight()); textureCopy.setSize(pendingFrame.rotatedWidth(), pendingFrame.rotatedHeight());

View File

@ -32,6 +32,7 @@
#include "talk/app/webrtc/java/jni/androidmediacodeccommon.h" #include "talk/app/webrtc/java/jni/androidmediacodeccommon.h"
#include "talk/app/webrtc/java/jni/classreferenceholder.h" #include "talk/app/webrtc/java/jni/classreferenceholder.h"
#include "talk/app/webrtc/java/jni/native_handle_impl.h" #include "talk/app/webrtc/java/jni/native_handle_impl.h"
#include "talk/app/webrtc/java/jni/surfacetexturehelper_jni.h"
#include "webrtc/base/bind.h" #include "webrtc/base/bind.h"
#include "webrtc/base/checks.h" #include "webrtc/base/checks.h"
#include "webrtc/base/logging.h" #include "webrtc/base/logging.h"
@ -110,7 +111,7 @@ class MediaCodecVideoDecoder : public webrtc::VideoDecoder,
bool use_surface_; bool use_surface_;
VideoCodec codec_; VideoCodec codec_;
webrtc::I420BufferPool decoded_frame_pool_; webrtc::I420BufferPool decoded_frame_pool_;
NativeHandleImpl native_handle_; rtc::scoped_refptr<SurfaceTextureHelper> surface_texture_helper_;
DecodedImageCallback* callback_; DecodedImageCallback* callback_;
int frames_received_; // Number of frames received by decoder. int frames_received_; // Number of frames received by decoder.
int frames_decoded_; // Number of frames decoded by decoder. int frames_decoded_; // Number of frames decoded by decoder.
@ -143,10 +144,10 @@ class MediaCodecVideoDecoder : public webrtc::VideoDecoder,
jfieldID j_height_field_; jfieldID j_height_field_;
jfieldID j_stride_field_; jfieldID j_stride_field_;
jfieldID j_slice_height_field_; jfieldID j_slice_height_field_;
jfieldID j_surface_texture_field_;
// MediaCodecVideoDecoder.DecodedTextureBuffer fields. // MediaCodecVideoDecoder.DecodedTextureBuffer fields.
jfieldID j_textureID_field_; jfieldID j_textureID_field_;
jfieldID j_texture_presentation_timestamp_us_field_; jfieldID j_transform_matrix_field_;
jfieldID j_texture_timestamp_ns_field_;
// MediaCodecVideoDecoder.DecodedByteBuffer fields. // MediaCodecVideoDecoder.DecodedByteBuffer fields.
jfieldID j_info_index_field_; jfieldID j_info_index_field_;
jfieldID j_info_offset_field_; jfieldID j_info_offset_field_;
@ -155,8 +156,6 @@ class MediaCodecVideoDecoder : public webrtc::VideoDecoder,
// Global references; must be deleted in Release(). // Global references; must be deleted in Release().
std::vector<jobject> input_buffers_; std::vector<jobject> input_buffers_;
jobject surface_texture_;
jobject previous_surface_texture_;
// Render EGL context - owned by factory, should not be allocated/destroyed // Render EGL context - owned by factory, should not be allocated/destroyed
// by VideoDecoder. // by VideoDecoder.
@ -170,8 +169,6 @@ MediaCodecVideoDecoder::MediaCodecVideoDecoder(
key_frame_required_(true), key_frame_required_(true),
inited_(false), inited_(false),
sw_fallback_required_(false), sw_fallback_required_(false),
surface_texture_(NULL),
previous_surface_texture_(NULL),
codec_thread_(new Thread()), codec_thread_(new Thread()),
j_media_codec_video_decoder_class_( j_media_codec_video_decoder_class_(
jni, jni,
@ -190,7 +187,7 @@ MediaCodecVideoDecoder::MediaCodecVideoDecoder(
j_init_decode_method_ = GetMethodID( j_init_decode_method_ = GetMethodID(
jni, *j_media_codec_video_decoder_class_, "initDecode", jni, *j_media_codec_video_decoder_class_, "initDecode",
"(Lorg/webrtc/MediaCodecVideoDecoder$VideoCodecType;" "(Lorg/webrtc/MediaCodecVideoDecoder$VideoCodecType;"
"IILandroid/opengl/EGLContext;)Z"); "IILorg/webrtc/SurfaceTextureHelper;)Z");
j_release_method_ = j_release_method_ =
GetMethodID(jni, *j_media_codec_video_decoder_class_, "release", "()V"); GetMethodID(jni, *j_media_codec_video_decoder_class_, "release", "()V");
j_dequeue_input_buffer_method_ = GetMethodID( j_dequeue_input_buffer_method_ = GetMethodID(
@ -220,17 +217,15 @@ MediaCodecVideoDecoder::MediaCodecVideoDecoder(
jni, *j_media_codec_video_decoder_class_, "stride", "I"); jni, *j_media_codec_video_decoder_class_, "stride", "I");
j_slice_height_field_ = GetFieldID( j_slice_height_field_ = GetFieldID(
jni, *j_media_codec_video_decoder_class_, "sliceHeight", "I"); jni, *j_media_codec_video_decoder_class_, "sliceHeight", "I");
j_surface_texture_field_ = GetFieldID(
jni, *j_media_codec_video_decoder_class_, "surfaceTexture",
"Landroid/graphics/SurfaceTexture;");
jclass j_decoder_decoded_texture_buffer_class = FindClass(jni, jclass j_decoder_decoded_texture_buffer_class = FindClass(jni,
"org/webrtc/MediaCodecVideoDecoder$DecodedTextureBuffer"); "org/webrtc/MediaCodecVideoDecoder$DecodedTextureBuffer");
j_textureID_field_ = GetFieldID( j_textureID_field_ = GetFieldID(
jni, j_decoder_decoded_texture_buffer_class, "textureID", "I"); jni, j_decoder_decoded_texture_buffer_class, "textureID", "I");
j_texture_presentation_timestamp_us_field_ = j_transform_matrix_field_ = GetFieldID(
GetFieldID(jni, j_decoder_decoded_texture_buffer_class, jni, j_decoder_decoded_texture_buffer_class, "transformMatrix", "[F");
"presentationTimestampUs", "J"); j_texture_timestamp_ns_field_ = GetFieldID(
jni, j_decoder_decoded_texture_buffer_class, "timestampNs", "J");
jclass j_decoder_decoded_byte_buffer_class = FindClass(jni, jclass j_decoder_decoded_byte_buffer_class = FindClass(jni,
"org/webrtc/MediaCodecVideoDecoder$DecodedByteBuffer"); "org/webrtc/MediaCodecVideoDecoder$DecodedByteBuffer");
@ -253,14 +248,6 @@ MediaCodecVideoDecoder::MediaCodecVideoDecoder(
MediaCodecVideoDecoder::~MediaCodecVideoDecoder() { MediaCodecVideoDecoder::~MediaCodecVideoDecoder() {
// Call Release() to ensure no more callbacks to us after we are deleted. // Call Release() to ensure no more callbacks to us after we are deleted.
Release(); Release();
// Delete global references.
JNIEnv* jni = AttachCurrentThreadIfNeeded();
if (previous_surface_texture_ != NULL) {
jni->DeleteGlobalRef(previous_surface_texture_);
}
if (surface_texture_ != NULL) {
jni->DeleteGlobalRef(surface_texture_);
}
} }
int32_t MediaCodecVideoDecoder::InitDecode(const VideoCodec* inst, int32_t MediaCodecVideoDecoder::InitDecode(const VideoCodec* inst,
@ -310,6 +297,11 @@ int32_t MediaCodecVideoDecoder::InitDecodeOnCodecThread() {
frames_received_ = 0; frames_received_ = 0;
frames_decoded_ = 0; frames_decoded_ = 0;
if (use_surface_) {
surface_texture_helper_ = new rtc::RefCountedObject<SurfaceTextureHelper>(
jni, render_egl_context_);
}
jobject j_video_codec_enum = JavaEnumFromIndex( jobject j_video_codec_enum = JavaEnumFromIndex(
jni, "MediaCodecVideoDecoder$VideoCodecType", codecType_); jni, "MediaCodecVideoDecoder$VideoCodecType", codecType_);
bool success = jni->CallBooleanMethod( bool success = jni->CallBooleanMethod(
@ -318,7 +310,8 @@ int32_t MediaCodecVideoDecoder::InitDecodeOnCodecThread() {
j_video_codec_enum, j_video_codec_enum,
codec_.width, codec_.width,
codec_.height, codec_.height,
use_surface_ ? render_egl_context_ : nullptr); use_surface_ ? surface_texture_helper_->GetJavaSurfaceTextureHelper()
: nullptr);
if (CheckException(jni) || !success) { if (CheckException(jni) || !success) {
ALOGE("Codec initialization error - fallback to SW codec."); ALOGE("Codec initialization error - fallback to SW codec.");
sw_fallback_required_ = true; sw_fallback_required_ = true;
@ -358,15 +351,6 @@ int32_t MediaCodecVideoDecoder::InitDecodeOnCodecThread() {
} }
} }
if (use_surface_) {
jobject surface_texture = GetObjectField(
jni, *j_media_codec_video_decoder_, j_surface_texture_field_);
if (previous_surface_texture_ != NULL) {
jni->DeleteGlobalRef(previous_surface_texture_);
}
previous_surface_texture_ = surface_texture_;
surface_texture_ = jni->NewGlobalRef(surface_texture);
}
codec_thread_->PostDelayed(kMediaCodecPollMs, this); codec_thread_->PostDelayed(kMediaCodecPollMs, this);
return WEBRTC_VIDEO_CODEC_OK; return WEBRTC_VIDEO_CODEC_OK;
@ -391,6 +375,7 @@ int32_t MediaCodecVideoDecoder::ReleaseOnCodecThread() {
} }
input_buffers_.clear(); input_buffers_.clear();
jni->CallVoidMethod(*j_media_codec_video_decoder_, j_release_method_); jni->CallVoidMethod(*j_media_codec_video_decoder_, j_release_method_);
surface_texture_helper_ = nullptr;
inited_ = false; inited_ = false;
rtc::MessageQueueManager::Clear(this); rtc::MessageQueueManager::Clear(this);
if (CheckException(jni)) { if (CheckException(jni)) {
@ -499,7 +484,7 @@ int32_t MediaCodecVideoDecoder::DecodeOnCodecThread(
if (frames_received_ > frames_decoded_ + max_pending_frames_) { if (frames_received_ > frames_decoded_ + max_pending_frames_) {
ALOGV("Received: %d. Decoded: %d. Wait for output...", ALOGV("Received: %d. Decoded: %d. Wait for output...",
frames_received_, frames_decoded_); frames_received_, frames_decoded_);
if (!DeliverPendingOutputs(jni, kMediaCodecTimeoutMs * 1000)) { if (!DeliverPendingOutputs(jni, kMediaCodecTimeoutMs)) {
ALOGE("DeliverPendingOutputs error"); ALOGE("DeliverPendingOutputs error");
return ProcessHWErrorOnCodecThread(); return ProcessHWErrorOnCodecThread();
} }
@ -562,7 +547,7 @@ int32_t MediaCodecVideoDecoder::DecodeOnCodecThread(
} }
bool MediaCodecVideoDecoder::DeliverPendingOutputs( bool MediaCodecVideoDecoder::DeliverPendingOutputs(
JNIEnv* jni, int dequeue_timeout_us) { JNIEnv* jni, int dequeue_timeout_ms) {
if (frames_received_ <= frames_decoded_) { if (frames_received_ <= frames_decoded_) {
// No need to query for output buffers - decoder is drained. // No need to query for output buffers - decoder is drained.
return true; return true;
@ -571,7 +556,7 @@ bool MediaCodecVideoDecoder::DeliverPendingOutputs(
jobject j_decoder_output_buffer = jni->CallObjectMethod( jobject j_decoder_output_buffer = jni->CallObjectMethod(
*j_media_codec_video_decoder_, *j_media_codec_video_decoder_,
j_dequeue_output_buffer_method_, j_dequeue_output_buffer_method_,
dequeue_timeout_us); dequeue_timeout_ms);
if (CheckException(jni)) { if (CheckException(jni)) {
ALOGE("dequeueOutputBuffer() error"); ALOGE("dequeueOutputBuffer() error");
return false; return false;
@ -596,14 +581,15 @@ bool MediaCodecVideoDecoder::DeliverPendingOutputs(
// Extract data from Java DecodedTextureBuffer. // Extract data from Java DecodedTextureBuffer.
const int texture_id = const int texture_id =
GetIntField(jni, j_decoder_output_buffer, j_textureID_field_); GetIntField(jni, j_decoder_output_buffer, j_textureID_field_);
const int64_t timestamp_us = const jfloatArray j_transform_matrix =
GetLongField(jni, j_decoder_output_buffer, reinterpret_cast<jfloatArray>(GetObjectField(
j_texture_presentation_timestamp_us_field_); jni, j_decoder_output_buffer, j_transform_matrix_field_));
output_timestamps_ms = timestamp_us / rtc::kNumMicrosecsPerMillisec; const int64_t timestamp_ns = GetLongField(jni, j_decoder_output_buffer,
j_texture_timestamp_ns_field_);
output_timestamps_ms = timestamp_ns / rtc::kNumNanosecsPerMillisec;
// Create webrtc::VideoFrameBuffer with native texture handle. // Create webrtc::VideoFrameBuffer with native texture handle.
native_handle_.SetTextureObject(surface_texture_, texture_id); frame_buffer = surface_texture_helper_->CreateTextureFrame(
frame_buffer = new rtc::RefCountedObject<JniNativeHandleBuffer>( width, height, NativeHandleImpl(jni, texture_id, j_transform_matrix));
&native_handle_, width, height);
} else { } else {
// Extract data from Java ByteBuffer and create output yuv420 frame - // Extract data from Java ByteBuffer and create output yuv420 frame -
// for non surface decoding only. // for non surface decoding only.

View File

@ -31,32 +31,17 @@
namespace webrtc_jni { namespace webrtc_jni {
NativeHandleImpl::NativeHandleImpl() : texture_object_(NULL), texture_id_(-1) {} NativeHandleImpl::NativeHandleImpl(JNIEnv* jni,
jint j_oes_texture_id,
void* NativeHandleImpl::GetHandle() { jfloatArray j_transform_matrix)
return texture_object_; : oes_texture_id(j_oes_texture_id) {
} RTC_CHECK_EQ(16, jni->GetArrayLength(j_transform_matrix));
jfloat* transform_matrix_ptr =
int NativeHandleImpl::GetTextureId() { jni->GetFloatArrayElements(j_transform_matrix, nullptr);
return texture_id_; for (int i = 0; i < 16; ++i) {
} sampling_matrix[i] = transform_matrix_ptr[i];
}
void NativeHandleImpl::SetTextureObject(void* texture_object, int texture_id) { jni->ReleaseFloatArrayElements(j_transform_matrix, transform_matrix_ptr, 0);
texture_object_ = reinterpret_cast<jobject>(texture_object);
texture_id_ = texture_id;
}
JniNativeHandleBuffer::JniNativeHandleBuffer(void* native_handle,
int width,
int height)
: NativeHandleBuffer(native_handle, width, height) {}
rtc::scoped_refptr<webrtc::VideoFrameBuffer>
JniNativeHandleBuffer::NativeToI420Buffer() {
// TODO(pbos): Implement before using this in the encoder pipeline (or
// remove the RTC_CHECK() in VideoCapture).
RTC_NOTREACHED();
return nullptr;
} }
} // namespace webrtc_jni } // namespace webrtc_jni

View File

@ -31,33 +31,16 @@
#include <jni.h> #include <jni.h>
#include "webrtc/common_video/interface/video_frame_buffer.h"
namespace webrtc_jni { namespace webrtc_jni {
// Wrapper for texture object. // Wrapper for texture object.
class NativeHandleImpl { struct NativeHandleImpl {
public: NativeHandleImpl(JNIEnv* jni,
NativeHandleImpl(); jint j_oes_texture_id,
jfloatArray j_transform_matrix);
void* GetHandle(); const int oes_texture_id;
int GetTextureId(); float sampling_matrix[16];
void SetTextureObject(void* texture_object, int texture_id);
private:
jobject texture_object_;
int32_t texture_id_;
};
class JniNativeHandleBuffer : public webrtc::NativeHandleBuffer {
public:
JniNativeHandleBuffer(void* native_handle, int width, int height);
// TODO(pbos): Override destructor to release native handle, at the moment the
// native handle is not released based on refcount.
private:
rtc::scoped_refptr<webrtc::VideoFrameBuffer> NativeToI420Buffer() override;
}; };
} // namespace webrtc_jni } // namespace webrtc_jni

View File

@ -771,7 +771,7 @@ class JavaVideoRendererWrapper : public VideoRendererInterface {
jni, *j_frame_class_, "<init>", "(III[I[Ljava/nio/ByteBuffer;J)V")), jni, *j_frame_class_, "<init>", "(III[I[Ljava/nio/ByteBuffer;J)V")),
j_texture_frame_ctor_id_(GetMethodID( j_texture_frame_ctor_id_(GetMethodID(
jni, *j_frame_class_, "<init>", jni, *j_frame_class_, "<init>",
"(IIILjava/lang/Object;IJ)V")), "(IIII[FJ)V")),
j_byte_buffer_class_(jni, FindClass(jni, "java/nio/ByteBuffer")) { j_byte_buffer_class_(jni, FindClass(jni, "java/nio/ByteBuffer")) {
CHECK_EXCEPTION(jni); CHECK_EXCEPTION(jni);
} }
@ -827,13 +827,13 @@ class JavaVideoRendererWrapper : public VideoRendererInterface {
jobject CricketToJavaTextureFrame(const cricket::VideoFrame* frame) { jobject CricketToJavaTextureFrame(const cricket::VideoFrame* frame) {
NativeHandleImpl* handle = NativeHandleImpl* handle =
reinterpret_cast<NativeHandleImpl*>(frame->GetNativeHandle()); reinterpret_cast<NativeHandleImpl*>(frame->GetNativeHandle());
jobject texture_object = reinterpret_cast<jobject>(handle->GetHandle()); jfloatArray sampling_matrix = jni()->NewFloatArray(16);
int texture_id = handle->GetTextureId(); jni()->SetFloatArrayRegion(sampling_matrix, 0, 16, handle->sampling_matrix);
return jni()->NewObject( return jni()->NewObject(
*j_frame_class_, j_texture_frame_ctor_id_, *j_frame_class_, j_texture_frame_ctor_id_,
frame->GetWidth(), frame->GetHeight(), frame->GetWidth(), frame->GetHeight(),
static_cast<int>(frame->GetVideoRotation()), static_cast<int>(frame->GetVideoRotation()),
texture_object, texture_id, javaShallowCopy(frame)); handle->oes_texture_id, sampling_matrix, javaShallowCopy(frame));
} }
JNIEnv* jni() { JNIEnv* jni() {

View File

@ -27,7 +27,6 @@
package org.webrtc; package org.webrtc;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecCapabilities;
@ -42,8 +41,9 @@ import android.view.Surface;
import org.webrtc.Logging; import org.webrtc.Logging;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
// Java-side of peerconnection_jni.cc:MediaCodecVideoDecoder. // Java-side of peerconnection_jni.cc:MediaCodecVideoDecoder.
// This class is an implementation detail of the Java PeerConnection API. // This class is an implementation detail of the Java PeerConnection API.
@ -93,10 +93,11 @@ public class MediaCodecVideoDecoder {
private int stride; private int stride;
private int sliceHeight; private int sliceHeight;
private boolean useSurface; private boolean useSurface;
private int textureID = 0; // |isWaitingForTexture| is true when waiting for the transition:
private SurfaceTexture surfaceTexture = null; // MediaCodec.releaseOutputBuffer() -> onTextureFrameAvailable().
private boolean isWaitingForTexture = false;
private TextureListener textureListener;
private Surface surface = null; private Surface surface = null;
private EglBase eglBase;
private MediaCodecVideoDecoder() { } private MediaCodecVideoDecoder() { }
@ -180,12 +181,13 @@ public class MediaCodecVideoDecoder {
} }
} }
// Pass null in |sharedContext| to configure the codec for ByteBuffer output. // Pass null in |surfaceTextureHelper| to configure the codec for ByteBuffer output.
private boolean initDecode(VideoCodecType type, int width, int height, EGLContext sharedContext) { private boolean initDecode(
VideoCodecType type, int width, int height, SurfaceTextureHelper surfaceTextureHelper) {
if (mediaCodecThread != null) { if (mediaCodecThread != null) {
throw new RuntimeException("Forgot to release()?"); throw new RuntimeException("Forgot to release()?");
} }
useSurface = (sharedContext != null); useSurface = (surfaceTextureHelper != null);
String mime = null; String mime = null;
String[] supportedCodecPrefixes = null; String[] supportedCodecPrefixes = null;
if (type == VideoCodecType.VIDEO_CODEC_VP8) { if (type == VideoCodecType.VIDEO_CODEC_VP8) {
@ -204,9 +206,6 @@ public class MediaCodecVideoDecoder {
Logging.d(TAG, "Java initDecode: " + type + " : "+ width + " x " + height + Logging.d(TAG, "Java initDecode: " + type + " : "+ width + " x " + height +
". Color: 0x" + Integer.toHexString(properties.colorFormat) + ". Color: 0x" + Integer.toHexString(properties.colorFormat) +
". Use Surface: " + useSurface); ". Use Surface: " + useSurface);
if (sharedContext != null) {
Logging.d(TAG, "Decoder shared EGL Context: " + sharedContext);
}
mediaCodecThread = Thread.currentThread(); mediaCodecThread = Thread.currentThread();
try { try {
this.width = width; this.width = width;
@ -215,16 +214,8 @@ public class MediaCodecVideoDecoder {
sliceHeight = height; sliceHeight = height;
if (useSurface) { if (useSurface) {
// Create shared EGL context. textureListener = new TextureListener(surfaceTextureHelper);
eglBase = new EglBase(sharedContext, EglBase.ConfigType.PIXEL_BUFFER); surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
eglBase.createDummyPbufferSurface();
eglBase.makeCurrent();
// Create output surface
textureID = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
Logging.d(TAG, "Video decoder TextureID = " + textureID);
surfaceTexture = new SurfaceTexture(textureID);
surface = new Surface(surfaceTexture);
} }
MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
@ -265,11 +256,7 @@ public class MediaCodecVideoDecoder {
if (useSurface) { if (useSurface) {
surface.release(); surface.release();
surface = null; surface = null;
Logging.d(TAG, "Delete video decoder TextureID " + textureID); textureListener.release();
GLES20.glDeleteTextures(1, new int[] {textureID}, 0);
textureID = 0;
eglBase.release();
eglBase = null;
} }
} }
@ -317,11 +304,72 @@ public class MediaCodecVideoDecoder {
private static class DecodedTextureBuffer { private static class DecodedTextureBuffer {
private final int textureID; private final int textureID;
private final long presentationTimestampUs; private final float[] transformMatrix;
private final long timestampNs;
public DecodedTextureBuffer(int textureID, long presentationTimestampUs) { public DecodedTextureBuffer(int textureID, float[] transformMatrix, long timestampNs) {
this.textureID = textureID; this.textureID = textureID;
this.presentationTimestampUs = presentationTimestampUs; this.transformMatrix = transformMatrix;
this.timestampNs = timestampNs;
}
}
// Poll based texture listener.
private static class TextureListener
implements SurfaceTextureHelper.OnTextureFrameAvailableListener {
private final SurfaceTextureHelper surfaceTextureHelper;
private DecodedTextureBuffer textureBuffer;
// |newFrameLock| is used to synchronize arrival of new frames with wait()/notifyAll().
private final Object newFrameLock = new Object();
public TextureListener(SurfaceTextureHelper surfaceTextureHelper) {
this.surfaceTextureHelper = surfaceTextureHelper;
surfaceTextureHelper.setListener(this);
}
// Callback from |surfaceTextureHelper|. May be called on an arbitrary thread.
@Override
public void onTextureFrameAvailable(
int oesTextureId, float[] transformMatrix, long timestampNs) {
synchronized (newFrameLock) {
if (textureBuffer != null) {
Logging.e(TAG,
"Unexpected onTextureFrameAvailable() called while already holding a texture.");
throw new IllegalStateException("Already holding a texture.");
}
textureBuffer = new DecodedTextureBuffer(oesTextureId, transformMatrix, timestampNs);
newFrameLock.notifyAll();
}
}
// Dequeues and returns a texture buffer if available, or null otherwise.
public DecodedTextureBuffer dequeueTextureFrame(int timeoutMs) {
synchronized (newFrameLock) {
if (textureBuffer == null && timeoutMs > 0) {
try {
newFrameLock.wait(timeoutMs);
} catch(InterruptedException e) {
// Restore the interrupted status by reinterrupting the thread.
Thread.currentThread().interrupt();
}
}
final DecodedTextureBuffer textureBuffer = this.textureBuffer;
this.textureBuffer = null;
return textureBuffer;
}
}
public void release() {
// SurfaceTextureHelper.disconnect() will block until any onTextureFrameAvailable() in
// progress is done. Therefore, the call to disconnect() must be outside any synchronized
// statement that is also used in the onTextureFrameAvailable() above to avoid deadlocks.
surfaceTextureHelper.disconnect();
synchronized (newFrameLock) {
if (textureBuffer != null) {
surfaceTextureHelper.returnTextureFrame();
textureBuffer = null;
}
}
} }
} }
@ -330,14 +378,25 @@ public class MediaCodecVideoDecoder {
// Throws IllegalStateException if call is made on the wrong thread, if color format changes to an // Throws IllegalStateException if call is made on the wrong thread, if color format changes to an
// unsupported format, or if |mediaCodec| is not in the Executing state. Throws CodecException // unsupported format, or if |mediaCodec| is not in the Executing state. Throws CodecException
// upon codec error. // upon codec error.
private Object dequeueOutputBuffer(int dequeueTimeoutUs) private Object dequeueOutputBuffer(int dequeueTimeoutMs)
throws IllegalStateException, MediaCodec.CodecException { throws IllegalStateException, MediaCodec.CodecException {
checkOnMediaCodecThread(); checkOnMediaCodecThread();
// Calling multiple MediaCodec.releaseOutputBuffer() with render=true in a row will result in
// dropped texture frames. Therefore, wait for any pending onTextureFrameAvailable() before
// proceeding.
if (isWaitingForTexture) {
final DecodedTextureBuffer textureBuffer =
textureListener.dequeueTextureFrame(dequeueTimeoutMs);
isWaitingForTexture = (textureBuffer == null);
return textureBuffer;
}
// Drain the decoder until receiving a decoded buffer or hitting // Drain the decoder until receiving a decoded buffer or hitting
// MediaCodec.INFO_TRY_AGAIN_LATER. // MediaCodec.INFO_TRY_AGAIN_LATER.
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (true) { while (true) {
final int result = mediaCodec.dequeueOutputBuffer(info, dequeueTimeoutUs); final int result = mediaCodec.dequeueOutputBuffer(
info, TimeUnit.MILLISECONDS.toMicros(dequeueTimeoutMs));
switch (result) { switch (result) {
case MediaCodec.INFO_TRY_AGAIN_LATER: case MediaCodec.INFO_TRY_AGAIN_LATER:
return null; return null;
@ -371,9 +430,10 @@ public class MediaCodecVideoDecoder {
// Output buffer decoded. // Output buffer decoded.
if (useSurface) { if (useSurface) {
mediaCodec.releaseOutputBuffer(result, true /* render */); mediaCodec.releaseOutputBuffer(result, true /* render */);
// TODO(magjed): Wait for SurfaceTexture.onFrameAvailable() before returning a texture final DecodedTextureBuffer textureBuffer =
// frame. textureListener.dequeueTextureFrame(dequeueTimeoutMs);
return new DecodedTextureBuffer(textureID, info.presentationTimeUs); isWaitingForTexture = (textureBuffer == null);
return textureBuffer;
} else { } else {
return new DecodedByteBuffer(result, info.offset, info.size, info.presentationTimeUs); return new DecodedByteBuffer(result, info.offset, info.size, info.presentationTimeUs);
} }

View File

@ -44,7 +44,11 @@ public class VideoRenderer {
public final int[] yuvStrides; public final int[] yuvStrides;
public ByteBuffer[] yuvPlanes; public ByteBuffer[] yuvPlanes;
public final boolean yuvFrame; public final boolean yuvFrame;
public Object textureObject; // Matrix that transforms standard coordinates to their proper sampling locations in
// the texture. This transform compensates for any properties of the video source that
// cause it to appear different from a normalized texture. This matrix does not take
// |rotationDegree| into account.
public final float[] samplingMatrix;
public int textureId; public int textureId;
// Frame pointer in C++. // Frame pointer in C++.
private long nativeFramePointer; private long nativeFramePointer;
@ -69,6 +73,15 @@ public class VideoRenderer {
if (rotationDegree % 90 != 0) { if (rotationDegree % 90 != 0) {
throw new IllegalArgumentException("Rotation degree not multiple of 90: " + rotationDegree); throw new IllegalArgumentException("Rotation degree not multiple of 90: " + rotationDegree);
} }
// The convention in WebRTC is that the first element in a ByteBuffer corresponds to the
// top-left corner of the image, but in glTexImage2D() the first element corresponds to the
// bottom-left corner. This discrepancy is corrected by setting a vertical flip as sampling
// matrix.
samplingMatrix = new float[] {
1, 0, 0, 0,
0, -1, 0, 0,
0, 0, 1, 0,
0, 1, 0, 1};
} }
/** /**
@ -76,12 +89,12 @@ public class VideoRenderer {
*/ */
private I420Frame( private I420Frame(
int width, int height, int rotationDegree, int width, int height, int rotationDegree,
Object textureObject, int textureId, long nativeFramePointer) { int textureId, float[] samplingMatrix, long nativeFramePointer) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.yuvStrides = null; this.yuvStrides = null;
this.yuvPlanes = null; this.yuvPlanes = null;
this.textureObject = textureObject; this.samplingMatrix = samplingMatrix;
this.textureId = textureId; this.textureId = textureId;
this.yuvFrame = false; this.yuvFrame = false;
this.rotationDegree = rotationDegree; this.rotationDegree = rotationDegree;
@ -124,7 +137,6 @@ public class VideoRenderer {
*/ */
public static void renderFrameDone(I420Frame frame) { public static void renderFrameDone(I420Frame frame) {
frame.yuvPlanes = null; frame.yuvPlanes = null;
frame.textureObject = null;
frame.textureId = 0; frame.textureId = 0;
if (frame.nativeFramePointer != 0) { if (frame.nativeFramePointer != 0) {
releaseNativeFrame(frame.nativeFramePointer); releaseNativeFrame(frame.nativeFramePointer);