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

【聲 明】

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

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

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二塞祈、使用OpenGL渲染視頻畫面篇
三醇锚、Android FFmpeg音視頻解碼篇
  • 1哼御,F(xiàn)Fmpeg 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實(shí)現(xiàn)硬解碼的流程挟炬,包含MediaCodec輸入輸出緩沖、MediaCodec解碼流程矩屁、解碼代碼封裝和講解辟宗。

一爵赵、簡介

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

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

數(shù)據(jù)流

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

MediaCodec數(shù)據(jù)流

仔細(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)部使用異步的方式對input和output數(shù)據(jù)進(jìn)行處理紧阔。MediaCodec將處理好input的數(shù)據(jù)坊罢,填充到output緩沖區(qū),交給客戶端渲染或處理

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

狀態(tài)

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

MediaCodec狀態(tài)圖

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

  • Stoped:包含了3個(gè)小狀態(tài):Error、Uninitialized崔涂、Configured阳掐。

首先,新建MediaCodec后,會進(jìn)入U(xiǎn)ninitialized狀態(tài)缭保;
其次汛闸,調(diào)用configure方法配置參數(shù)后,會進(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)我們在解碼過程中浙垫,進(jìn)入了End of Stream后,解碼器就不再接收輸入了郑诺,這時(shí)候夹姥,需要調(diào)用flush方法,重新進(jìn)入接收數(shù)據(jù)狀態(tài)辙诞。

或者辙售,我們在播放視頻過程中旦部,想進(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),無論音頻還是視頻锐朴,解碼流程基本是一致的兴喂,不同的地方只在于【配置】蔼囊、【渲染】兩個(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

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

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

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

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

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

定義了解碼器的一些基礎(chǔ)操作膳沽,如暫停/繼續(xù)/停止解碼,獲取視頻的時(shí)長让禀,視頻的寬高挑社,解碼狀態(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í)際很簡單梯浪。

  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)
    

初始化MediaCodec的時(shí)候:

  1. 首先虏劲,通過Extractor獲取到音視頻數(shù)據(jù)的編碼信息MediaFormat托酸;
  2. 然后,查詢MediaFormat中的編碼類型(如video/avc柒巫,即H264励堡;audio/mp4a-latm,即AAC)堡掏;
  3. 最后应结,調(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)用了以下方法:

  1. 查詢是否有可用的輸入緩沖,返回緩沖索引拴鸵。其中參數(shù)2000為等待2000ms玷坠,如果填入-1則無限等待。
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)
  1. 通過緩沖索引 inputBufferIndex 獲取可用的緩沖區(qū)劲藐,并使用Extractor提取待解碼數(shù)據(jù)八堡,填充到緩沖區(qū)中。
val inputBuffer = mInputBuffers!![inputBufferIndex]
val sampleSize = mExtractor!!.readBuffer(inputBuffer)
  1. 調(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ù)厌漂,等會再來

大于等于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. 判斷解碼是否完成】

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

當(dāng)接收到這個(gè)標(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)
}
  • 【解碼步驟: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)篇幅太長弧圆,不利于閱讀赋兵,看了會累。所以把真正實(shí)現(xiàn)播放部分和下一篇【音視頻播放:音視頻同步】做一個(gè)整合搔预,內(nèi)容和長度都會更合理霹期。

so,下一篇見拯田!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載历造,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吭产,隨后出現(xiàn)的幾起案子侣监,更是在濱河造成了極大的恐慌,老刑警劉巖臣淤,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橄霉,死亡現(xiàn)場離奇詭異,居然都是意外死亡荒典,警方通過查閱死者的電腦和手機(jī)酪劫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寺董,“玉大人覆糟,你說我怎么就攤上這事≌诳В” “怎么了滩字?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長御吞。 經(jīng)常有香客問我麦箍,道長,這世上最難降的妖魔是什么陶珠? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任挟裂,我火速辦了婚禮,結(jié)果婚禮上揍诽,老公的妹妹穿的比我還像新娘诀蓉。我一直安慰自己,他們只是感情好暑脆,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布渠啤。 她就那樣靜靜地躺著,像睡著了一般添吗。 火紅的嫁衣襯著肌膚如雪沥曹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天碟联,我揣著相機(jī)與錄音妓美,去河邊找鬼。 笑死玄帕,一個(gè)胖子當(dāng)著我的面吹牛部脚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播裤纹,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鹰椒?” 一聲冷哼從身側(cè)響起锡移,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎漆际,沒想到半個(gè)月后淆珊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡奸汇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年施符,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片擂找。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡戳吝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贯涎,到底是詐尸還是另有隱情听哭,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布塘雳,位于F島的核電站陆盘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏败明。R本人自食惡果不足惜隘马,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妻顶。 院中可真熱鬧酸员,春花似錦、人聲如沸盈包。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呢燥。三九已至崭添,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叛氨,已是汗流浹背呼渣。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寞埠,地道東北人屁置。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像仁连,于是被迫代替她去往敵國和親蓝角。 傳聞我的和親對象是個(gè)殘疾皇子阱穗,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容