Android MediaCodec 簡明教程(五):使用 MediaCodec 編碼 ByteBuffer 數據,并保存為 MP4 文件

系列文章目錄

  1. Android MediaCodec 簡明教程(一):使用 MediaCodecList 查詢 Codec 信息,并創建 MediaCodec 編解碼器
  2. Android MediaCodec 簡明教程(二):使用 MediaCodecInfo.CodecCapabilities 查詢 Codec 支持的寬高,顏色空間等能力
  3. Android MediaCodec 簡明教程(三):詳解如何在同步與異步模式下,使用MediaCodec將視頻解碼到ByteBuffers,并在ImageView上展示
  4. 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)
  1. val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC:定義了一個字符串常量mimeType,其值為MediaFormat.MIMETYPE_VIDEO_AVC,表示我們將使用的是AVC(即H.264)編碼格式。
  2. val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight):創建一個MediaFormat對象,該對象描述了我們想要的視頻格式,包括編碼格式、視頻寬度和高度。
  3. val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS):獲取系統中所有常規(非硬件加速)的編解碼器列表。
  4. val encodeCodecName = codecList.findEncoderForFormat(format):在編解碼器列表中查找能夠處理我們指定格式的編碼器。
  5. 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是一個抽象類,它定義了四個方法:

  1. onInputBufferAvailable(MediaCodec codec, int index):當輸入緩沖區可用時,此方法被調用。參數index指示了哪個輸入緩沖區已經變得可用。

  2. onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info):當輸出緩沖區可用時,此方法被調用。參數index指示了哪個輸出緩沖區已經變得可用,info包含了關于這個緩沖區的元數據,如其包含的數據的大小,時間戳等。

  3. onError(MediaCodec codec, MediaCodec.CodecException e):當編解碼器發生錯誤時,此方法被調用。參數e是一個MediaCodec.CodecException對象,包含了關于錯誤的詳細信息。

  4. 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)
  1. colorFormat 選擇 COLOR_FormatYUV420Flexible 這是一種最常用的像素格式。
  2. 接下來這行代碼是一個斷言,它檢查編碼器是否支持上面定義的顏色格式。為了確保我們 Demo 的簡潔,我假定你的機器是一定支持 COLOR_FormatYUV420Flexible 的,否則我需要寫額外的代碼來兼容,這會使得代碼變得負責。
  3. 接著,設置了顏色格式、比特率、幀率等重要的編碼信息。
  4. 最后調用 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 回調:

  1. val pts = computePresentationTime(generateIndex):這行代碼計算了當前幀的顯示時間,通常是根據幀率和當前幀的索引來計算的。
  2. if(generateIndex == NUM_FRAMES):這行代碼檢查是否已經處理完所有的幀。如果是,那么就需要向編碼器發送一個表示輸入結束的標志。
  3. codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM):這行代碼向編碼器的輸入隊列中添加一個空的緩沖區,并設置了一個表示輸入結束的標志。這告訴編碼器不會有更多的數據輸入了。
  4. val frameData = ByteArray(videoWidth * videoHeight * 3 / 2):這行代碼創建了一個字節數組,用于存儲一幀的數據。這里假設的是YUV420格式的數據,所以大小是寬度乘以高度的1.5倍。
  5. generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData):這行代碼生成了一幀的數據。
  6. val inputBuffer = codec.getInputBuffer(index):這行代碼獲取了編碼器的一個輸入緩沖區。
  7. inputBuffer.put(frameData):這行代碼將生成的幀數據放入輸入緩沖區。
  8. codec.queueInputBuffer(index, 0, frameData.size, pts, 0):這行代碼將填充了數據的輸入緩沖區添加到編碼器的輸入隊列中。
  9. 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()
  1. 在編碼線程中,我們等等編碼結束,outputEnd 是退出的標志位
  2. 停止 encoder 和 muxer,接著調用 release 方法釋放 encoder 資源

總結

本文介紹 MediaCodec 使用異步模式編碼的各種細節,并提供了完整的示例代碼,在示例中我們生成 YUV 數據,并配合 MediaMuxer 將編碼后的數據保存到本地 MP4 文件。

