1. 简介
本文将要点介绍在 Android 平台上,WebRTC 是怎么运用 MediaCodec 对视频数据进行编码,以及在整个编码进程中 webrtc native 与 java 的流程交互。
本篇开端会先回忆一下 Andorid MediaCodec 的概念和根底运用,然后再跟着问题去源码中分析。
2. MediaCodec 根底知识
MediaCodec 是 Android 供给的一个用于处理音频和视频数据的底层 API。它支撑编码(将原始数据转换为紧缩格局)和解码(将紧缩数据转换回原始格局)的进程。MediaCodec 是自 Android 4.1(API 16)起引入的,(通常与MediaExtractor
、MediaSync
、MediaMuxer
、MediaCrypto
、 MediaDrm
、Image
、Surface
一同运用)。
以下是 MediaCodec 的一些要害概念和用法:
-
创立和装备 MediaCodec:首先,需求依据所需的编解码器类型(例如 H.264、VP8、Opus 等)创立一个 MediaCodec 实例。接下来,经过 MediaFormat 方针指定编解码器的一些参数,如分辨率、帧率、码率等。然后,运用
configure()
办法装备 MediaCodec。try { // 1. 创立和装备 MediaCodec MediaCodecInfo codecInfo = selectCodec(MIME_TYPE); if (codecInfo == null) { throw new RuntimeException("No codec found for " + MIME_TYPE); } MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL); encoder = MediaCodec.createByCodecName(codecInfo.getName()); encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); encoder.start(); } catch (IOException e) { throw new RuntimeException("Failed to initialize encoder", e); }
-
输入和输出缓冲区:MediaCodec 有两个缓冲区队列,一个用于输入,另一个用于输出。输入缓冲区用于接纳原始数据(例如从摄像头捕获的视频帧),输出缓冲区用于存储编码后的数据。在编解码进程中,需求将这些缓冲区填充或消费。
-
编码器作业形式:MediaCodec 支撑两种作业形式,分别是同步和异步。在同步形式下,需求手动办理输入和输出缓冲区。在异步形式下,经过设置回调函数(
MediaCodec.Callback
),能够在编解码事情发生时主动告诉应用程序。同步:
MediaCodec codec = MediaCodec.createByCodecName(name); codec.configure(format, …); MediaFormat outputFormat = codec.getOutputFormat(); // option B codec.start(); for (;;) { int inputBufferId = codec.dequeueInputBuffer(timeoutUs); if (inputBufferId >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(…); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } int outputBufferId = codec.dequeueOutputBuffer(…); if (outputBufferId >= 0) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is identical to outputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) outputFormat = codec.getOutputFormat(); // option B } } codec.stop(); codec.release();
异步(引荐运用):
MediaCodec codec = MediaCodec.createByCodecName(name); MediaFormat mOutputFormat; // member variable codec.setCallback(new MediaCodec.Callback() { @Override void onInputBufferAvailable(MediaCodec mc, int inputBufferId) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId); // fill inputBuffer with valid data … codec.queueInputBuffer(inputBufferId, …); } @Override void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId); MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A // bufferFormat is equivalent to mOutputFormat // outputBuffer is ready to be processed or rendered. … codec.releaseOutputBuffer(outputBufferId, …); } @Override void onOutputFormatChanged(MediaCodec mc, MediaFormat format) { // Subsequent data will conform to new format. // Can ignore if using getOutputFormat(outputBufferId) mOutputFormat = format; // option B } @Override void onError(…) { … } @Override void onCryptoError(…) { … } }); codec.configure(format, …); mOutputFormat = codec.getOutputFormat(); // option B codec.start(); // wait for processing to complete codec.stop(); codec.release();
-
MediaCodec 与 Surface:对于视频编解码,MediaCodec 能够与 Surface 方针一同运用,以便运用 GPU 进行高效处理。经过将编解码器与 Surface 相关,能够将图画数据直接从 Surface 传输到编解码器,而无需在 CPU 和 GPU 之间复制数据。这能够提高性能并降低功耗。
可运用如下 api 进行创立一个输入 surface
public Surface createInputSurface ();
回来的 inputSurface 可与 EGL 进行绑定,与 OpenGL ES 再进行相关。 sample 能够参阅这个开源库 grafika
-
开端和中止编解码:装备完 MediaCodec 后,调用
start()
办法开端编解码进程。在完成编解码任务后,需求调用stop()
办法中止编解码器,并运用release()
办法开释资源。 -
过错处理:在运用 MediaCodec 时,可能会遇到各种类型的过错,如不支撑的编解码格局、资源缺乏等。为了保证应用程序的稳定性,需求妥善处理这些过错情况。
总之,MediaCodec 是 Android 中处理音视频编解码的要害组件。了解其基本概念和用法有助于构建高效、稳定的媒体应用程序。
3. webrtc 中怎么运用硬件编码器?
由于在 WebRTC 中优先运用的是 VP8 编码器,所以咱们想要分析 Android 上硬件编码的流程,需求先支撑 h264 的硬件编码
-
创立 PeerConnectionFactory 时设置视频编码器
private PeerConnectionFactory createPeerConnectionFactory() { PeerConnectionFactory.initialize( PeerConnectionFactory.InitializationOptions.builder(applicationContext) .setEnableInternalTracer(true) .createInitializationOptions()); PeerConnectionFactory.Options options = new PeerConnectionFactory.Options(); DefaultVideoEncoderFactory defaultVideoEncoderFactory = new DefaultVideoEncoderFactory( rootEglBase.getEglBaseContext(), true /* enableIntelVp8Encoder */, true); DefaultVideoDecoderFactory defaultVideoDecoderFactory = new DefaultVideoDecoderFactory(rootEglBase.getEglBaseContext()); return PeerConnectionFactory.builder() .setOptions(options) .setVideoEncoderFactory(defaultVideoEncoderFactory) .setVideoDecoderFactory(defaultVideoDecoderFactory) .createPeerConnectionFactory(); }
-
在 createOffer / createAnswer 将 SDP 中 m=video 的 h264 playload 编号放在第一位
这部分代码能够参阅 preferCodec
4. webrtc 中编码器是怎么初始化的?
经过上一个问题得知,咱们运用的是 DefaultVideoEncoderFactory 默认编码器,内部完成便是运用的硬件才能
内部实例化了一个 HardwareVideoEncoderFactory ,咱们在 DefaultVideoEncoderFactory 中看到了 createEncoder
函数,这里的内部便是实例化 HardwareVideoEncoder 的当地,咱们先 debug 看下是哪里调用的,如下图所示,
下图的第一点能够发现底层传递过来的已经是 h264 编码器的信息了。
发现调用栈并没有在 java 端,那肯定在 native 端了,咱们能够经过 createPeerConnectionFactory 查看下调用
-
将 videoEnvoderFactory 引证传递到 native
-
Native 进口在 PeerConnectionFactory_jni.h
-
依据调用栈,发现将 jencoder_factory 包装到了 CreateVideoEncoderFactory
ScopedJavaLocalRef<jobject> CreatePeerConnectionFactoryForJava( JNIEnv* jni, const JavaParamRef<jobject>& jcontext, const JavaParamRef<jobject>& joptions, rtc::scoped_refptr<AudioDeviceModule> audio_device_module, rtc::scoped_refptr<AudioEncoderFactory> audio_encoder_factory, rtc::scoped_refptr<AudioDecoderFactory> audio_decoder_factory, const JavaParamRef<jobject>& jencoder_factory, const JavaParamRef<jobject>& jdecoder_factory, rtc::scoped_refptr<AudioProcessing> audio_processor, std::unique_ptr<FecControllerFactoryInterface> fec_controller_factory, std::unique_ptr<NetworkControllerFactoryInterface> network_controller_factory, std::unique_ptr<NetworkStatePredictorFactoryInterface> network_state_predictor_factory, std::unique_ptr<NetEqFactory> neteq_factory) { ... media_dependencies.video_encoder_factory = absl::WrapUnique(CreateVideoEncoderFactory(jni, jencoder_factory)); ... } VideoEncoderFactory* CreateVideoEncoderFactory( JNIEnv* jni, const JavaRef<jobject>& j_encoder_factory) { return IsNull(jni, j_encoder_factory) ? nullptr : new VideoEncoderFactoryWrapper(jni, j_encoder_factory); }
-
经过一系列的调用,咱们发现java 端的引证,被封装成了 c++ 端的 VideoEncoderFactoryWrapper ,咱们看一下它的结构函数
首要便是经过 jni 调用 java 端的代码,用以获取当时设备所支撑的编码器和编码器的信息
-
猜测既然在 Native 中包装了 java 端 VideoEncoder.java 的引证,那么肯定也有对应的 CreateEncoder 函数
咱们在 video_encoder_factory_wrapper.h 中看到了咱们想要的函数,咱们看下它的完成
这不便是咱们找到了 createEncoder jni 调用的进口吗?那么是什么时分调用的呢?咱们进行 debug 一下
它的调用栈是媒体协商成功后,依据发起方的编码器来匹配,目前匹配到了最优的是 H264 编码,然后进行创立 H264 编码器
此刻,咱们已经又回到了 java 端的 createEncoder 代码,咱们来看下是怎样对 MediaCodec 初始化的
-
MediaCodec 中心初始化代码
在 HardwareVideoEncoderFactory 中的 createEncoder 中
上面的逻辑是判别 MediaCodec 是否仅仅 baseline 和 high ,假如都不支撑回来空,反之回来 HardwareVideoEncoder 实例,该实例又回来给了 native ,然后转为了 native 的智能指针 std::unique_ptr 的实体 VideoEncoderWrapper
经过 debug ,咱们找到了在 native jni 履行 initEncode 的进口函数
经过媒体协商后,咱们得到了编码器装备的一些参数
内部履行了 initEncodeInternal ,咱们看下具体完成
这里便是咱们所熟悉的 MediaCodec 编码装备了,依据上面的序号咱们知道,先依据媒体协商后的编码器名称来创立一个 MediaCodec 方针,然后装备一些必要的参数,最终发动编码器.
下一步咱们开端分析 webrtc 怎么将收集到的纹路送入到编码器中进行编码的。还没有看 WebRTC 源码分析 (一) Android 相机收集 需求去温习一下。
5. webrtc 中是怎么将数据送入编码器的?
WebRTC 运用 VideoEncoder
接口来进行视频编码,该接口定义了一个用于编码视频帧的办法:encode(VideoFrame frame, EncodeInfo info)
。WebRTC 供给了一个名为 HardwareVideoEncoder
的类,该类完成了 VideoEncoder
接口,并运用 MediaCodec 对视频帧进行编码。
在 HardwareVideoEncoder
类中,WebRTC 将 VideoFrame
方针转换为与 MediaCodec 相关的 Surface
的纹路。这是经过运用 EglBase
类创立一个 EGL 环境,并运用该环境将 VideoFrame
的纹路制作到 Surface
上来完成的。
为了更好的了解 MediaCodec createInputSurface 和 OpenGL ES 、EGL 的联系,我简略画了一个架构图。如下所示:
EGL、OpenGL ES、 InputSurface 联系流程:
- 运用 OpenGL ES 制作图画。
- EGL 办理和衔接 OpenGL ES 渲染的表面。
- 经过 Input Surface,将 OpenGL ES 制作的图画传递给 MediaCodec。
- MediaCodec 对接纳到的图画数据进行编码。
咱们看下具体的流程吧,经过上一篇文章得知, WebRTC 源码分析 (一) Android 相机收集 收集到相机数据后,会提交给 VideoStreamEncoder ,咱们来看一下仓库
依据上面流程得知,收集到的 VideoFrame 会提交给 VideoStreamEncoder::OnFrame 然后经过调用 EncodeVideoFrame 会履行到 VideoEncoder.java 的包装类,webrtc::jni::VideoEnacoderWrapper::Encode 函数,最终经过 jni 将(videoFrame,encodeInfo) 回调给了 java 端。
接下来咱们看 java 端怎么处理的 VideoFrame
该函数的中心是判别是否运用 surface 形式进行编码,假如条件成立调用 encodeTextureBuffer 进行纹路编码,
咱们先看上图的第一步,
第一步的 1-3 小点首要是经过 OpenGL ES 将 OES 纹路数据制作出来,然后第二大步的 textureEglBase.swapBuffers(…) 首要是将 OpenGL ES 处理后的图画数据提交给 EGLSurface 。经过这些操作后纹路数据就提交给 MediaCodec 的 inputsurface 了。
6. webrtc 是怎么获取编码后的数据?
在 HardwareVideoEncoder
类中,运用 MediaCodec 同步形式进行获取编码后的数据。当数据可用时,会调用 callback.onEncodedFrame(encodedImage, new CodecSpecificInfo());
办法,然后将编码后的帧传递给 WebRTC 引擎。WebRTC 引擎会对编码后的帧进行进一步处理,如封装 RTP 包、发送到对端等。
首要流程如下:
第一步有点印象吧?对,便是在编码器初始化的时分会开启一个循环获取解码数据的线程,咱们分析下 deliverEncodedImage 函数的完成逻辑
这段代码的首要功能是从编解码器 (MediaCodec) 中获取编码后的视频帧,并对要害帧进行处理。以下是代码的逐渐分析:
-
定义一个
MediaCodec.BufferInfo
方针,用于存储输出缓冲区的元信息。 -
调用
codec.dequeueOutputBuffer()
办法来获取编码后的输出缓冲区索引。假如索引小于 0,则有特别意义。比方MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
表示输出缓冲区已更改,此刻需求从头获取输出缓冲区。 -
运用索引获取编码后的输出缓冲区 (ByteBuffer)。
-
设置缓冲区的位置 (position) 和限制 (limit),以便读取数据。
-
查看
info.flags
中的MediaCodec.BUFFER_FLAG_CODEC_CONFIG
标志。假如存在,表示当时帧为编解码器装备帧。这种情况下,将装备帧数据存储在configBuffer
中。 -
假如当时帧不是装备帧,则履行以下操作:
6.1 查看当时是否从头装备编码码率,假如是就更新比特率。
6.2 查看当时帧是否为要害帧。假如
info.flags
中的MediaCodec.BUFFER_FLAG_SYNC_FRAME
标志存在,则表示当时帧为要害帧。 6.3 对于 H.264 编码的要害帧,将 SPS 和 PPS NALs 数据附加到帧的最初。创立一个新的缓冲区,将configBuffer
和编码后的输出缓冲区的内容复制到新缓冲区中。6.4 依据帧类型 (要害帧或非要害帧),创立一个
EncodedImage
方针。在开释输出缓冲区时,保证不抛出任何反常。6.5 调用
callback.onEncodedFrame()
办法传递编码后的图画和编解码器特定信息。6.6 开释
EncodedImage
方针。
当遇到反常 (例如 IllegalStateException
) 时,代码将记录过错信息。
总之,这段代码的方针是从 MediaCodec 中获取编码后的视频帧,对要害帧进行处理,并将成果传递给回调函数。
对,该疑问的答案便是 6.5 它将编码后的数据经过 onEncodedFrame 告知了 webrtc 引擎。由于后面的处理不是本章的要点,所以不再分析。
7. webrtc 是怎么做码流操控的?
WebRTC 的码流操控包含拥塞操控和比特率自适应两个首要方面。这里只简略介绍下概念,及 Android 是怎么合作 webrtc 来动态修改码率的。
- 拥塞操控 (Congestion Control): 拥塞操控首要重视在不引起网络拥塞的情况下传输尽可能多的数据。WebRTC 完成了基于 Google Congestion Control (GCC) 的拥塞操控算法,它也被称为 Send Side Bandwidth Estimation(发送端带宽估量)。此算法依据丢包率、往复时间 (RTT) 和接纳端的 ACK 信息来调整发送端的码率。拥塞操控算法会继续监测网络情况,并依据需求动态调整发送码率。
- 比特率自适应 (Bitrate Adaptation): 比特率自适应重视怎么依据网络条件和设备性能调整视频编码参数,以完成最佳的视频质量。
当比特率发生变化时,WebRTC 会调用 VideoEncoder.setRateAllocation()
办法来告诉更新比特率。
在编码的时分,其实在上一个疑问中已经知道了怎么调节码率。判别条件是当当时的码率与需求调节的码率不匹配时,调用如下代码进行更新:
8. 总结
本文深入分析了 WebRTC 在 Android 平台上是怎么运用 MediaCodec 对视频数据进行编码的,以及整个编码进程中 webrtc native 与 java 的流程交互。首先回忆了 Android MediaCodec 的概念和根底运用,包含创立和装备 MediaCodec、输入和输出缓冲区、编码器作业形式以及 MediaCodec 与 Surface 的联系。然后,经过具体的代码示例,具体说明了在 WebRTC 中怎么完成视频数据的编解码。并经过几个疑问的方法从源码的角度了解到了整个编码流程。期望经过此文能协助读者更好地了解 WebRTC Android 编码技能。
参阅
- WebRTC Native 源码导读(三):安卓视频硬编码完成分析
- developer.android.com/reference/a…