
【聲 明】
首先,這一系列文章均基于自己的理解和實(shí)踐,可能有不對(duì)的地方,歡迎大家指正。
其次,這是一個(gè)入門系列,涉及的知識(shí)也僅限于夠用,深入的知識(shí)網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后,寫文章過程中,會(huì)借鑒參考其他人分享的文章,會(huì)在文章最后列出,感謝這些作者的分享。
碼字不易,轉(zhuǎn)載請(qǐng)注明出處!
| 教程代碼:【Github傳送門】 |
|---|
目錄
一、Android音視頻硬解碼篇:
- 1,音視頻基礎(chǔ)知識(shí)
- 2,音視頻硬解碼流程:封裝基礎(chǔ)解碼框架
- 3,音視頻播放:音視頻同步
- 4,音視頻解封和封裝:生成一個(gè)MP4
二、使用OpenGL渲染視頻畫面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻,實(shí)現(xiàn)畫中畫
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO數(shù)據(jù)緩沖區(qū)
- 6,Android音視頻硬編碼:生成一個(gè)MP4
三、Android FFmpeg音視頻解碼篇
- 1,F(xiàn)Fmpeg so庫(kù)編譯
- 2,Android 引入FFmpeg
- 3,Android FFmpeg視頻解碼播放
- 4,Android FFmpeg+OpenSL ES音頻解碼播放
- 5,Android FFmpeg+OpenGL ES播放視頻
- 6,Android FFmpeg簡(jiǎn)單合成MP4:視屏解封與重新封裝
- 7,Android FFmpeg視頻編碼
本文你可以了解到
本文主要簡(jiǎn)介Android使用硬解碼API實(shí)現(xiàn)硬解碼的流程,包含MediaCodec輸入輸出緩沖、MediaCodec解碼流程、解碼代碼封裝和講解。
一、簡(jiǎn)介
MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口,同時(shí)支持音視頻的編碼和解碼。
一定要好好理解接下來這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。
數(shù)據(jù)流
首先,來看看MediaCodec的數(shù)據(jù)流,也是官方Api文檔中的,很多文章都會(huì)引用。
仔細(xì)看一下,MediaCodec將數(shù)據(jù)分為兩部分,分別為input(左邊)和output(右邊),即輸入和輸出兩個(gè)數(shù)據(jù)緩沖區(qū)。
input:是給客戶端輸入需要解碼的數(shù)據(jù)(解碼時(shí))或者需要編碼的數(shù)據(jù)(編碼時(shí))。
output:是輸出解碼好(解碼時(shí))或者編碼好(編碼時(shí))的數(shù)據(jù)給客戶端。
MediaCodec內(nèi)部使用異步的方式對(duì)input和output數(shù)據(jù)進(jìn)行處理。MediaCodec將處理好input的數(shù)據(jù),填充到output緩沖區(qū),交給客戶端渲染或處理
注:客戶端處理完數(shù)據(jù)后,必須手動(dòng)釋放output緩沖區(qū),否則將會(huì)導(dǎo)致MediaCodec輸出緩沖被占用,無法繼續(xù)解碼。
狀態(tài)
依然是一副來自官方的狀態(tài)圖

再仔細(xì)看看這幅圖,整體上分為三個(gè)大的狀態(tài):Sotpped、Executing、Released。
- Stoped:包含了3個(gè)小狀態(tài):Error、Uninitialized、Configured。
首先,新建MediaCodec后,會(huì)進(jìn)入U(xiǎn)ninitialized狀態(tài);
其次,調(diào)用configure方法配置參數(shù)后,會(huì)進(jìn)入Configured;
- Executing:同樣包含3個(gè)小狀態(tài):Flushed、Running、End of Stream。
再次,調(diào)用start方法后,MediaCodec進(jìn)入Flushed狀態(tài);
接著,調(diào)用dequeueInputBuffer方法后,進(jìn)入Running狀態(tài);
最后,當(dāng)解碼/編碼結(jié)束時(shí),進(jìn)入End of Stream(EOF)狀態(tài)。
這時(shí),一個(gè)視頻就處理完成了。
- Released:最后,如果想結(jié)束整個(gè)數(shù)據(jù)處理過程,可以調(diào)用release方法,釋放所有的資源。
那么,F(xiàn)lushed是什么狀態(tài)呢?
從圖中我們可以看到,在Running或者End of Stream狀態(tài)時(shí),都可以調(diào)用flush方法,重新進(jìn)入Flushed狀態(tài)。
當(dāng)我們?cè)诮獯a過程中,進(jìn)入了End of Stream后,解碼器就不再接收輸入了,這時(shí)候,需要調(diào)用flush方法,重新進(jìn)入接收數(shù)據(jù)狀態(tài)。
或者,我們?cè)诓シ乓曨l過程中,想進(jìn)行跳播,這時(shí)候,我們需要Seek到指定的時(shí)間點(diǎn),這時(shí)候,也需要調(diào)用flush方法,清除緩沖,否則解碼時(shí)間戳?xí)靵y。
再次強(qiáng)調(diào)一下,一定要好好理解這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。
二、解碼流程
MediaCodec有兩種工作模式,分別為異步模式和同步模式,這里我們使用同步模式,異步模式可以參考官網(wǎng)例子。
根據(jù)官方的數(shù)據(jù)流圖和狀態(tài)圖,畫出一個(gè)最基礎(chǔ)的解碼流程如下:

