本文将重点介绍在 Android 平台上,WebRTC 是如何使用 MediaCodec 对视频数据进行编码,以及在整个编码过程中 webrtc native 与 java 的流程交互。
本篇开始会先回顾一下 Andorid MediaCodec 的概念和基础使用,然后再跟着问题去源码中分析。
MediaCodec 基础知识
MediaCodec 是 Android 提供的一个用于处理音频和视频数据的底层 API。它支持编码(将原始数据转换为压缩格式)和解码(将压缩数据转换回原始格式)的过程。MediaCodec 是自 Android 4.1(API 16)起引入的,(通常与MediaExtractor
、MediaSync
、MediaMuxer
、MediaCrypto
、 MediaDrm
、Image
、Surface
一起使用)。
以下是 MediaCodec 的一些关键概念和用法:
1、创建和配置 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);
}
2、输入和输出缓冲区:MediaCodec 有两个缓冲区队列,一个用于输入,另一个用于输出。输入缓冲区用于接收原始数据(例如从摄像头捕获的视频帧),输出缓冲区用于存储编码后的数据。在编解码过程中,需要将这些缓冲区填充或消费。
3、编码器工作模式: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();
4、MediaCodec 与 Surface:对于视频编解码,MediaCodec 可以与 Surface 对象一起使用,以便使用 GPU 进行高效处理。通过将编解码器与 Surface 关联,可以将图像数据直接从 Surface 传输到编解码器,而无需在 CPU 和 GPU 之间复制数据。这可以提高性能并降低功耗。可使用如下 api 进行创建一个输入 surface:
public Surface createInputSurface ();
返回的 inputSurface 可与 EGL 进行绑定,与 OpenGL ES 再进行关联。sample 可以参考这个开源库 grafika
5、开始和停止编解码:配置完 MediaCodec 后,调用 start()
方法开始编解码过程。在完成编解码任务后,需要调用 stop()
方法停止编解码器,并使用 release()
方法释放资源。
6、错误处理:在使用 MediaCodec 时,可能会遇到各种类型的错误,如不支持的编解码格式、资源不足等。为了确保应用程序的稳定性,需要妥善处理这些错误情况。
总之,MediaCodec 是 Android 中处理音视频编解码的关键组件。了解其基本概念和用法有助于构建高效、稳定的媒体应用程序。
webrtc 中如何使用硬件编码器?
由于在 WebRTC 中优先使用的是 VP8 编码器,所以我们想要分析 Android 上硬件编码的流程,需要先支持 h264 的硬件编码
1、创建 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();
}
2、在 createOffer / createAnswer 将 SDP 中 m=video 的 h264 playload 编号放在第一位这部分代码可以参考 preferCodec
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 相机采集 需要去温习一下。
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 了。
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 引擎。由于后面的处理不是本章的重点,所以不再分析。
webrtc 是如何做码流控制的?
WebRTC 的码流控制包括拥塞控制和比特率自适应两个主要方面。这里只简单介绍下概念,及 Android 是如何配合 webrtc 来动态修改码率的。
- 拥塞控制 (Congestion Control):拥塞控制主要关注在不引起网络拥塞的情况下传输尽可能多的数据。WebRTC 实现了基于 Google Congestion Control (GCC) 的拥塞控制算法,它也被称为 Send Side Bandwidth Estimation(发送端带宽估计)。此算法根据丢包率、往返时间 (RTT) 和接收端的 ACK 信息来调整发送端的码率。拥塞控制算法会持续监测网络状况,并根据需要动态调整发送码率。
- 比特率自适应 (Bitrate Adaptation):比特率自适应关注如何根据网络条件和设备性能调整视频编码参数,以实现最佳的视频质量。
当比特率发生变化时,WebRTC 会调用 VideoEncoder.setRateAllocation()
方法来通知更新比特率。
在编码的时候,其实在上一个疑问中已经知道了如何调节码率。判断条件是当当前的码率与需要调节的码率不匹配时,调用如下代码进行更新:
总结
本文深入剖析了 WebRTC 在 Android 平台上是如何使用 MediaCodec 对视频数据进行编码的,以及整个编码过程中 webrtc native 与 java 的流程交互。首先回顾了 Android MediaCodec 的概念和基础使用,包括创建和配置 MediaCodec、输入和输出缓冲区、编码器工作模式以及 MediaCodec 与 Surface 的关系。然后,通过具体的代码示例,详细说明了在 WebRTC 中如何实现视频数据的编解码。并通过几个疑问的方式从源码的角度了解到了整个编码流程。希望通过此文能帮助读者更好地理解 WebRTC Android 编码技术。
参考
- WebRTC Native 源码导读(三):安卓视频硬编码实现分析
- https://developer.android.com/reference/android/media/MediaCodec.html
作者:DevYk
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。