【Android 音視頻開發(fā)打怪升級:音視頻硬解碼篇】二、音視頻硬解碼流程:封裝基礎(chǔ)解碼框架

【聲 明】

首先,這一系列文章均基于自己的理解和實(shí)踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網(wǎng)上也有許許多多的博文供大家學(xué)習(xí)了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。

碼字不易,轉(zhuǎn)載請注明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三、Android FFmpeg音視頻解碼篇

本文你可以了解到

本文主要簡介Android使用硬解碼API實(shí)現(xiàn)硬解碼的流程,包含MediaCodec輸入輸出緩沖、MediaCodec解碼流程、解碼代碼封裝和講解。

一、簡介

MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口,同時支持音視頻的編碼和解碼。

一定要好好理解接下來這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。

數(shù)據(jù)流

首先,來看看MediaCodec的數(shù)據(jù)流,也是官方Api文檔中的,很多文章都會引用。

仔細(xì)看一下,MediaCodec將數(shù)據(jù)分為兩部分,分別為input(左邊)和output(右邊),即輸入和輸出兩個數(shù)據(jù)緩沖區(qū)。

input:是給客戶端輸入需要解碼的數(shù)據(jù)(解碼時)或者需要編碼的數(shù)據(jù)(編碼時)。

output:是輸出解碼好(解碼時)或者編碼好(編碼時)的數(shù)據(jù)給客戶端。

MediaCodec內(nèi)部使用異步的方式對input和output數(shù)據(jù)進(jìn)行處理。MediaCodec將處理好input的數(shù)據(jù),填充到output緩沖區(qū),交給客戶端渲染或處理

注:客戶端處理完數(shù)據(jù)后,必須手動釋放output緩沖區(qū),否則將會導(dǎo)致MediaCodec輸出緩沖被占用,無法繼續(xù)解碼。

狀態(tài)

依然是一副來自官方的狀態(tài)圖

再仔細(xì)看看這幅圖,整體上分為三個大的狀態(tài):Sotpped、Executing、Released。

  • Stoped:包含了3個小狀態(tài):Error、Uninitialized、Configured。

首先,新建MediaCodec后,會進(jìn)入Uninitialized狀態(tài);
其次,調(diào)用configure方法配置參數(shù)后,會進(jìn)入Configured;

  • Executing:同樣包含3個小狀態(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é)束時,進(jìn)入End of Stream(EOF)狀態(tài)。
這時,一個視頻就處理完成了。

  • Released:最后,如果想結(jié)束整個數(shù)據(jù)處理過程,可以調(diào)用release方法,釋放所有的資源。

那么,F(xiàn)lushed是什么狀態(tài)呢?

從圖中我們可以看到,在Running或者End of Stream狀態(tài)時,都可以調(diào)用flush方法,重新進(jìn)入Flushed狀態(tài)。

當(dāng)我們在解碼過程中,進(jìn)入了End of Stream后,解碼器就不再接收輸入了,這時候,需要調(diào)用flush方法,重新進(jìn)入接收數(shù)據(jù)狀態(tài)。

或者,我們在播放視頻過程中,想進(jìn)行跳播,這時候,我們需要Seek到指定的時間點(diǎn),這時候,也需要調(diào)用flush方法,清除緩沖,否則解碼時間戳?xí)靵y。

再次強(qiáng)調(diào)一下,一定要好好理解這兩幅圖,因?yàn)楹罄m(xù)的代碼就是基于這兩幅圖來編寫的。

二、解碼流程

MediaCodec有兩種工作模式,分別為異步模式和同步模式,這里我們使用同步模式,異步模式可以參考官網(wǎng)例子。

根據(jù)官方的數(shù)據(jù)流圖和狀態(tài)圖,畫出一個最基礎(chǔ)的解碼流程如下:

經(jīng)過初始化和配置以后,進(jìn)入循環(huán)解碼流程,不斷的輸入數(shù)據(jù),然后獲取解碼完數(shù)據(jù),最后渲染出來,直到所有數(shù)據(jù)解碼完成(End of Stream)。

三、開始解碼

根據(jù)上面的流程圖,可以發(fā)現(xiàn),無論音頻還是視頻,解碼流程基本是一致的,不同的地方只在于【配置】、【渲染】兩個部分。