經(jīng)過初始化和配置以后,進(jìn)入循環(huán)解碼流程,不斷的輸入數(shù)據(jù),然后獲取解碼完數(shù)據(jù),最后渲染出來,直到所有數(shù)據(jù)解碼完成(End of Stream)。
三、開始解碼
根據(jù)上面的流程圖,可以發(fā)現(xiàn),無論音頻還是視頻,解碼流程基本是一致的,不同的地方只在于【配置】、【渲染】?jī)蓚€(gè)部分。
定義解碼器
因此,我們將整個(gè)解碼流程抽象為一個(gè)解碼基類:BaseDecoder,為了規(guī)范代碼和更好的拓展性,我們先定義一個(gè)解碼器:IDecoder,繼承Runnable。
interface IDecoder: Runnable {
/**
* 暫停解碼
*/
fun pause()
/**
* 繼續(xù)解碼
*/
fun goOn()
/**
* 停止解碼
*/
fun stop()
/**
* 是否正在解碼
*/
fun isDecoding(): Boolean
/**
* 是否正在快進(jìn)
*/
fun isSeeking(): Boolean
/**
* 是否停止解碼
*/
fun isStop(): Boolean
/**
* 設(shè)置狀態(tài)監(jiān)聽器
*/
fun setStateListener(l: IDecoderStateListener?)
/**
* 獲取視頻寬
*/
fun getWidth(): Int
/**
* 獲取視頻高
*/
fun getHeight(): Int
/**
* 獲取視頻長(zhǎng)度
*/
fun getDuration(): Long
/**
* 獲取視頻旋轉(zhuǎn)角度
*/
fun getRotationAngle(): Int
/**
* 獲取音視頻對(duì)應(yīng)的格式參數(shù)
*/
fun getMediaFormat(): MediaFormat?
/**
* 獲取音視頻對(duì)應(yīng)的媒體軌道
*/
fun getTrack(): Int
/**
* 獲取解碼的文件路徑
*/
fun getFilePath(): String
}
定義了解碼器的一些基礎(chǔ)操作,如暫停/繼續(xù)/停止解碼,獲取視頻的時(shí)長(zhǎng),視頻的寬高,解碼狀態(tài)等等
為什么繼承Runnable?
這里使用的是同步模式解碼,需要不斷循環(huán)壓入和拉取數(shù)據(jù),是一個(gè)耗時(shí)操作,因此,我們將解碼器定義為一個(gè)Runnable,最后放到線程池中執(zhí)行。
接著,繼承IDecoder,定義基礎(chǔ)解碼器BaseDecoder。
首先來看下基礎(chǔ)參數(shù):
abstract class BaseDecoder: IDecoder {
//-------------線程相關(guān)------------------------
/**
* 解碼器是否在運(yùn)行
*/
private var mIsRunning = true
/**
* 線程等待鎖
*/
private val mLock = Object()
/**
* 是否可以進(jìn)入解碼
*/
private var mReadyForDecode = false
//---------------解碼相關(guān)-----------------------
/**
* 音視頻解碼器
*/
protected var mCodec: MediaCodec? = null
/**
* 音視頻數(shù)據(jù)讀取器
*/
protected var mExtractor: IExtractor? = null
/**
* 解碼輸入緩存區(qū)
*/
protected var mInputBuffers: Array<ByteBuffer>? = null
/**
* 解碼輸出緩存區(qū)
*/
protected var mOutputBuffers: Array<ByteBuffer>? = null
/**
* 解碼數(shù)據(jù)信息
*/
private var mBufferInfo = MediaCodec.BufferInfo()
private var mState = DecodeState.STOP
private var mStateListener: IDecoderStateListener? = null
/**
* 流數(shù)據(jù)是否結(jié)束
*/
private var mIsEOS = false
protected var mVideoWidth = 0
protected var mVideoHeight = 0
//省略后面的方法
....
}
首先,我們定義了線程相關(guān)的資源,用于判斷是否持續(xù)解碼的mIsRunning,掛起線程的mLock等。
然后,就是解碼相關(guān)的資源了,比如MdeiaCodec本身,輸入輸出緩沖,解碼狀態(tài)等等。
其中,有一個(gè)解碼狀態(tài)DecodeState和音視頻數(shù)據(jù)讀取器IExtractor。
定義解碼狀態(tài)
為了方便記錄解碼狀態(tài),這里使用一個(gè)枚舉類表示
enum class DecodeState {
/**開始狀態(tài)*/
START,
/**解碼中*/
DECODING,
/**解碼暫停*/
PAUSE,
/**正在快進(jìn)*/
SEEKING,
/**解碼完成*/
FINISH,
/**解碼器釋放*/
STOP
}
定義音視頻數(shù)據(jù)分離器
前面說過,MediaCodec需要我們不斷地喂數(shù)據(jù)給輸入緩沖,那么數(shù)據(jù)從哪里來呢?肯定是音視頻文件了,這里的IExtractor就是用來提取音視頻文件中數(shù)據(jù)流。
Android自帶有一個(gè)音視頻數(shù)據(jù)讀取器MediaExtractor,同樣為了方便維護(hù)和拓展性,我們依然先定一個(gè)讀取器IExtractor。
interface IExtractor {
/**
* 獲取音視頻格式參數(shù)
*/
fun getFormat(): MediaFormat?
/**
* 讀取音視頻數(shù)據(jù)
*/
fun readBuffer(byteBuffer: ByteBuffer): Int
/**
* 獲取當(dāng)前幀時(shí)間
*/
fun getCurrentTimestamp(): Long
/**
* Seek到指定位置,并返回實(shí)際幀的時(shí)間戳
*/
fun seek(pos: Long): Long
fun setStartPos(pos: Long)
/**
* 停止讀取數(shù)據(jù)
*/
fun stop()
}
最重要的一個(gè)方法就是readBuffer,用于讀取音視頻數(shù)據(jù)流
定義解碼流程
前面我們只貼出了解碼器的參數(shù)部分,接下來,貼出最重要的部分,也就是解碼流程部分。
abstract class BaseDecoder: IDecoder {
//省略參數(shù)定義部分,見上
.......
final override fun run() {
mState = DecodeState.START
mStateListener?.decoderPrepare(this)
//【解碼步驟:1. 初始化,并啟動(dòng)解碼器】
if (!init()) return
while (mIsRunning) {
if (mState != DecodeState.START &&
mState != DecodeState.DECODING &&
mState != DecodeState.SEEKING) {
waitDecode()
}
if (!mIsRunning ||
mState == DecodeState.STOP) {
mIsRunning = false
break
}
//如果數(shù)據(jù)沒有解碼完畢,將數(shù)據(jù)推入解碼器解碼
if (!mIsEOS) {
//【解碼步驟:2. 將數(shù)據(jù)壓入解碼器輸入緩沖】
mIsEOS = pushBufferToDecoder()
}
//【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】
val index = pullBufferFromDecoder()
if (index >= 0) {
//【解碼步驟:4. 渲染】
render(mOutputBuffers!![index], mBufferInfo)
//【解碼步驟:5. 釋放輸出緩沖】
mCodec!!.releaseOutputBuffer(index, true)
if (mState == DecodeState.START) {
mState = DecodeState.PAUSE
}
}
//【解碼步驟:6. 判斷解碼是否完成】
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
}
doneDecode()
//【解碼步驟:7. 釋放解碼器】
release()
}
/**
* 解碼線程進(jìn)入等待
*/
private fun waitDecode() {
try {
if (mState == DecodeState.PAUSE) {
mStateListener?.decoderPause(this)
}
synchronized(mLock) {
mLock.wait()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* 通知解碼線程繼續(xù)運(yùn)行
*/
protected fun notifyDecode() {
synchronized(mLock) {
mLock.notifyAll()
}
if (mState == DecodeState.DECODING) {
mStateListener?.decoderRunning(this)
}
}
/**
* 渲染
*/
abstract fun render(outputBuffers: ByteBuffer,
bufferInfo: MediaCodec.BufferInfo)
/**
* 結(jié)束解碼
*/
abstract fun doneDecode()
}
在Runnable的run回調(diào)方法中,集成了整個(gè)解碼流程:
- 【解碼步驟:1. 初始化,并啟動(dòng)解碼器】
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun init(): Boolean {
//1.檢查參數(shù)是否完整
if (mFilePath.isEmpty() || File(mFilePath).exists()) {
Log.w(TAG, "文件路徑為空")
mStateListener?.decoderError(this, "文件路徑為空")
return false
}
//調(diào)用虛函數(shù),檢查子類參數(shù)是否完整
if (!check()) return false
//2.初始化數(shù)據(jù)提取器
mExtractor = initExtractor(mFilePath)
if (mExtractor == null ||
mExtractor!!.getFormat() == null) return false
//3.初始化參數(shù)
if (!initParams()) return false
//4.初始化渲染器
if (!initRender()) return false
//5.初始化解碼器
if (!initCodec()) return false
return true
}
private fun initParams(): Boolean {
try {
val format = mExtractor!!.getFormat()!!
mDuration = format.getLong(MediaFormat.KEY_DURATION) / 1000
if (mEndPos == 0L) mEndPos = mDuration
initSpecParams(mExtractor!!.getFormat()!!)
} catch (e: Exception) {
return false
}
return true
}
private fun initCodec(): Boolean {
try {
//1.根據(jù)音視頻編碼格式初始化解碼器
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
mCodec = MediaCodec.createDecoderByType(type)
//2.配置解碼器
if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
waitDecode()
}
//3.啟動(dòng)解碼器
mCodec!!.start()
//4.獲取解碼器緩沖區(qū)
mInputBuffers = mCodec?.inputBuffers
mOutputBuffers = mCodec?.outputBuffers
} catch (e: Exception) {
return false
}
return true
}
/**
* 檢查子類參數(shù)
*/
abstract fun check(): Boolean
/**
* 初始化數(shù)據(jù)提取器
*/
abstract fun initExtractor(path: String): IExtractor
/**
* 初始化子類自己特有的參數(shù)
*/
abstract fun initSpecParams(format: MediaFormat)
/**
* 初始化渲染器
*/
abstract fun initRender(): Boolean
/**
* 配置解碼器
*/
abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
}
初始化方法中,分為5個(gè)步驟,看起很復(fù)雜,實(shí)際很簡(jiǎn)單。
檢查參數(shù)是否完整:路徑是否有效等
初始化數(shù)據(jù)提取器:初始化Extractor
初始化參數(shù):提取一些必須的參數(shù),duration,width,height等
初始化渲染器:視頻不需要,音頻為AudioTracker
-
初始化解碼器:初始化MediaCodec
在initCodec()中,
val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME) mCodec = MediaCodec.createDecoderByType(type)
初始化MediaCodec的時(shí)候:
- 首先,通過Extractor獲取到音視頻數(shù)據(jù)的編碼信息MediaFormat;
- 然后,查詢MediaFormat中的編碼類型(如video/avc,即H264;audio/mp4a-latm,即AAC);
- 最后,調(diào)用createDecoderByType創(chuàng)建解碼器。
需要說明的是:由于音頻和視頻的初始化稍有不同,所以定義了幾個(gè)虛函數(shù),將不同的東西交給子類去實(shí)現(xiàn)。具體將在下一篇文章[音視頻播放:音視頻同步]說明。
- 【解碼步驟:2. 將數(shù)據(jù)壓入解碼器輸入緩沖】
直接進(jìn)入pushBufferToDecoder方法中
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun pushBufferToDecoder(): Boolean {
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
var isEndOfStream = false
if (inputBufferIndex >= 0) {
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
if (sampleSize < 0) {
//如果數(shù)據(jù)已經(jīng)取完,壓入數(shù)據(jù)結(jié)束標(biāo)志:BUFFER_FLAG_END_OF_STREAM
mCodec!!.queueInputBuffer(inputBufferIndex, 0, 0,
0, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
isEndOfStream = true
} else {
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
}
}
return isEndOfStream
}
}
調(diào)用了以下方法:
- 查詢是否有可用的輸入緩沖,返回緩沖索引。其中參數(shù)2000為等待2000ms,如果填入-1則無限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
- 通過緩沖索引 inputBufferIndex 獲取可用的緩沖區(qū),并使用Extractor提取待解碼數(shù)據(jù),填充到緩沖區(qū)中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
- 調(diào)用queueInputBuffer將數(shù)據(jù)壓入解碼器。
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
注意:如果SampleSize返回-1,說明沒有更多的數(shù)據(jù)了。
這個(gè)時(shí)候,queueInputBuffer的最后一個(gè)參數(shù)要傳入結(jié)束標(biāo)記MediaCodec.BUFFER_FLAG_END_OF_STREAM。
- 【解碼步驟:3. 將解碼好的數(shù)據(jù)從緩沖區(qū)拉取出來】
直接進(jìn)入pullBufferFromDecoder()
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun pullBufferFromDecoder(): Int {
// 查詢是否有解碼完成的數(shù)據(jù),index >=0 時(shí),表示數(shù)據(jù)有效,并且index為緩沖區(qū)索引
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
when (index) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {}
MediaCodec.INFO_TRY_AGAIN_LATER -> {}
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
mOutputBuffers = mCodec!!.outputBuffers
}
else -> {
return index
}
}
return -1
}
}
第一、調(diào)用dequeueOutputBuffer方法查詢是否有解碼完成的可用數(shù)據(jù),其中mBufferInfo用于獲取數(shù)據(jù)幀信息,第二參數(shù)是等待時(shí)間,這里等待1000ms,填入-1是無限等待。
var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
第二、判斷index類型:
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:輸出格式改變了
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:輸入緩沖改變了
MediaCodec.INFO_TRY_AGAIN_LATER:沒有可用數(shù)據(jù),等會(huì)再來
大于等于0:有可用數(shù)據(jù),index就是輸出緩沖索引
- 【解碼步驟:4. 渲染】
這里調(diào)用了一個(gè)虛函數(shù)render,也就是將渲染交給子類
- 【解碼步驟:5. 釋放輸出緩沖】
調(diào)用releaseOutputBuffer方法, 釋放輸出緩沖區(qū)。
注:第二個(gè)參數(shù),是個(gè)boolean,命名為render,這個(gè)參數(shù)在視頻解碼時(shí),用于決定是否要將這一幀數(shù)據(jù)顯示出來。
mCodec!!.releaseOutputBuffer(index, true)
- 【解碼步驟:6. 判斷解碼是否完成】
還記得我們?cè)诎褦?shù)據(jù)壓入解碼器時(shí),當(dāng)sampleSize < 0 時(shí),壓入了一個(gè)結(jié)束標(biāo)記嗎?
當(dāng)接收到這個(gè)標(biāo)志后,解碼器就知道所有數(shù)據(jù)已經(jīng)接收完畢,在所有數(shù)據(jù)解碼完成以后,會(huì)在最后一幀數(shù)據(jù)加上結(jié)束標(biāo)記信息,即
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
mState = DecodeState.FINISH
mStateListener?.decoderFinish(this)
}
- 【解碼步驟:7. 釋放解碼器】
在while循環(huán)結(jié)束后,釋放掉所有的資源。至此,一次解碼結(jié)束。
abstract class BaseDecoder: IDecoder {
//省略上面已有代碼
......
private fun release() {
try {
mState = DecodeState.STOP
mIsEOS = false
mExtractor?.stop()
mCodec?.stop()
mCodec?.release()
mStateListener?.decoderDestroy(this)
} catch (e: Exception) {
}
}
}
最后,解碼器定義的其他方法(如pause、goOn、stop等)不再細(xì)說,可查看工程源碼。
結(jié)尾
本來打算把音頻和視頻播放部分也放到本篇來講,最后發(fā)現(xiàn)篇幅太長(zhǎng),不利于閱讀,看了會(huì)累。所以把真正實(shí)現(xiàn)播放部分和下一篇【音視頻播放:音視頻同步】做一個(gè)整合,內(nèi)容和長(zhǎng)度都會(huì)更合理。
so,下一篇見!