系列文章目錄
- Android MediaCodec 簡明教程(一):使用 MediaCodecList 查詢 Codec 信息,并創建 MediaCodec 編解碼器
- Android MediaCodec 簡明教程(二):使用 MediaCodecInfo.CodecCapabilities 查詢 Codec 支持的寬高,顏色空間等能力
- Android MediaCodec 簡明教程(三):詳解如何在同步與異步模式下,使用MediaCodec將視頻解碼到ByteBuffers,并在ImageView上展示
- Android MediaCodec 簡明教程(四):使用 MediaCodec 將視頻解碼到 Surface,并使用 SurfaceView 播放視頻
文章目錄
- 系列文章目錄
- 前言
- 編碼流程概述
- MediaCodec 異步模式編碼
- 創建編碼器
- 設置編碼回調
- 編碼器 Configure
- 創建 Muxer
- 開始編碼的工作
- 循環地編碼視頻幀
- 等待編碼結束,釋放資源
- 總結
- 參考
前言
前面我們了解了 MediaCodec 解碼的具體使用流程,包括異步和同步模式、解碼到 ByteBuffers 或者 Surface。本章開始,我們將開始學習如何使用 MediaCodec 進行編碼。
與解碼類似,MediaCodec 編碼的輸入支持 ByteBuffer 或者 Surface。 遵循循序漸進的原則,我們從最簡單的一種情況開始講起:MediaCodec 編碼過程中,輸入的圖像數據存放在 ByteBuffer 中。
編碼流程概述
首先,我們需要創建對應的 MediaCodec 編碼器,并進行正確的 configure。這一步中,你要考慮一些編碼的參數,包括視頻的分辨率、幀率、比特率、color format 等。其中 color format 非常重要,它描述了送給編碼器的數據是如何排列的,編碼器根據這個屬性來讀取數據。
接著,為了將編碼后的數據保存為 MP4 文件,我們創建 MediaMuxer 來進行封裝的工作。
當 MediaCodec 編碼器和 MediaMuxer 準備好后,就能夠開始編碼了:將視頻數據送給 Codec,Codec 將編碼后的數據吐給 MediaMuxer,Muxer 將這些壓縮后的數據寫入本地文件。一切都很簡單。
接下來我將對具體的代碼進行說明,本文完整代碼你可以在 EncodeUsingBuffersActivity 找到,該代碼使用異步模式進行編碼,異步模式更加簡潔,我更喜歡這種模式。如果你想看同步模式是如何實現的,可以參考 CTS - EncodeDecodeTest 中的 doEncodeDecodeVideoFromBuffer 函數。
MediaCodec 異步模式編碼
創建編碼器
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val encodeCodecName = codecList.findEncoderForFormat(format)
val encoder = MediaCodec.createByCodecName(encodeCodecName)
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
:定義了一個字符串常量mimeType,其值為MediaFormat.MIMETYPE_VIDEO_AVC,表示我們將使用的是AVC(即H.264)編碼格式。val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
:創建一個MediaFormat對象,該對象描述了我們想要的視頻格式,包括編碼格式、視頻寬度和高度。val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
:獲取系統中所有常規(非硬件加速)的編解碼器列表。val encodeCodecName = codecList.findEncoderForFormat(format)
:在編解碼器列表中查找能夠處理我們指定格式的編碼器。val encoder = MediaCodec.createByCodecName(encodeCodecName)
:通過編碼器的名稱創建一個MediaCodec對象,這個對象就是我們的視頻編碼器。
當然,也可以更簡單:
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val encoder = MediaCodec.createEncoderByType(encodeCodecName)
設置編碼回調
encoder.setCallback(object: MediaCodec.Callback(){override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {//}override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo) {//}override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {//}override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {//}
})
MediaCodec類中的setCallback()方法用于設置一個回調接口,這個接口將在編解碼操作的各個階段被調用。這個方法接收一個MediaCodec.Callback對象作為參數。
MediaCodec.Callback是一個抽象類,它定義了四個方法:
-
onInputBufferAvailable(MediaCodec codec, int index)
:當輸入緩沖區可用時,此方法被調用。參數index指示了哪個輸入緩沖區已經變得可用。 -
onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)
:當輸出緩沖區可用時,此方法被調用。參數index指示了哪個輸出緩沖區已經變得可用,info包含了關于這個緩沖區的元數據,如其包含的數據的大小,時間戳等。 -
onError(MediaCodec codec, MediaCodec.CodecException e)
:當編解碼器發生錯誤時,此方法被調用。參數e是一個MediaCodec.CodecException對象,包含了關于錯誤的詳細信息。 -
onOutputFormatChanged(MediaCodec codec, MediaFormat format)
:當輸出格式發生變化時,此方法被調用。參數format是一個MediaFormat對象,包含了新的輸出格式。
回調中的代碼是我們具體的編碼邏輯,這個放后面詳細講。
編碼器 Configure
val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
assert(encoder.codecInfo.getCapabilitiesForType(mimeType).colorFormats.contains(colorFormat))
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
- colorFormat 選擇
COLOR_FormatYUV420Flexible
這是一種最常用的像素格式。 - 接下來這行代碼是一個斷言,它檢查編碼器是否支持上面定義的顏色格式。為了確保我們 Demo 的簡潔,我假定你的機器是一定支持
COLOR_FormatYUV420Flexible
的,否則我需要寫額外的代碼來兼容,這會使得代碼變得負責。 - 接著,設置了顏色格式、比特率、幀率等重要的編碼信息。
- 最后調用
configure
函數,這行代碼用上面設置的參數來配置編碼器,最后一個參數指定了這是一個編碼器,而不是解碼器。
創建 Muxer
val outputDir = externalCacheDir
val outputName = "test.mp4"
val outputFile = File(outputDir, outputName)
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
開始編碼的工作
現在我們有 encoder 和 muxer 組件,要開始編碼視頻的任務,需要啟動這兩個組件,但兩者啟動時機有差別。
首先,我們先啟動 encoder
encoder.start()
那么 muxer 何時啟動呢?在啟動 muxer 之前我們需要明確知道 output format 的信息。
在使用MediaCodec進行編碼時,onOutputFormatChanged 方法會在開始編碼后首次調用。這是因為在開始編碼后,MediaCodec 會根據你設置的參數(如分辨率、比特率等)來確定最終的輸出格式。一旦輸出格式確定,就會觸發onOutputFormatChanged方法。
這個方法的調用表示編碼器的輸出格式已經準備好,你可以獲取到這個新的輸出格式,并用它來配置你的MediaMuxer。這是必要的,因為MediaMuxer需要知道它正在混合的音頻和視頻的具體格式。
基于上述原因,在異步模式下我們可以在 onOutputFormatChanged 回調函數中啟動 muxer:
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {videoTrackIndex = muxer.addTrack(format)muxer.start()
}
循環地編碼視頻幀
讓我們來看回調函數中的具體邏輯,這些邏輯表明了我們是如何進行編碼的
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {val pts = computePresentationTime(generateIndex)// input eosif(generateIndex == NUM_FRAMES){codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM)}else{val frameData = ByteArray(videoWidth * videoHeight * 3 / 2)generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData)val inputBuffer = codec.getInputBuffer(index)inputBuffer.put(frameData)codec.queueInputBuffer(index, 0, frameData.size, pts, 0)generateIndex++}
}
override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo
) {// output eosval isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0if(isDone){outputEnd.set(true)info.size = 0}if(info.size > 0){val encodedData = codec.getOutputBuffer(index)muxer.writeSampleData(videoTrackIndex, encodedData!!, info)codec.releaseOutputBuffer(index, false)}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {//...
}
首先看 onInputBufferAvailable 回調:
- val pts = computePresentationTime(generateIndex):這行代碼計算了當前幀的顯示時間,通常是根據幀率和當前幀的索引來計算的。
- if(generateIndex == NUM_FRAMES):這行代碼檢查是否已經處理完所有的幀。如果是,那么就需要向編碼器發送一個表示輸入結束的標志。
- codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM):這行代碼向編碼器的輸入隊列中添加一個空的緩沖區,并設置了一個表示輸入結束的標志。這告訴編碼器不會有更多的數據輸入了。
- val frameData = ByteArray(videoWidth * videoHeight * 3 / 2):這行代碼創建了一個字節數組,用于存儲一幀的數據。這里假設的是YUV420格式的數據,所以大小是寬度乘以高度的1.5倍。
- generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData):這行代碼生成了一幀的數據。
- val inputBuffer = codec.getInputBuffer(index):這行代碼獲取了編碼器的一個輸入緩沖區。
- inputBuffer.put(frameData):這行代碼將生成的幀數據放入輸入緩沖區。
- codec.queueInputBuffer(index, 0, frameData.size, pts, 0):這行代碼將填充了數據的輸入緩沖區添加到編碼器的輸入隊列中。
- generateIndex++:這行代碼將幀的索引加一,準備處理下一幀的數據。
需要說明的是,我們使用 generateFrame 來生成 YUV 數據,而不是從某個圖片或者視頻讀取,這是為了示例代碼更簡單。這部分代碼參考了 CTS - EncodeDecodeTest 中的代碼。生成的視頻如下:

