原文地址
原創文章,未經做者容許不得轉載java
秋風清,秋月明
葉葉梧桐檻外聲
難教歸夢成git
歡迎你們關注個人項目MediaLearn,這是一個以學習分享音視頻知識爲目的創建的項目,目前僅侷限於Android平臺,後續會逐漸擴展。
對音視頻領域知識感興趣的朋友,歡迎一塊兒來學習!!!github
在上一篇文章Camera2錄製視頻(一):音頻的錄製及編碼,主要分享了使用Camera2搭配MediaCodeC和MediaMuxer進行視頻錄製中的音頻錄製部分。那麼在這篇文章中呢,就着手分析使用MediaCodeC完成視頻的錄製編碼和MediaMuxer完成Mux視頻合成模塊。有關使用MediaCodeC硬編碼對視頻編解碼的相關視頻,我以前也有分享,想看的朋友們能夠點擊如下傳送門回顧。數組
MediaCodeC硬編碼將圖片集編碼爲視頻Mp4文件MediaCodeC編碼視頻
MediaCodeC將視頻完整解碼,並存儲爲圖片文件。使用兩種不一樣的方式,硬編碼解碼視頻
MediaCodeC解碼視頻指定幀硬編碼解碼指定幀緩存
項目中使用的攝像頭API爲
Camera2
bash
在文章開始以前,依然是老規矩,咱們從結果導向,梳理流程。看看在視頻錄製這個階段,流程是如何運做的,數據在這其中發生了什麼變化。session
當設備的攝像頭在運轉時,sensor【傳感器】會將光信號轉爲電信號,再轉爲數字信號。sensor會輸出四種格式的圖片格式:YUV、RGB、RAW RGB DATA、JPEG。YUV是最經常使用的一種格式,YUV輸出的數據中亮度信號是無損的,RGB會有必定的損耗會丟掉一些原始信息。而RAW DATA是最原始的信息,可是存儲空間會變大,並且須要一些特定軟件才能打開。
在廢棄的攝像頭API——Camera
中,默認的預覽回調數據格式就是NV21。而在Camera2
中,函數只提供了Surface
做爲橋接對象。如果想獲取YUV、或者JPEG和RAW_SENSOR的話,可使用ImageReader
提供的Surface,再經過監聽獲取圖像信息。
不論是Camera仍是Camera2,都是支持設置Surface的。經過Surface,咱們能夠將Camera拿到的數據直接輸送到GPU經過OpenGL來渲染處理。這樣能夠不用再CPU中處理Camera幀數據,從而節省大量時間。好了,接下來我用一張流程圖,來展現MediaCodeC
如何編碼Camera幀數據的。 app
video/avc
createInputSurface
建立出一個做輸入的Input—Surface經過以上操做,咱們就能夠把Camera採集的數據直接傳入到GPU,不用在CPU中費力的處理一番。ide
在上一篇文章我提到過,會將整個視頻錄製中涉及到的各個功能模塊化,以供後續複用。那麼在視頻的錄製編碼這塊,我將它分裝爲了一個Runnable——VideoRecorder。VideoRecorder
的內部職責爲,封裝了MediaCodeC+OpenGL編碼的流程。對外提供OpenGL紋理的Surface,和硬編碼編碼後的ByteBuffer、以及BufferInfo和其餘視頻幀相關的信息。模塊化
// MediaCodeC配置
codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
val s = codec.createInputSurface()
val surfaceTexture = encodeCore.buildEGLSurface(s)
inputSurface = Surface(surfaceTexture)
// 構建一個搭載了OpenGL紋理的Surface,而後回調出去
readySurface.invoke(inputSurface)
// 開始編碼
codec.start()
// 計時
val startTime = System.nanoTime()
// 使用數組來保持視頻錄製線程和音頻錄製線程以及Mux線程的同步
while (isRecording.isNotEmpty()) {
// 編碼數據
drainEncoder(false)
frameCount++
// OpenGL 繪製
encodeCore.draw()
val curFrameTime = System.nanoTime() - startTime
encodeCore.swapData(curFrameTime)
}
// 發送編碼結束信號
drainEncoder(true)
複製代碼
以上僞代碼表明瞭視頻編碼的所有流程,首先咱們須要配置一個合適的MediaCodeC,經過MediaCodeC的codec.createInputSurface
函數獲得一個Surface對象,前文我稱之爲InputSurface。而後配置EGL環境,構建OpenGLProgram。【有關OpenGL相關的代碼,我都封裝到了SurfaceEncodeCore這個類裏面。SurfaceEncodeCore
的內部職責主要是:構建EGL環境,配置OpenGL程序,繪製紋理】。
OpenGL自己是不負責窗口管理和上下文環境管理的,這個功能由各自平臺提供。Android裏負責爲OpenGL提供窗口管理和上下文環境管理的就是EGL。在EGL裏,是使用EGLSurface將輸出渲染到設備屏幕。而建立EGLSurface有兩種方式,一種是建立一個可實際顯示的Surface,經過eglCreateWindowSurface
函數,而這個函數須要一個Surface做爲參數。另外一個是經過eglCreatePbufferSurface
建立一個離屏Surface。至此,MediaCodeC的輸入渠道就搭建完畢,這個渠道會在錄製期間,不停接受Camera回調的數據,並經過OpenGLProgram處理。咱們只須要從MediaCodeC源源不斷地提取出已經編碼好的H264碼流,對外回調視頻幀數據便可。
函數drainEncoder的實現爲:
fun MediaCodec.handleOutputBuffer(bufferInfo: MediaCodec.BufferInfo, defTimeOut: Long,
formatChanged: () -> Unit = {},
render: (bufferId: Int) -> Unit,
needEnd: Boolean = true) {
loopOut@ while (true) {
// 獲取可用的輸出緩存隊列
val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
Log.d("handleOutputBuffer", "output buffer id : $outputBufferId ")
if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
if (needEnd) {
break@loopOut
}
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
formatChanged.invoke()
} else if (outputBufferId >= 0) {
render.invoke(outputBufferId)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break@loopOut
}
}
}
}
private fun drainEncoder(isEnd: Boolean = false) {
if (isEnd) {
codec.signalEndOfInputStream()
}
codec.handleOutputBuffer(bufferInfo, 2500, {
if (!isFormatChanged) {
outputFormatChanged.invoke(codec.outputFormat)
isFormatChanged = true
}
}, {
val encodedData = codec.getOutputBuffer(it)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
Log.d(TAG, "buffer info offset ${bufferInfo.offset} time is ${bufferInfo.presentationTimeUs} ")
encodedData.position(bufferInfo.offset)
encodedData.limit(bufferInfo.offset + bufferInfo.size)
Log.d(TAG, "sent " + bufferInfo.size + " bytes to muxer")
dataCallback.invoke(frameCount, bufferInfo.presentationTimeUs, bufferInfo, encodedData)
}
codec.releaseOutputBuffer(it, false)
}, !isEnd)
}
複製代碼
這是MediaCodeC處理輸出數據的老一套代碼了,根據dequeueOutputBuffer
返回的ID,確認目前編碼器處於何種狀態。再分別加以處理,將獲得的原始數據對外回調。
至此爲止,整個視頻錄製功能中,視頻錄製編碼模塊完成、音頻錄製編碼模塊完成,只須要一個Mux模塊。將其他兩個模塊提供的數據,串聯起來輸出Mp4文件便可。
在Mux模塊中,已經沒有什麼技術含量了,具體工做就是,維護了兩個數據隊列。一個是視頻幀隊列,另外一個是音頻幀隊列。Mux模塊無限循環地從兩個隊列中提取隊列首端數據。而後比較視頻幀數據和音頻幀數據中的時間戳大小,將時間小的先行封裝便可。具體代碼可參考Muxer
mediaMuxer = MediaMuxer(p, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 添加視頻軌
val videoTrackId = mediaMuxer!!.addTrack(videoTrack)
// 添加音頻軌
val audioTrackId = mediaMuxer!!.addTrack(audioTrack)
mediaMuxer!!.start()
while (isRecording.isNotEmpty()) {
// 從隊列中提取首端數據
val videoFrame = videoQueue.firstSafe
val audioFrame = audioQueue.firstSafe
val videoTime = videoFrame?.bufferInfo?.presentationTimeUs ?: -1L
val audioTime = audioFrame?.bufferInfo?.presentationTimeUs ?: -1L
// 比較音頻幀和視頻幀的時間戳
if (videoTime == -1L && audioTime != -1L) {
writeAudio(audioTrackId)
} else if (audioTime == -1L && videoTime != -1L) {
writeVideo(videoTrackId)
} else if (audioTime != -1L && videoTime != -1L) {
// 先寫小一點的時間戳的數據
if (audioTime < videoTime) {
// 封裝音頻幀數據
writeAudio(audioTrackId)
} else {
// 封裝視頻幀數據
writeVideo(videoTrackId)
}
} else {
// do nothing
}
}
複製代碼
好了。整個視頻錄製所有功能已所有整理完畢。接下來咱們分析一個視頻的尺寸選擇問題,以及Camera2中使我迷惑的點————如何選擇視頻尺寸?
在Android官方文檔中,要想獲取Camera2攝像頭數據,必須依靠Surface。To capture or stream images from a camera device, the application must first create a camera capture session with a set of output Surfaces for use with the camera device, with createCaptureSession(SessionConfiguration).
。Camera2會根據你配置的Surface來匹配相應的尺寸,Each Surface has to be pre-configured with an appropriate size and format (if applicable) to match the sizes and formats available from the camera device
,每個Surface都必須提早配置好相應的尺寸,以便去匹配Camera2合適的Size。
Camera2在配置的時候,會返回一個可供選擇的尺寸集合,表示當前設備攝像頭所支持的全部尺寸。我在測試視頻錄製時,測試設備返回的尺寸列表以下:
那麼OK。既然知道了支持的尺寸,那麼我在配置MediaCodeC的時候,設置的寬高從這裏面選擇就ok了,就不用再進一步的圖像處理了。但是實際我試驗的結果卻不理想,在MediaCodeC設置第一個尺寸的時候,錄製的視頻畫面毫無變形。可選擇其餘尺寸譬如720 X 960、720 X 1280卻變形嚴重。可當我選擇1088 X 1088 或者 960 X 960,這種等寬高尺寸時,錄製的畫面卻毫無變形。對此我也是毫無頭緒,由於摸不清楚Surface匹配的尺寸機制問題,在OpenGL繪製的時候,就不知道該如何裁剪,這纔是大問題。若是有解決這個問題的朋友,但願能給我提一些建議。
作視頻圖像處理的時候,我走了一些彎路【手動狗頭】。以前我錯誤的斷定了Surface尺寸的來源,致使在視頻尺寸這裏進行了不正確的邏輯判斷。事實上使用SurfaceTexture的setDefaultBufferSize
函數能夠達到尺寸匹配。但相應的,Camera2返回的size是寬高相反的,因此這裏的setDefaultBufferSize
的寬高也是相反的才能匹配。