作者:DevYK
1. 簡(jiǎn)介
本文將重點(diǎn)介紹在 Android 平臺(tái)上,WebRTC 是如何使用 MediaCodec 對(duì)視頻數(shù)據(jù)進(jìn)行編碼,以及在整個(gè)編碼過程中 webrtc native 與 java 的流程交互读跷。
本篇開始會(huì)先回顧一下 Andorid MediaCodec 的概念和基礎(chǔ)使用萍恕,然后再跟著問題去源碼中分析唤崭。
2. MediaCodec 基礎(chǔ)知識(shí)
MediaCodec 是 Android 提供的一個(gè)用于處理音頻和視頻數(shù)據(jù)的底層 API。它支持編碼(將原始數(shù)據(jù)轉(zhuǎn)換為壓縮格式)和解碼(將壓縮數(shù)據(jù)轉(zhuǎn)換回原始格式)的過程。MediaCodec 是自 Android 4.1(API 16)起引入的朗和,(通常與MediaExtractor
馁痴、MediaSync
谊娇、MediaMuxer
、MediaCrypto
罗晕、 MediaDrm
济欢、Image
、Surface
一起使用)小渊。
以下是 MediaCodec 的一些關(guān)鍵概念和用法:
- 創(chuàng)建和配置 MediaCodec:首先法褥,需要根據(jù)所需的編解碼器類型(例如 H.264、VP8酬屉、Opus 等)創(chuàng)建一個(gè) MediaCodec 實(shí)例挖胃。接下來,通過 MediaFormat 對(duì)象指定編解碼器的一些參數(shù)梆惯,如分辨率酱鸭、幀率、碼率等垛吗。然后凹髓,使用
configure()
方法配置 MediaCodec。
try {
// 1\. 創(chuàng)建和配置 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);
}
- 輸入和輸出緩沖區(qū):MediaCodec 有兩個(gè)緩沖區(qū)隊(duì)列怯屉,一個(gè)用于輸入蔚舀,另一個(gè)用于輸出。輸入緩沖區(qū)用于接收原始數(shù)據(jù)(例如從攝像頭捕獲的視頻幀)锨络,輸出緩沖區(qū)用于存儲(chǔ)編碼后的數(shù)據(jù)赌躺。在編解碼過程中,需要將這些緩沖區(qū)填充或消費(fèi)羡儿。
-
編碼器工作模式:MediaCodec 支持兩種工作模式礼患,分別是同步和異步。在同步模式下,需要手動(dòng)管理輸入和輸出緩沖區(qū)缅叠。在異步模式下悄泥,通過設(shè)置回調(diào)函數(shù)(
MediaCodec.Callback
),可以在編解碼事件發(fā)生時(shí)自動(dòng)通知應(yīng)用程序肤粱。同步:
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:對(duì)于視頻編解碼弹囚,MediaCodec 可以與 Surface 對(duì)象一起使用,以便使用 GPU 進(jìn)行高效處理领曼。通過將編解碼器與 Surface 關(guān)聯(lián)鸥鹉,可以將圖像數(shù)據(jù)直接從 Surface 傳輸?shù)骄幗獯a器,而無需在 CPU 和 GPU 之間復(fù)制數(shù)據(jù)庶骄。這可以提高性能并降低功耗宋舷。
可使用如下 api 進(jìn)行創(chuàng)建一個(gè)輸入 surface
public Surface createInputSurface ();
返回的 inputSurface 可與 EGL 進(jìn)行綁定,與 OpenGL ES 再進(jìn)行關(guān)聯(lián)瓢姻。 sample 可以參考這個(gè)開源庫 grafika
開始和停止編解碼:配置完 MediaCodec 后祝蝠,調(diào)用
start()
方法開始編解碼過程。在完成編解碼任務(wù)后幻碱,需要調(diào)用stop()
方法停止編解碼器绎狭,并使用release()
方法釋放資源。錯(cuò)誤處理:在使用 MediaCodec 時(shí)褥傍,可能會(huì)遇到各種類型的錯(cuò)誤儡嘶,如不支持的編解碼格式、資源不足等恍风。為了確保應(yīng)用程序的穩(wěn)定性蹦狂,需要妥善處理這些錯(cuò)誤情況。
總之朋贬,MediaCodec 是 Android 中處理音視頻編解碼的關(guān)鍵組件凯楔。了解其基本概念和用法有助于構(gòu)建高效、穩(wěn)定的媒體應(yīng)用程序锦募。
3. webrtc 中如何使用硬件編碼器摆屯?
由于在 WebRTC 中優(yōu)先使用的是 VP8 編碼器,所以我們想要分析 Android 上硬件編碼的流程糠亩,需要先支持 h264 的硬件編碼
- 創(chuàng)建 PeerConnectionFactory 時(shí)設(shè)置視頻編碼器
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 編號(hào)放在第一位
這部分代碼可以參考 preferCodec
4. webrtc 中編碼器是如何初始化的虐骑?
通過上一個(gè)問題得知,我們使用的是 DefaultVideoEncoderFactory 默認(rèn)編碼器赎线,內(nèi)部實(shí)現(xiàn)就是使用的硬件能力
內(nèi)部實(shí)例化了一個(gè) HardwareVideoEncoderFactory 廷没,我們?cè)?DefaultVideoEncoderFactory 中看到了 createEncoder
函數(shù),這里的內(nèi)部就是實(shí)例化 HardwareVideoEncoder 的地方垂寥,我們先 debug 看下是哪里調(diào)用的颠黎,如下圖所示另锋,
下圖的第一點(diǎn)可以發(fā)現(xiàn)底層傳遞過來的已經(jīng)是 h264 編碼器的信息了。
發(fā)現(xiàn)調(diào)用棧并沒有在 java 端盏缤,那肯定在 native 端了,我們可以通過 createPeerConnectionFactory 查看下調(diào)用
- 將 videoEnvoderFactory 引用傳遞到 native
- Native 入口在 PeerConnectionFactory_jni.h
- 根據(jù)調(diào)用棧蓖扑,發(fā)現(xiàn)將 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);
}
- 通過一系列的調(diào)用唉铜,我們發(fā)現(xiàn)java 端的引用,被封裝成了 c++ 端的 VideoEncoderFactoryWrapper ,我們看一下它的構(gòu)造函數(shù)
主要就是通過 jni 調(diào)用 java 端的代碼律杠,用以獲取當(dāng)前設(shè)備所支持的編碼器和編碼器的信息
- 猜測(cè)既然在 Native 中包裝了 java 端 VideoEncoder.java 的引用潭流,那么肯定也有對(duì)應(yīng)的 CreateEncoder 函數(shù)
我們?cè)?video_encoder_factory_wrapper.h 中看到了我們想要的函數(shù),我們看下它的實(shí)現(xiàn)
這不就是我們找到了 createEncoder jni 調(diào)用的入口嗎柜去?那么是什么時(shí)候調(diào)用的呢灰嫉?我們進(jìn)行 debug 一下
它的調(diào)用棧是媒體協(xié)商成功后,根據(jù)發(fā)起方的編碼器來匹配嗓奢,目前匹配到了最優(yōu)的是 H264 編碼讼撒,然后進(jìn)行創(chuàng)建 H264 編碼器
此時(shí),我們已經(jīng)又回到了 java 端的 createEncoder 代碼股耽,我們來看下是怎么對(duì) MediaCodec 初始化的
-
MediaCodec 核心初始化代碼
在 HardwareVideoEncoderFactory 中的 createEncoder 中
上面的邏輯是判斷 MediaCodec 是否只是 baseline 和 high ,如果都不支持返回空根盒,反之返回 HardwareVideoEncoder 實(shí)例,該實(shí)例又返回給了 native 物蝙,然后轉(zhuǎn)為了 native 的智能指針 std::unique_ptr 的實(shí)體 VideoEncoderWrapper
通過 debug 炎滞,我們找到了在 native jni 執(zhí)行 initEncode 的入口函數(shù)
通過媒體協(xié)商后,我們得到了編碼器配置的一些參數(shù)
內(nèi)部執(zhí)行了 **initEncodeInternal** ,我們看下具體實(shí)現(xiàn)
這里就是我們所熟悉的 MediaCodec 編碼配置了诬乞,根據(jù)上面的序號(hào)我們知道册赛,先根據(jù)媒體協(xié)商后的編碼器名稱來創(chuàng)建一個(gè) MediaCodec 對(duì)象,然后配置一些必要的參數(shù)震嫉,最后啟動(dòng)編碼器.
5. webrtc 中是如何將數(shù)據(jù)送入編碼器的森瘪?
WebRTC 使用 VideoEncoder
接口來進(jìn)行視頻編碼,該接口定義了一個(gè)用于編碼視頻幀的方法:encode(VideoFrame frame, EncodeInfo info)
票堵。WebRTC 提供了一個(gè)名為 HardwareVideoEncoder
的類柜砾,該類實(shí)現(xiàn)了 VideoEncoder
接口,并使用 MediaCodec 對(duì)視頻幀進(jìn)行編碼换衬。
在 HardwareVideoEncoder
類中痰驱,WebRTC 將 VideoFrame
對(duì)象轉(zhuǎn)換為與 MediaCodec 關(guān)聯(lián)的 Surface
的紋理。這是通過使用 EglBase
類創(chuàng)建一個(gè) EGL 環(huán)境瞳浦,并使用該環(huán)境將 VideoFrame
的紋理繪制到 Surface
上來實(shí)現(xiàn)的担映。
為了更好的理解 MediaCodec createInputSurface 和 OpenGL ES 、EGL 的關(guān)系叫潦,我簡(jiǎn)單畫了一個(gè)架構(gòu)圖蝇完。如下所示:
EGL、OpenGL ES、 InputSurface 關(guān)系流程:
- 使用 OpenGL ES 繪制圖像短蜕。
- EGL 管理和連接 OpenGL ES 渲染的表面氢架。
- 通過 Input Surface,將 OpenGL ES 繪制的圖像傳遞給 MediaCodec朋魔。
- MediaCodec 對(duì)接收到的圖像數(shù)據(jù)進(jìn)行編碼岖研。
根據(jù)上面流程得知,采集到的 VideoFrame 會(huì)提交給 VideoStreamEncoder::OnFrame 然后經(jīng)過調(diào)用 EncodeVideoFrame 會(huì)執(zhí)行到 VideoEncoder.java 的包裝類,webrtc::jni::VideoEnacoderWrapper::Encode 函數(shù)警检,最后通過 jni 將(videoFrame,encodeInfo) 回調(diào)給了 java 端孙援。
接下來我們看 java 端如何處理的 VideoFrame
該函數(shù)的核心是判斷是否使用 surface 模式進(jìn)行編碼,如果條件成立調(diào)用 encodeTextureBuffer 進(jìn)行紋理編碼扇雕,
我們先看上圖的第一步,
第一步的 1-3 小點(diǎn)主要是通過 OpenGL ES 將 OES 紋理數(shù)據(jù)繪制出來拓售,然后第二大步的 textureEglBase.swapBuffers(...) 主要是將 OpenGL ES 處理后的圖像數(shù)據(jù)提交給 EGLSurface 。經(jīng)過這些操作后紋理數(shù)據(jù)就提交給 MediaCodec 的 inputsurface 了镶奉。
6. webrtc 是如何獲取編碼后的數(shù)據(jù)础淤?
在 HardwareVideoEncoder
類中,使用 MediaCodec 同步模式進(jìn)行獲取編碼后的數(shù)據(jù)哨苛。當(dāng)數(shù)據(jù)可用時(shí)值骇,會(huì)調(diào)用 callback.onEncodedFrame(encodedImage, new CodecSpecificInfo());
方法,然后將編碼后的幀傳遞給 WebRTC 引擎。WebRTC 引擎會(huì)對(duì)編碼后的幀進(jìn)行進(jìn)一步處理移国,如封裝 RTP 包吱瘩、發(fā)送到對(duì)端等。
主要流程如下:
第一步有點(diǎn)印象吧迹缀?對(duì)使碾,就是在編碼器初始化的時(shí)候會(huì)開啟一個(gè)循環(huán)獲取解碼數(shù)據(jù)的線程,我們分析下 deliverEncodedImage 函數(shù)的實(shí)現(xiàn)邏輯
這段代碼的主要功能是從編解碼器 (MediaCodec) 中獲取編碼后的視頻幀祝懂,并對(duì)關(guān)鍵幀進(jìn)行處理票摇。以下是代碼的逐步分析:
定義一個(gè)
MediaCodec.BufferInfo
對(duì)象,用于存儲(chǔ)輸出緩沖區(qū)的元信息砚蓬。調(diào)用
codec.dequeueOutputBuffer()
方法來獲取編碼后的輸出緩沖區(qū)索引矢门。如果索引小于 0,則有特殊含義灰蛙。比如MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED
表示輸出緩沖區(qū)已更改祟剔,此時(shí)需要重新獲取輸出緩沖區(qū)。使用索引獲取編碼后的輸出緩沖區(qū) (ByteBuffer)摩梧。
設(shè)置緩沖區(qū)的位置 (position) 和限制 (limit)物延,以便讀取數(shù)據(jù)。
檢查
info.flags
中的MediaCodec.BUFFER_FLAG_CODEC_CONFIG
標(biāo)志仅父。如果存在叛薯,表示當(dāng)前幀為編解碼器配置幀浑吟。這種情況下,將配置幀數(shù)據(jù)存儲(chǔ)在configBuffer
中耗溜。-
如果當(dāng)前幀不是配置幀组力,則執(zhí)行以下操作:
6.1 查看當(dāng)前是否重新配置編碼碼率,如果是就更新比特率抖拴。
6.2 檢查當(dāng)前幀是否為關(guān)鍵幀燎字。如果
info.flags
中的MediaCodec.BUFFER_FLAG_SYNC_FRAME
標(biāo)志存在,則表示當(dāng)前幀為關(guān)鍵幀城舞。 6.3 對(duì)于 H.264 編碼的關(guān)鍵幀轩触,將 SPS 和 PPS NALs 數(shù)據(jù)附加到幀的開頭寞酿。創(chuàng)建一個(gè)新的緩沖區(qū)家夺,將configBuffer
和編碼后的輸出緩沖區(qū)的內(nèi)容復(fù)制到新緩沖區(qū)中。6.4 根據(jù)幀類型 (關(guān)鍵幀或非關(guān)鍵幀)伐弹,創(chuàng)建一個(gè)
EncodedImage
對(duì)象拉馋。在釋放輸出緩沖區(qū)時(shí),確保不拋出任何異常惨好。6.5 調(diào)用
callback.onEncodedFrame()
方法傳遞編碼后的圖像和編解碼器特定信息煌茴。6.6 釋放
EncodedImage
對(duì)象。
當(dāng)遇到異常 (例如 IllegalStateException
) 時(shí)日川,代碼將記錄錯(cuò)誤信息蔓腐。
總之,這段代碼的目標(biāo)是從 MediaCodec 中獲取編碼后的視頻幀龄句,對(duì)關(guān)鍵幀進(jìn)行處理回论,并將結(jié)果傳遞給回調(diào)函數(shù)。
對(duì)分歇,該疑問的答案就是 6.5 它將編碼后的數(shù)據(jù)通過 onEncodedFrame 告知了 webrtc 引擎傀蓉。由于后面的處理不是本章的重點(diǎn),所以不再分析职抡。
7. webrtc 是如何做碼流控制的葬燎?
WebRTC 的碼流控制包括擁塞控制和比特率自適應(yīng)兩個(gè)主要方面。這里只簡(jiǎn)單介紹下概念缚甩,及 Android 是如何配合 webrtc 來動(dòng)態(tài)修改碼率的谱净。
- 擁塞控制 (Congestion Control): 擁塞控制主要關(guān)注在不引起網(wǎng)絡(luò)擁塞的情況下傳輸盡可能多的數(shù)據(jù)。WebRTC 實(shí)現(xiàn)了基于 Google Congestion Control (GCC) 的擁塞控制算法擅威,它也被稱為 Send Side Bandwidth Estimation(發(fā)送端帶寬估計(jì))岳遥。此算法根據(jù)丟包率、往返時(shí)間 (RTT) 和接收端的 ACK 信息來調(diào)整發(fā)送端的碼率裕寨。擁塞控制算法會(huì)持續(xù)監(jiān)測(cè)網(wǎng)絡(luò)狀況浩蓉,并根據(jù)需要?jiǎng)討B(tài)調(diào)整發(fā)送碼率派继。
- 比特率自適應(yīng) (Bitrate Adaptation): 比特率自適應(yīng)關(guān)注如何根據(jù)網(wǎng)絡(luò)條件和設(shè)備性能調(diào)整視頻編碼參數(shù),以實(shí)現(xiàn)最佳的視頻質(zhì)量捻艳。
當(dāng)比特率發(fā)生變化時(shí)驾窟,WebRTC 會(huì)調(diào)用 VideoEncoder.setRateAllocation()
方法來通知更新比特率。
在編碼的時(shí)候认轨,其實(shí)在上一個(gè)疑問中已經(jīng)知道了如何調(diào)節(jié)碼率绅络。判斷條件是當(dāng)當(dāng)前的碼率與需要調(diào)節(jié)的碼率不匹配時(shí),調(diào)用如下代碼進(jìn)行更新:
8. 總結(jié)
本文深入剖析了 WebRTC 在 Android 平臺(tái)上是如何使用 MediaCodec 對(duì)視頻數(shù)據(jù)進(jìn)行編碼的嘁字,以及整個(gè)編碼過程中 webrtc native 與 java 的流程交互恩急。首先回顧了 Android MediaCodec 的概念和基礎(chǔ)使用,包括創(chuàng)建和配置 MediaCodec纪蜒、輸入和輸出緩沖區(qū)衷恭、編碼器工作模式以及 MediaCodec 與 Surface 的關(guān)系。然后纯续,通過具體的代碼示例随珠,詳細(xì)說明了在 WebRTC 中如何實(shí)現(xiàn)視頻數(shù)據(jù)的編解碼。并通過幾個(gè)疑問的方式從源碼的角度了解到了整個(gè)編碼流程猬错。希望通過此文能幫助讀者更好地理解 WebRTC Android 編碼技術(shù)窗看。