onOutputBufferAvailable 回調邏輯:
11. val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0:這行代碼檢查編碼器是否已經處理完所有的輸入數據并生成了所有的輸出數據。如果是,那么isDone會被設置為true。
12. if(isDone) {…}:這個if語句檢查是否已經完成了所有的編碼工作。如果是,那么就設置outputEnd為true,表示輸出結束,并將info.size設置為0,表示沒有更多的輸出數據。
13. if(info.size > 0){…}:這個if語句檢查是否有輸出數據。如果有,那么就處理這些數據。
14. val encodedData = codec.getOutputBuffer(index):這行代碼獲取了編碼器的一個輸出緩沖區,這個緩沖區包含了編碼后的數據。
15. muxer.writeSampleData(videoTrackIndex, encodedData!!, info):這行代碼將編碼后的數據寫入到媒體混合器中。這里的videoTrackIndex是視頻軌道的索引,encodedData是編碼后的數據,info包含了這些數據的元信息,如顯示時間、大小等。
16. codec.releaseOutputBuffer(index, false):這行代碼釋放了編碼器的輸出緩沖區,讓編碼器可以繼續使用這個緩沖區來存儲新的輸出數據。這里的false表示不需要將這個緩沖區的數據顯示出來,因為我們是在編碼數據,而不是播放數據。
等待編碼結束,釋放資源
while (!outputEnd.get())
{Thread.sleep(10)
}
encoder.stop()
muxer.stop()
encoder.release()
- 在編碼線程中,我們等等編碼結束,outputEnd 是退出的標志位
- 停止 encoder 和 muxer,接著調用 release 方法釋放 encoder 資源
總結
本文介紹 MediaCodec 使用異步模式編碼的各種細節,并提供了完整的示例代碼,在示例中我們生成 YUV 數據,并配合 MediaMuxer 將編碼后的數據保存到本地 MP4 文件。
參考
- EncodeUsingBuffersActivity
- CTS - EncodeDecodeTest