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

【聲 明】

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

碼字不易,轉載請註明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:

二、使用OpenGL渲染視頻畫面篇

  • 1,初步瞭解OpenGL ES
  • 2,使用OpenGL渲染視頻畫面
  • 3,OpenGL渲染多視頻,實現畫中畫
  • 4,深入瞭解OpenGL之EGL
  • 5,OpenGL FBO數據緩衝區
  • 6,Android音視頻硬編碼:生成一個MP4

三、Android FFmpeg音視頻解碼篇

  • 1,FFmpeg so庫編譯
  • 2,Android 引入FFmpeg
  • 3,Android FFmpeg視頻解碼播放
  • 4,Android FFmpeg+OpenSL ES音頻解碼播放
  • 5,Android FFmpeg+OpenGL ES播放視頻
  • 6,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
  • 7,Android FFmpeg視頻編碼

本文你可以瞭解到

本文主要簡介Android使用硬解碼API實現硬解碼的流程,包含MediaCodec輸入輸出緩衝、MediaCodec解碼流程、解碼代碼封裝和講解。

一、簡介

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

一定要好好理解接下來這兩幅圖,因爲後續的代碼就是基於這兩幅圖來編寫的。

數據流

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

MediaCodec數據流

仔細看一下,MediaCodec將數據分爲兩部分,分別爲input(左邊)和output(右邊),即輸入和輸出兩個數據緩衝區。

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

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

MediaCodec內部使用異步的方式對input和output數據進行處理。MediaCodec將處理好input的數據,填充到output緩衝區,交給客戶端渲染或處理

注:客戶端處理完數據後,必須手動釋放output緩衝區,否則將會導致MediaCodec輸出緩衝被佔用,無法繼續解碼。

狀態

依然是一副來自官方的狀態圖

MediaCodec狀態

再仔細看看這幅圖,整體上分爲三個大的狀態:Sotpped、Executing、Released。

  • Stoped:包含了3個小狀態:Error、Uninitialized、Configured。

首先,新建MediaCodec後,會進入Uninitialized狀態;
其次,調用configure方法配置參數後,會進入Configured;

  • Executing:同樣包含3個小狀態:Flushed、Running、End of Stream。

再次,調用start方法後,MediaCodec進入Flushed狀態;
接着,調用dequeueInputBuffer方法後,進入Running狀態;
最後,當解碼/編碼結束時,進入End of Stream(EOF)狀態。
這時,一個視頻就處理完成了。

  • Released:最後,如果想結束整個數據處理過程,可以調用release方法,釋放所有的資源。

那麼,Flushed是什麼狀態呢?

從圖中我們可以看到,在Running或者End of Stream狀態時,都可以調用flush方法,重新進入Flushed狀態。

當我們在解碼過程中,進入了End of Stream後,解碼器就不再接收輸入了,這時候,需要調用flush方法,重新進入接收數據狀態。

或者,我們在播放視頻過程中,想進行跳播,這時候,我們需要Seek到指定的時間點,這時候,也需要調用flush方法,清除緩衝,否則解碼時間戳會混亂。

再次強調一下,一定要好好理解這兩幅圖,因爲後續的代碼就是基於這兩幅圖來編寫的。

二、解碼流程

MediaCodec有兩種工作模式,分別爲異步模式和同步模式,這裏我們使用同步模式,異步模式可以參考官網例子

根據官方的數據流圖和狀態圖,畫出一個最基礎的解碼流程如下:

解碼流程圖

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

三、開始解碼

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

定義解碼器

因此,我們將整個解碼流程抽象爲一個解碼基類:BaseDecoder,爲了規範代碼和更好的拓展性,我們先定義一個解碼器:IDecoder,繼承Runnable。

interface IDecoder: Runnable {

    /**
     * 暫停解碼
     */
    fun pause()

    /**
     * 繼續解碼
     */
    fun goOn()

    /**
     * 停止解碼
     */
    fun stop()

    /**
     * 是否正在解碼
     */
    fun isDecoding(): Boolean

    /**
     * 是否正在快進
     */
    fun isSeeking(): Boolean

    /**
     * 是否停止解碼
     */
    fun isStop(): Boolean

    /**
     * 設置狀態監聽器
     */
    fun setStateListener(l: IDecoderStateListener?)

    /**
     * 獲取視頻寬
     */
    fun getWidth(): Int

    /**
     * 獲取視頻高
     */
    fun getHeight(): Int

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

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

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

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