定義解碼器

因此,我們將整個解碼流程抽象為一個解碼基類:BaseDecoder,為了規(guī)范代碼和更好的拓展性,我們先定義一個解碼器: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

    /**
     * 獲取視頻長度
     */
    fun getDuration(): Long

    /**
     * 獲取視頻旋轉(zhuǎn)角度
     */
    fun getRotationAngle(): Int

    /**
     * 獲取音視頻對應(yīng)的格式參數(shù)
     */
    fun getMediaFormat(): MediaFormat?

    /**
     * 獲取音視頻對應(yīng)的媒體軌道
     */
    fun getTrack(): Int

    /**
     * 獲取解碼的文件路徑
     */
    fun getFilePath(): String
}
復(fù)制代碼

定義了解碼器的一些基礎(chǔ)操作,如暫停/繼續(xù)/停止解碼,獲取視頻的時長,視頻的寬高,解碼狀態(tài)等等

為什么繼承Runnable?

這里使用的是同步模式解碼,需要不斷循環(huán)壓入和拉取數(shù)據(jù),是一個耗時操作,因此,我們將解碼器定義為一個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

    //省略后面的方法
    ....
}
復(fù)制代碼
  • 首先,我們定義了線程相關(guān)的資源,用于判斷是否持續(xù)解碼的mIsRunning,掛起線程的mLock等。

  • 然后,就是解碼相關(guān)的資源了,比如MdeiaCodec本身,輸入輸出緩沖,解碼狀態(tài)等等。

  • 其中,有一個解碼狀態(tài)DecodeState和音視頻數(shù)據(jù)讀取器IExtractor。

定義解碼狀態(tài)

為了方便記錄解碼狀態(tài),這里使用一個枚舉類表示

enum class DecodeState {
    /**開始狀態(tài)*/
    START,
    /**解碼中*/
    DECODING,
    /**解碼暫停*/
    PAUSE,
    /**正在快進(jìn)*/
    SEEKING,
    /**解碼完成*/
    FINISH,
    /**解碼器釋放*/
    STOP
}
復(fù)制代碼

定義音視頻數(shù)據(jù)分離器

前面說過,MediaCodec需要我們不斷地喂數(shù)據(jù)給輸入緩沖,那么數(shù)據(jù)從哪里來呢?肯定是音視頻文件了,這里的IExtractor就是用來提取音視頻文件中數(shù)據(jù)流。

Android自帶有一個音視頻數(shù)據(jù)讀取器MediaExtractor,同樣為了方便維護(hù)和拓展性,我們依然先定一個讀取器IExtractor。

interface IExtractor {
    /**
     * 獲取音視頻格式參數(shù)
     */
    fun getFormat(): MediaFormat?

    /**
     * 讀取音視頻數(shù)據(jù)
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int

    /**
     * 獲取當(dāng)前幀時間
     */
    fun getCurrentTimestamp(): Long

    /**
     * Seek到指定位置,并返回實(shí)際幀的時間戳
     */
    fun seek(pos: Long): Long

    fun setStartPos(pos: Long)

    /**
     * 停止讀取數(shù)據(jù)
     */
    fun stop()
}
復(fù)制代碼

最重要的一個方法就是readBuffer,用于讀取音視頻數(shù)據(jù)流

定義解碼流程

前面我們只貼出了解碼器的參數(shù)部分,接下來,貼出最重要的部分,也就是解碼流程部分。

abstract class BaseDecoder: IDecoder {
    //省略參數(shù)定義部分,見上
    .......

    final override fun run() {
        mState = DecodeState.START
        mStateListener?.decoderPrepare(this)

        //【解碼步驟:1\. 初始化,并啟動解碼器】
        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()
}
復(fù)制代碼

在Runnable的run回調(diào)方法中,集成了整個解碼流程:

  • 【解碼步驟:1. 初始化,并啟動解碼器】
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.啟動解碼器
            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
}
復(fù)制代碼