參考

  • EncodeUsingBuffersActivity
  • CTS - EncodeDecodeTest

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/719163.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/719163.shtml
英文地址,請注明出處:http://en.pswp.cn/news/719163.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

php:實現字符串補零str_pad()

說明 str_pad($input_string, $total_length, $pad_string, $pad_type); $input_string 是要填充的原始字符串。$total_length 是填充后的字符串總長度,包括原始字符串的長度。$pad_string 是用于填充的字符,通常是零。$pad_type 是填充的位置&#xff0…

欲哭無淚,2024年軟考有變!中高項只考1次了

今天可能最重磅的消息是:2024年軟考工作安排及有關事項的通知文件在瘋傳,這份文件中提到了: 軟考高級方面: 信息系統項目管理師從2次改為了1年只考1次,放在了上半年考。 系統規劃與管理師依然保持1次,但是…

每日一練:LeeCode-707. 設計鏈表 【鏈表+虛擬頭結點+設計】

每日一練:LeeCode-707. 設計鏈表 【鏈表虛擬頭結點設計】 思路設置虛擬頭節點 本文是力扣 每日一練:LeeCode-707. 設計鏈表 【鏈表虛擬頭結點設計】 學習與理解過程,本文僅做學習之用,對本題感興趣的小伙伴可以出門左拐LeeCode-70…

0101二階與三階行列式-行列式-線性代數

