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

【聲 明】

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

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

教程代碼:【Github傳送門

目錄

1、Android音視頻硬解碼篇:

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

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

3、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解碼流程、解碼代碼封裝和講解。github

1、簡介

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

必定要好好理解接下來這兩幅圖,由於後續的代碼就是基於這兩幅圖來編寫的。緩存

數據流

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

MediaCodec數據流

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

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

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

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

注:客戶端處理完數據後,必須手動釋放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方法,清除緩衝,不然解碼時間戳會混亂。

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

2、解碼流程

MediaCodec有兩種工做模式,分別爲異步模式和同步模式,這裏咱們使用同步模式,異步模式能夠參考官網例子

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

解碼流程圖

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

3、開始解碼

根據上面的流程圖,能夠發現,不管音頻仍是視頻,解碼流程基本是一致的,不一樣的地方只在於【配置】、【渲染】兩個部分。

定義解碼器

所以,咱們將整個解碼流程抽象爲一個解碼基類: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. 經過緩衝索引 inputBufferIndex 獲取可用的緩衝區,並使用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
    }
}
複製代碼

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

var index = mCodec!!.dequeueOutputBuffer(mBufferInfo, 1000)
複製代碼

第2、判斷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,下一篇見!

相關文章
相關標籤/搜索