初始化方法中,分為5個步驟,看起很復(fù)雜,實(shí)際很簡單。

  1. 檢查參數(shù)是否完整:路徑是否有效等

  2. 初始化數(shù)據(jù)提取器:初始化Extractor

  3. 初始化參數(shù):提取一些必須的參數(shù):duration,width,height等

  4. 初始化渲染器:視頻不需要,音頻為AudioTracker

  5. 初始化解碼器:初始化MediaCodec

    在initCodec()中,

    val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
    mCodec = MediaCodec.createDecoderByType(type)
    復(fù)制代碼
    

初始化MediaCodec的時候:

  1. 首先,通過Extractor獲取到音視頻數(shù)據(jù)的編碼信息MediaFormat;
  2. 然后,查詢MediaFormat中的編碼類型(如video/avc,即H264;audio/mp4a-latm,即AAC);
  3. 最后,調(diào)用createDecoderByType創(chuàng)建解碼器。

需要說明的是:由于音頻和視頻的初始化稍有不同,所以定義了幾個虛函數(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
    }
}
復(fù)制代碼

調(diào)用了以下方法:

  1. 查詢是否有可用的輸入緩沖,返回緩沖索引。其中參數(shù)2000為等待2000ms,如果填入-1則無限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
復(fù)制代碼
  1. 通過緩沖索引 inputBufferIndex 獲取可用的緩沖區(qū),并使用Extractor提取待解碼數(shù)據(jù),填充到緩沖區(qū)中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
復(fù)制代碼
  1. 調(diào)用queueInputBuffer將數(shù)據(jù)壓入解碼器。
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
    sampleSize, mExtractor!!.getCurrentTimestamp(), 0)
復(fù)制代碼

注意:如果SampleSize返回-1,說明沒有更多的數(shù)據(jù)了。
這個時候,queueInputBuffer的最后一個參數(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ù)據(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
    }
}
復(fù)制代碼

第一、調(diào)用dequeueOutputBuffer方法查詢是否有解碼完成的可用數(shù)據(jù),其中mBufferInfo用于獲取數(shù)據(jù)幀信息,第二參數(shù)是等待時間,這里等待1000ms,填入-1是無限等待。

var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
復(fù)制代碼

第二、判斷index類型:

MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:輸出格式改變了

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:輸入緩沖改變了

MediaCodec.INFO_TRY_AGAIN_LATER:沒有可用數(shù)據(jù),等會再來

大于等于0:有可用數(shù)據(jù),index就是輸出緩沖索引

  • 【解碼步驟:4. 渲染】

這里調(diào)用了一個虛函數(shù)render,也就是將渲染交給子類

  • 【解碼步驟:5. 釋放輸出緩沖】

調(diào)用releaseOutputBuffer方法, 釋放輸出緩沖區(qū)。

注:第二個參數(shù),是個boolean,命名為render,這個參數(shù)在視頻解碼時,用于決定是否要將這一幀數(shù)據(jù)顯示出來。

mCodec!!.releaseOutputBuffer(index, true)
復(fù)制代碼
  • 【解碼步驟:6. 判斷解碼是否完成】

還記得我們在把數(shù)據(jù)壓入解碼器時,當(dāng)sampleSize < 0 時,壓入了一個結(jié)束標(biāo)記嗎?

當(dāng)接收到這個標(biāo)志后,解碼器就知道所有數(shù)據(jù)已經(jīng)接收完畢,在所有數(shù)據(jù)解碼完成以后,會在最后一幀數(shù)據(jù)加上結(jié)束標(biāo)記信息,即

if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
    mState = DecodeState.FINISH
    mStateListener?.decoderFinish(this)
}
復(fù)制代碼
  • 【解碼步驟: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) {
        }
    }
}
復(fù)制代碼

最后,解碼器定義的其他方法(如pause、goOn、stop等)不再細(xì)說,可查看工程源碼。

結(jié)尾

本來打算把音頻和視頻播放部分也放到本篇來講,最后發(fā)現(xiàn)篇幅太長,不利于閱讀,看了會累。所以把真正實(shí)現(xiàn)播放部分和下一篇【音視頻播放:音視頻同步】做一個整合,內(nèi)容和長度都會更合理。

so,下一篇見!

本文轉(zhuǎn)載自:https://juejin.cn/post/6844903952165634055

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容