一 引例 求解二元一次方程組 { a 11 x 1 a 12 x 2 b 1 a 21 x 1 a 22 x 2 b 2 \begin{cases} a_{11}x_1a_{12}x_2b_1\\ a_{21}x_1a_{22}x_2b_2\\ \end{cases} {a11?x1?a12?x2?b1?a21?x1?a22?x2?b2?? 解: 1 a 21 ? 2 a 11 ? x 2 a 11 b 2 ? a…

Python函數的閉包

嵌套函數 在一個函數內部定義的函數稱為嵌套函數 閉包的形成 內層函數對外層函數非全局變量的引用就會形成閉包 閉包作用 保證數據安全 例子 li [] def average(value):li.append(value)return sum(li)/len(li) 如上面代碼li[]這個列表人人都能修改,這樣就…

自然語言處理實戰項目26-NLP模型訓練中前置應用之分詞方法的應用

大家好,我是微學AI,今天給大家介紹一下自然語言處理實戰項目26-NLP模型訓練中前置應用之分詞方法的應用。本文詳細介紹了自然語言處理(NLP)模型訓練中前置應用之分詞方法的應用。文章首先簡要概述了NLP的概念和分詞在其中的重要性。隨后,文章詳細介紹了四種主要的分詞方法…

MQL5學習之簡單移動平均線MA的編寫

昨天還是有點高估自己了,MACD相對較難一點,改學MA的編寫,首先明確MA的計算,假如有4個值,p[1,2, 3, 4], period3, 則v[0]p[0], v[1]p[1],v[2](p[0]p[1]p[2])/32, v[3](v[2]*3p[3]-p…

瀏覽器展示Blob/File文件

1. 瀏覽器展示Blob/File文件 I.Blob格式轉Base64格式 當我們接收到后端傳輸過來的文件時,很多時候我們需要將傳過來的文件轉為Base64格式。如后端傳來驗證碼圖片時等 下面將提供函數: // Blob轉Base64 export const blobToBase64 (blob: Blob) >ne…

ChatGPT論文指南|ChatGPT如何助力論文中的數據分析!【建議收藏】

點擊下方▼▼▼▼鏈接直達AIPaperPass ! AIPaperPass - AI論文寫作指導平臺 公眾號原文▼▼▼▼: ChatGPT論文指南|ChatGPT如何助力論文中的數據分析!【建議收藏】 小編在之前的論文寫作流程中,介紹了大量論文文字工作&#xff…

Effective objective-c-- 內存管理

Effective objective-c-- 內存管理 前言理解引用計數引用計數工作原理屬性存取方法中的內存管理自動釋放池保留環要點 以ARC簡化引用計數使用ARC時必須遵循的方法和命名規則變量的內存管理語義ARC如何清理實例變量覆寫內存管理方法要點 在dealloc方法中只釋放引用并解除監聽要點…

Mybatis-Plus 5分鐘快速上手,10分鐘熟練使用

小伙伴們好,歡迎關注,一起學習,無限進步 以下為學習 mybatis-plus 過程中的筆記 mybatis-plus 官網地址:https://mp.baomidou.com/ 文章目錄 特性快速開始mybatis-plus 配置插入測試及雪花算法主鍵生成策略查詢更新刪除查詢指定字…

Text2SQL 和 智能問答 的提示詞寫法

Text2SQL 生成 Query SQL System Message You are a {dialect} expert. Given an input question, creat a syntactically correct {dialect} query to run. Unless the user specifies in the question a specific number of examples to obtain, query for at most {top_k} r…

Linux 創建.NET 服務

文章目錄 創建服務啟用服務啟動 & 重啟服務查看服務狀態問題排查 創建服務 將服務文件上傳到 /home/mes/api-mes-dev, 其他服務修改對應的目錄在 /usr/lib/systemd/system/ 創建 mesapi-dev.service, 其他服務修改對應文件名 [Unit] Descriptionmesapi-dev service[Servi…

探索Linux世界:初次接觸和基本指令(文件操作)

文章目錄 1.基本介紹和準備2.基本指令和Linux的基本操作3.幾個重要基本指令3.1 ls - 列出文件和目錄3.1.1文件的知識3.1.2 .和..文件 3.2pwd - 顯示當前工作目錄3.2.1路徑知識 3.3 cd - 切換目錄3.4 touch - 創建文件或更新時間戳3.5mkdir - 創建新目錄3.6rm - 刪除文件或目錄3…

leetcode熱題100學習計劃-鏈表-反轉鏈表

思路 使用頭插法逆轉鏈表 注:鏈表一般為操作方便,頭結點不存值,是一個虛擬節點 代碼 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val)…

深入了解 Android 中的 FrameLayout 布局

FrameLayout 是 Android 中常用的布局之一&#xff0c;它允許子視圖堆疊在一起&#xff0c;可以在不同位置放置子視圖。在這篇博客中&#xff0c;我們將詳細介紹 FrameLayout 的屬性及其作用。 <FrameLayout xmlns:android"http://schemas.android.com/apk/res/androi…

【數據結構和算法初階(C語言)】帶環鏈表問題詳解(快慢指針的燒腦應用)

目錄 1.鋪墊-----帶環鏈表基本了解 2. 題目&#xff1a;環形鏈表 3.環形鏈表|| ?編輯 3.1題解1 3.2 題解2 4.總結 1.鋪墊-----帶環鏈表基本了解 環形鏈表題目啟迪&#xff1a; 環形鏈表特點&#xff1a;遍歷鏈表會出現一模一樣的地址 2. 題目&#xff1a;環形鏈表 給…

數字化轉型導師鵬:政府數字化轉型政務服務類案例研究

政府數字化轉型政務服務類案例研究 課程背景&#xff1a; 很多地方政府存在以下問題&#xff1a; 不清楚標桿省政府數字化轉型的政務服務類成功案例 不清楚地級市政府數字化轉型的政務服務類成功案例 不清楚縣區級政府數字化轉型的政務服務類成功案例 課程特色&#x…

基于C語言實現內存型數據庫(kv存儲)

基于C語言實現內存型數據庫(kv存儲) 文章目錄 基于C語言實現內存型數據庫(kv存儲)1. 項目背景1.1 Redis介紹1.2 項目預期及基本架構 2. 服務端原理及代碼框架2.1 網絡數據回環的實現2.2 array的實現2.3 rbtree的實現2.4 btree的實現2.5 hash的實現2.6 dhash的實現2.7 skiplist的…

XV4001KC數字輸出 車載用(piezoman)

EPSON的XV4001KC角速度傳感器是為滿足汽車行業對高精度和高可靠性需求而設計的。它不僅提供了高級的運動監測特性&#xff0c;高精度的角速度測量和溫度監測功能&#xff0c;而且其緊湊的設計6.04.83.3mm尺寸對于空間受限的車載環境來說&#xff0c;是一大優勢&#xff0c;使得…