    /**
     * 獲取解碼的文件路徑
     */
    fun getFilePath(): String
}

定義瞭解碼器的一些基礎操作,如暫停/繼續/停止解碼,獲取視頻的時長,視頻的寬高,解碼狀態等等

爲什麼繼承Runnable?

這裏使用的是同步模式解碼,需要不斷循環壓入和拉取數據,是一個耗時操作,因此,我們將解碼器定義爲一個Runnable,最後放到線程池中執行。

接着,繼承IDecoder,定義基礎解碼器BaseDecoder。

首先來看下基礎參數:

abstract class BaseDecoder: IDecoder {
    //-------------線程相關------------------------
    /**
     * 解碼器是否在運行
     */
    private var mIsRunning = true

    /**
     * 線程等待鎖
     */
    private val mLock = Object()

    /**
     * 是否可以進入解碼
     */
    private var mReadyForDecode = false

    //---------------解碼相關-----------------------
    /**
     * 音視頻解碼器
     */
    protected var mCodec: MediaCodec? = null
    
    /**
     * 音視頻數據讀取器
     */
    protected var mExtractor: IExtractor? = null

    /**
     * 解碼輸入緩存區
     */
    protected var mInputBuffers: Array<ByteBuffer>? = null

    /**
     * 解碼輸出緩存區
     */
    protected var mOutputBuffers: Array<ByteBuffer>? = null

    /**
     * 解碼數據信息
     */
    private var mBufferInfo = MediaCodec.BufferInfo()
    
    private var mState = DecodeState.STOP

    private var mStateListener: IDecoderStateListener? = null

    /**
     * 流數據是否結束
     */
    private var mIsEOS = false

    protected var mVideoWidth = 0

    protected var mVideoHeight = 0
    
    //省略後面的方法
    ....
}
  • 首先,我們定義了線程相關的資源,用於判斷是否持續解碼的mIsRunning,掛起線程的mLock等。

  • 然後,就是解碼相關的資源了,比如MdeiaCodec本身,輸入輸出緩衝,解碼狀態等等。

  • 其中,有一個解碼狀態DecodeState和音視頻數據讀取器IExtractor。

定義解碼狀態

爲了方便記錄解碼狀態,這裏使用一個枚舉類表示

enum class DecodeState {
    /**開始狀態*/
    START,
    /**解碼中*/
    DECODING,
    /**解碼暫停*/
    PAUSE,
    /**正在快進*/
    SEEKING,
    /**解碼完成*/
    FINISH,
    /**解碼器釋放*/
    STOP
}

定義音視頻數據分離器

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

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

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

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

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

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

    fun setStartPos(pos: Long)

    /**
     * 停止讀取數據
     */
    fun stop()
}

最重要的一個方法就是readBuffer,用於讀取音視頻數據流

定義解碼流程

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

abstract class BaseDecoder: IDecoder {
    //省略參數定義部分,見上
    .......
    
    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
            }

            //如果數據沒有解碼完畢,將數據推入解碼器解碼
            if (!mIsEOS) {
                //【解碼步驟:2. 將數據壓入解碼器輸入緩衝】
                mIsEOS = pushBufferToDecoder()
            }

            //【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】
            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()
    }


    /**
     * 解碼線程進入等待
     */
    private fun waitDecode() {
        try {
            if (mState == DecodeState.PAUSE) {
                mStateListener?.decoderPause(this)
            }
            synchronized(mLock) {
                mLock.wait()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
    
    /**
     * 通知解碼線程繼續運行
     */
    protected fun notifyDecode() {
        synchronized(mLock) {
            mLock.notifyAll()
        }
        if (mState == DecodeState.DECODING) {
            mStateListener?.decoderRunning(this)
        }
    }
    
    /**
     * 渲染
     */
    abstract fun render(outputBuffers: ByteBuffer,
                        bufferInfo: MediaCodec.BufferInfo)

    /**
     * 結束解碼
     */
    abstract fun doneDecode()
}

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

  • 【解碼步驟:1. 初始化,並啓動解碼器】
abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun init(): Boolean {
        //1.檢查參數是否完整
        if (mFilePath.isEmpty() || File(mFilePath).exists()) {
            Log.w(TAG, "文件路徑爲空")
            mStateListener?.decoderError(this, "文件路徑爲空")
            return false
        }
        //調用虛函數,檢查子類參數是否完整
        if (!check()) return false

        //2.初始化數據提取器
        mExtractor = initExtractor(mFilePath)
        if (mExtractor == null ||
            mExtractor!!.getFormat() == null) return false

        //3.初始化參數
        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.根據音視頻編碼格式初始化解碼器
            val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
            mCodec = MediaCodec.createDecoderByType(type)
            //2.配置解碼器
            if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
                waitDecode()
            }
            //3.啓動解碼器
            mCodec!!.start()
            
            //4.獲取解碼器緩衝區
            mInputBuffers = mCodec?.inputBuffers
            mOutputBuffers = mCodec?.outputBuffers
        } catch (e: Exception) {
            return false
        }
        return true
    }
    
    /**
     * 檢查子類參數
     */
    abstract fun check(): Boolean

    /**
     * 初始化數據提取器
     */
    abstract fun initExtractor(path: String): IExtractor

    /**
     * 初始化子類自己特有的參數
     */
    abstract fun initSpecParams(format: MediaFormat)

    /**
     * 初始化渲染器
     */
    abstract fun initRender(): Boolean

    /**
     * 配置解碼器
     */
    abstract fun configCodec(codec: MediaCodec, format: MediaFormat): Boolean
}

初始化方法中,分爲5個步驟,看起很複雜,實際很簡單。

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

  2. 初始化數據提取器:初始化Extractor

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

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

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

    在initCodec()中,

    val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
    mCodec = MediaCodec.createDecoderByType(type)
    

初始化MediaCodec的時候:

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

需要說明的是:由於音頻和視頻的初始化稍有不同,所以定義了幾個虛函數,將不同的東西交給子類去實現。具體將在下一篇文章[音視頻播放:音視頻同步]說明。

  • 【解碼步驟:2. 將數據壓入解碼器輸入緩衝】

直接進入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) {
                //如果數據已經取完,壓入數據結束標誌: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
    }
}

調用了以下方法:

  1. 查詢是否有可用的輸入緩衝,返回緩衝索引。其中參數2000爲等待2000ms,如果填入-1則無限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
  1. 通過Extractor獲取緩衝區,並往緩衝區填充數據
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
  1. 調用queueInputBuffer將數據壓入解碼器
mCodec!!.queueInputBuffer(inputBufferIndex, 0,
    sampleSize, mExtractor!!.getCurrentTimestamp(), 0)

注意:如果SampleSize返回-1,說明沒有更多的數據了。
這個時候,queueInputBuffer的最後一個參數要傳入結束標記MediaCodec.BUFFER_FLAG_END_OF_STREAM。

  • 【解碼步驟:3. 將解碼好的數據從緩衝區拉取出來】

直接進入pullBufferFromDecoder()

abstract class BaseDecoder: IDecoder {
    //省略上面已有代碼
    ......
    
    private fun pullBufferFromDecoder(): Int {
        // 查詢是否有解碼完成的數據,index >=0 時,表示數據有效,並且index爲緩衝區索引
        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
    }
}

第一、調用dequeueOutputBuffer方法查詢是否有解碼完成的可用數據,其中mBufferInfo用於獲取數據幀信息,第二參數是等待時間,這裏等待1000ms,填入-1是無限等待。

var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)

第二、判斷index類型:

MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:輸出格式改變了

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:輸入緩衝改變了

MediaCodec.INFO_TRY_AGAIN_LATER:沒有可用數據,等會再來

大於等於0:有可用數據,index就是輸出緩衝索引

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

這裏調用了一個虛函數render,也就是將渲染交給子類

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

調用releaseOutputBuffer方法, 釋放輸出緩衝區。

注:第二個參數,是個boolean,命名爲render,這個參數在視頻解碼時,用於決定是否要將這一幀數據顯示出來。

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

還記得我們在把數據壓入解碼器時,當sampleSize < 0 時,壓入了一個結束標記嗎?

當接收到這個標誌後,解碼器就知道所有數據已經接收完畢,在所有數據解碼完成以後,會在最後一幀數據加上結束標記信息,即

if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
    mState = DecodeState.FINISH
    mStateListener?.decoderFinish(this)
}
  • 【解碼步驟:7. 釋放解碼器】

在while循環結束後,釋放掉所有的資源。至此,一次解碼結束。

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等)不再細說,可查看工程源碼。

結尾

本來打算把音頻和視頻播放部分也放到本篇來講,最後發現篇幅太長,不利於閱讀,看了會累。所以把真正實現播放部分和下一篇【音視頻播放:音視頻同步】做一個整合,內容和長度都會更合理。

so,下一篇見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章