MediaCodec(1)-音视频硬解码流程:解码框架的基础

本篇文章的目的是为了跟随其他大佬的文章学习一遍,目的就是为了印象更加深刻

通过本文你可以学习到

以Android为基础,了解如何调用MediaCodec的API实现硬解码的过程,包括MediaCodec的输入输出缓冲,MediaCodec的解码流程,以及对该流程的代码编写。

一、MediaCodec介绍

MediaCodec是是 Android 平台提供的一个多媒体编解码器,从Android4.1(api 16)开始引入的,所以说,MediaCodec不支持跨平台,这也是跟ffmpeg最大的区别之一。MediaCodec封装的比较成熟,同时支持音视频的编码和解码,如果你的应用只发布在Android市场,其实MediaCodec够用了,而且也比较成熟。

下面关于数据流的两张图来自于MediaCodec官网,需要好好理解,后面的代码其实就是基于这两张图

数据流相关

官方文档首先介绍的就是数据流的内容,如下图所示

官网下面有对应的英文解释,大致翻译过来是这样:这个流程就是为了通过编码把输入数据转为输出数据。左边是输入数据input,右边是输出数据output。
input :首先使用一个空的缓冲区去input里面取数,数据填充满之后,进入codec环节,这个可以是解码的数据(解码时)或者需要编码的数据(编码时) output:解码好的的数据、或者编码好的数据通过缓冲区传递给请求者(或者订阅者),传递完成之后,请求者释放缓冲区给解码器,进行下一次的数据传递。

状态

再来看看编解码过程中的几个状态,来自于官网

该图主要表现为三个状态:Stopped、Exexuting、Released。更多的子环节,需要从左边图开始,跟随箭头,看向右边。

  • Stopped: 包含Error、Uninitialized、Configured三个小状态

首先在新建MediaCodec之后,会先进入Uninitialized状态;

随后,调用configure方法配置参数后,进入Configured状态

  • Exexuting: 包含Flushed、Running、End of Stream三个小状态

跟随上面,调用start方法后,MediaCodec进入Flushed状态;

接着,调用dequeueInputBuffer方法后,进入Running状态

最后,当编码/解码结束之后,进入End of Stream(EOF)状态

到这里,视频处理就算完成了

这里说明下Flushed状态

根据图中的箭头所示,在Running或者End of Stream状态时,都可以调用flush方法,重新进入Flushed状态。

在解码进入到End of Stream之后,解码器此时不再有任何输入,这时候需要调用flush方法,重新进入接收数据状态。

或者存在跳播的情况时,从一个视频到另一个视频,此时需要Seek到指定的时间点,这时候,需要调用flush方法,清除缓冲数据,否则解码时间戳会混乱。

  • Released: 为了避免资源一直占据内存等,在数据处理完成之后,需要调用release方法,释放所有资源

上面两张图不要走马观花的看,需要多看几遍,知道每个状态的流转,这样代码写起来更加方便

二、解码流程

MediaCodec的解码流程分为同步和异步两种模式,这里先把同步的模式学习下。根据上面官方数据流处理流程图和状态图,画出一个基础的解码流程图如下:

经过Codec的初始化和配置参数以后,进入循环的解码流程,不断的输入数据流,然后得到解码完成的数据,最后在终端渲染出来,直到所有的数据都解码完成(End of Stream),数据流缓冲区释放(release),算是真正的结束。

三、开始解码

音频和视频的解码其实都在上面的循环解码这个流程里面,不同的地方也就只有【配置】、【渲染】两个部分。
创建解码器

首先要去定义一个接口IDecoder,定义基础的解码方法,如暂停/继续/停止解码、获取视频的时长、获取视频的宽高、解码状态等,该接口继承Runnable。

js 复制代码
interface IDecoder: Runnable {

    /**
     * 暂停解码
     */
    fun pause()

    /**
     * 继续解码
     */
    fun goOn()

    /**
     * 跳转到指定位置
     * 并返回实际帧的时间
     *
     * @param pos: 毫秒
     * @return 实际时间戳,单位:毫秒
     */
    fun seekTo(pos: Long): Long

    /**
     * 跳转到指定位置,并播放
     * 并返回实际帧的时间
     *
     * @param pos: 毫秒
     * @return 实际时间戳,单位:毫秒
     */
    fun seekAndPlay(pos: Long): Long

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

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

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

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

    /**
     * 设置尺寸监听器
     */
    fun setSizeListener(l: IDecoderProgress)

    /**
     * 设置状态监听器
     */
    fun setStateListener(l: IDecoderStateListener?)

    /**
     * 获取视频宽
     */
    fun getWidth(): Int

    /**
     * 获取视频高
     */
    fun getHeight(): Int

    /**
     * 获取视频长度
     */
    fun getDuration(): Long

    /**
     * 当前帧时间,单位:ms
     */
    fun getCurTimeStamp(): Long

    /**
     * 获取视频旋转角度
     */
    fun getRotationAngle(): Int

    /**
     * 获取音视频对应的格式参数
     */
    fun getMediaFormat(): MediaFormat?

    /**
     * 获取音视频对应的媒体轨道
     */
    fun getTrack(): Int

    /**
     * 获取解码的文件路径
     */
    fun getFilePath(): String

    /**
     * 无需音视频同步
     */
    fun withoutSync(): IDecoder
}

为什么继承Runnable?

这主要是因为这里使用的是同步方式解码,需要不断的循环压入和拉取数据,是一个耗时操作,因此这里定义一个Runnable,放到线程池中,处理一直压入的数据

再来看看如何定义一个基础的解码器BaseDecoder,去实现各个方法。

js 复制代码
abstract class BaseDecoder(private val mFilePath: String): IDecoder {

    private val TAG = "BaseDecoder"

    //-------------线程相关------------------------
    /**
     * 解码器是否在运行
     */
    private var mIsRunning = true

    /**
     * 线程等待锁
     */
    private val mLock = Object()

    /**
     * 是否可以进入解码
     */
    private var mReadyForDecode = false

    //---------------状态相关-----------------------
    /**
     * 音视频解码器
     */
    private var mCodec: MediaCodec? = null

    /**
     * 音视频数据读取器
     */
    private var mExtractor: IExtractor? = null

    /**
     * 解码输入缓存区
     */
    private var mInputBuffers: Array<ByteBuffer>? = null

    /**
     * 解码输出缓存区
     */
    private var mOutputBuffers: Array<ByteBuffer>? = null

    /**
     * 解码数据信息
     */
    private var mBufferInfo = MediaCodec.BufferInfo()

    private var mState = DecodeState.STOP

    protected var mStateListener: IDecoderStateListener? = null

    /**
     * 流数据是否结束
     */
    private var mIsEOS = false

    protected var mVideoWidth = 0

    protected var mVideoHeight = 0

    private var mDuration: Long = 0

    private var mStartPos: Long = 0

    private var mEndPos: Long = 0

    /**
     * 开始解码时间,用于音视频同步
     */
    private var mStartTimeForSync = -1L

    // 是否需要音视频渲染同步
    private var mSyncRender = true

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

        //【解码步骤:1. 初始化,并启动解码器】
        if (!init()) return

        Log.i(TAG, "开始解码")
        try {
            while (mIsRunning) {
                if (mState != DecodeState.START &&
                    mState != DecodeState.DECODING &&
                    mState != DecodeState.SEEKING) {
                    Log.i(TAG, "进入等待:$mState")

                    waitDecode()

                    // ---------【同步时间矫正】-------------
                    //恢复同步的起始时间,即去除等待流失的时间
                    mStartTimeForSync = System.currentTimeMillis() - getCurTimeStamp()
                }

                if (!mIsRunning ||
                    mState == DecodeState.STOP) {
                    mIsRunning = false
                    break
                }

                if (mStartTimeForSync == -1L) {
                    mStartTimeForSync = System.currentTimeMillis()
                }

                //如果数据没有解码完毕,将数据推入解码器解码
                if (!mIsEOS) {
                    //【解码步骤:2. 见数据压入解码器输入缓冲】
                    mIsEOS = pushBufferToDecoder()
                }

                //【解码步骤:3. 将解码好的数据从缓冲区拉取出来】
                val index = pullBufferFromDecoder()
                Log.i(TAG, "pullBufferFromDecoder index = $index")
                if (index >= 0) {
                    // ---------【音视频同步】-------------
                    if (mSyncRender && mState == DecodeState.DECODING) {
                        sleepRender()
                    }
                    //【解码步骤:4. 渲染】
                    if (mSyncRender) {// 如果只是用于编码合成新视频,无需渲染
                        render(mOutputBuffers!![index], mBufferInfo)
                    }

                    //将解码数据传递出去
                    val frame = Frame()
                    frame.buffer = mOutputBuffers!![index]
                    frame.setBufferInfo(mBufferInfo)
                    mStateListener?.decodeOneFrame(this, frame)

                    //【解码步骤:5. 释放输出缓冲】
                    mCodec!!.releaseOutputBuffer(index, true)

                    if (mState == DecodeState.START) {
                        mState = DecodeState.PAUSE
                    }
                }
                //【解码步骤:6. 判断解码是否完成】
                if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                    Log.i(TAG, "解码结束")
                    mState = DecodeState.FINISH
                    mStateListener?.decoderFinish(this)
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            doneDecode()
            release()
        }
    }

    private fun init(): Boolean {
        if (mFilePath.isEmpty() || !File(mFilePath).exists()) {
            Log.w(TAG, "文件路径为空")
            mStateListener?.decoderError(this, "文件路径为空")
            return false
        }

        if (!check()) return false

        //初始化数据提取器
        mExtractor = initExtractor(mFilePath)
        if (mExtractor == null ||
            mExtractor!!.getFormat() == null) {
            Log.w(TAG, "无法解析文件")
            return false
        }

        //初始化参数
        if (!initParams()) return false

        //初始化渲染器
        if (!initRender()) return false

        //初始化解码器
        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 {
            val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
            mCodec = type?.let { MediaCodec.createDecoderByType(it) }
            if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
                waitDecode()
            }
            mCodec!!.start()

            mInputBuffers = mCodec?.inputBuffers
            mOutputBuffers = mCodec?.outputBuffers
        } catch (e: Exception) {
            return false
        }
        return true
    }

    private fun pushBufferToDecoder(): Boolean {
        var inputBufferIndex = mCodec!!.dequeueInputBuffer(1000)
        var isEndOfStream = false

        if (inputBufferIndex >= 0) {
            val inputBuffer = mInputBuffers!![inputBufferIndex]
            val sampleSize = mExtractor!!.readBuffer(inputBuffer)

            if (sampleSize < 0) {
                //如果数据已经取完,压入数据结束标志:MediaCodec.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
    }

    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
    }

    private fun sleepRender() {
        val passTime = System.currentTimeMillis() - mStartTimeForSync
        val curTime = getCurTimeStamp()
        if (curTime > passTime) {
            Thread.sleep(curTime - passTime)
        }
    }

    private fun release() {
        try {
            Log.i(TAG, "解码停止,释放解码器")
            mState = DecodeState.STOP
            mIsEOS = false
            mExtractor?.stop()
            mCodec?.stop()
            mCodec?.release()
            mStateListener?.decoderDestroy(this)
        } catch (e: Exception) {
        }
    }

    /**
     * 解码线程进入等待
     */
    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)
        }
    }

    override fun pause() {
        mState = DecodeState.DECODING
    }

    override fun goOn() {
        mState = DecodeState.DECODING
        notifyDecode()
    }

    override fun seekTo(pos: Long): Long {
        return 0
    }

    override fun seekAndPlay(pos: Long): Long {
        return 0
    }

    override fun stop() {
        mState = DecodeState.STOP
        mIsRunning = false
        notifyDecode()
    }

    override fun isDecoding(): Boolean {
        return mState == DecodeState.DECODING
    }

    override fun isSeeking(): Boolean {
        return mState == DecodeState.SEEKING
    }

    override fun isStop(): Boolean {
        return mState == DecodeState.STOP
    }

    override fun setSizeListener(l: IDecoderProgress) {
    }

    override fun setStateListener(l: IDecoderStateListener?) {
        mStateListener = l
    }

    override fun getWidth(): Int {
        return mVideoWidth
    }

    override fun getHeight(): Int {
        return mVideoHeight
    }

    override fun getDuration(): Long {
        return mDuration
    }

    override fun getCurTimeStamp(): Long {
        return mBufferInfo.presentationTimeUs / 1000
    }

    override fun getRotationAngle(): Int {
        return 0
    }

    override fun getMediaFormat(): MediaFormat? {
        return mExtractor?.getFormat()
    }

    override fun getTrack(): Int {
        return 0
    }

    override fun getFilePath(): String {
        return mFilePath
    }

    override fun withoutSync(): IDecoder {
        mSyncRender = false
        return this
    }

    /**
     * 检查子类参数
     */
    abstract fun check(): Boolean

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

    /**
     * 初始化子类自己特有的参数
     */
    abstract fun initSpecParams(format: MediaFormat)

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

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

    /**
     * 渲染
     */
    abstract fun render(outputBuffer: ByteBuffer,
                        bufferInfo: MediaCodec.BufferInfo)

    /**
     * 结束解码
     */
    abstract fun doneDecode()
}
  • 首先定义线程相关的资源,定义变量mIsRunning来判断当前是否一直处于解码状态,还有挂起线程的mLock等
  • 然后,就是定义解码相关的资源,比如创建MediaCodec,配置相关参数,还有定义输入输出缓冲,解码状态等;其中解码状态为DecodeState,音视频数据读取器定义为IExtractor。

定义解码状态

为了方便查看解码状态,使用一个枚举进行定义

js 复制代码
enum class DecodeState {
    /**开始状态*/
    START,
    /**解码中*/
    DECODING,
    /**解码暂停*/
    PAUSE,
    /**正在快进*/
    SEEKING,
    /**解码完成*/
    FINISH,
    /**解码器释放*/
    STOP
}

定义音视频数据分离器 音视频文件可能来自于本地,也可能来自于服务器,为了方便验证,我们这里先拿本地的mp4文件作为原始数据,不断的喂数据给输入缓冲区,Android自带有一个音视频数据读取器MediaExtractor,需要实现的方法使用接口进行定义

js 复制代码
interface IExtractor {

    fun getFormat(): MediaFormat?

    /**
     * 读取音视频数据
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int

    /**
     * 获取当前帧时间
     */
    fun getCurrentTimestamp(): Long

    fun getSampleFlag(): Int

    /**
     * Seek到指定位置,并返回实际帧的时间戳
     */
    fun seek(pos: Long): Long

    fun setStartPos(pos: Long)

    /**
     * 停止读取数据
     */
    fun stop()
}

这里面最重要的就是方法readBuffer,用于读取音视频数据流

定义解码流程

前面BaseDecoder这个解码器中含有解码流程的部分,这部分是最重要的,主要体现在Runnable的run方法中

  • 【解码步骤:1、初始化、并启动解码器】
js 复制代码
private fun init(): Boolean {
    if (mFilePath.isEmpty() || !File(mFilePath).exists()) {
        Log.w(TAG, "文件路径为空")
        mStateListener?.decoderError(this, "文件路径为空")
        return false
    }

    if (!check()) return false

    //初始化数据提取器
    mExtractor = initExtractor(mFilePath)
    if (mExtractor == null ||
        mExtractor!!.getFormat() == null) {
        Log.w(TAG, "无法解析文件")
        return false
    }

    //初始化参数
    if (!initParams()) return false

    //初始化渲染器
    if (!initRender()) return false

    //初始化解码器
    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 {
        val type = mExtractor!!.getFormat()!!.getString(MediaFormat.KEY_MIME)
        mCodec = type?.let { MediaCodec.createDecoderByType(it) }
        if (!configCodec(mCodec!!, mExtractor!!.getFormat()!!)) {
            waitDecode()
        }
        mCodec!!.start()

        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 configCodec(codec: MediaCodec, format: MediaFormat): Boolean

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

/**
 * 渲染
 */
abstract fun render(outputBuffer: ByteBuffer,
                    bufferInfo: MediaCodec.BufferInfo)

/**
 * 结束解码
 */
abstract fun doneDecode()

初始化MediaCodec的过程中,分为5个步骤

1、检查参数是否完整:路径是否有效等

2、初始化数据提取器:初始化Extractor

3、初始化参数:提取一些必须的参数,如duration、width、height等

4、初始化渲染器:视频不需要渲染器,音频需要AudioTracker 5、初始化解码器:初始化MediaCodec

在initCodec()方法中,

js 复制代码
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方法,代码如下:
js 复制代码
private fun pushBufferToDecoder(): Boolean {
    var inputBufferIndex = mCodec!!.dequeueInputBuffer(1000)
    var isEndOfStream = false

    if (inputBufferIndex >= 0) {
        val inputBuffer = mInputBuffers!![inputBufferIndex]
        val sampleSize = mExtractor!!.readBuffer(inputBuffer)

        if (sampleSize < 0) {
            //如果数据已经取完,压入数据结束标志:MediaCodec.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则进行无限等待。

js 复制代码
var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)

2、通过缓冲索引inputBufferIndex获取可用的缓冲区,从Extractor媒体文件的数据存储到缓冲区中,并返回大小

js 复制代码
val inputBuffer = mInputBuffers!![inputBufferIndex] 
val sampleSize = mExtractor!!.readBuffer(inputBuffer)

3、调用queueInputBuffer将数据压入解码器

js 复制代码
mCodec!!.queueInputBuffer(inputBufferIndex, 0, sampleSize, mExtractor!!.getCurrentTimestamp(), 0)

注意:如果步骤2中的sampleSize返回-1,说明没有更多的数据了。这时候,queueInputBuffer的最后一个参数传入结束标记MediaCodec.BUFFER_FLAG_END_OF_STREAM。

  • 【解码步骤:3、将解码好的数据从缓冲区拉取出来】
    直接进入pullBufferFromDecoder()环节
js 复制代码
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表示无限等待

js 复制代码
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、渲染】

    使用抽象方法进行定义,主要给音频渲染使用的

  • 【解码步骤:5、释放输出缓冲】

    调用releaseOutputBuffer方法,释放输出缓冲区

注意,这个方法里面的第二个参数,是boolean类型的,这个参数在视频解码时,决定是否要将这一帧数据展示出来,也就是大家经常使用到的封面图

js 复制代码
mCodec!!.releaseOutputBuffer(index, true)
  • 【解码步骤:6、判断解码是否完成】
    在前面的流程中,数据压入解码器之后,当没有数据的时候,sampleSize<0,此时压入一个结束标记。解码器就知道所有的数据已经接收完毕,并且在最后一帧数据加上结束标记信息,即
js 复制代码
if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) { 
mState = DecodeState.FINISH mStateListener?.decoderFinish(this) 
}
  • 【解码步骤:7、释放解码器】
    在解码循环整个流程结束后,也就是代码里面的while循环结束后,释放掉所有的资源,至此,一次解码结束
js 复制代码
private fun release() {
    try {
        Log.i(TAG, "解码停止,释放解码器")
        mState = DecodeState.STOP
        mIsEOS = false
        mExtractor?.stop()
        mCodec?.stop()
        mCodec?.release()
        mStateListener?.decoderDestroy(this)
    } catch (e: Exception) {
    }
}

解码器定义的其他方法,比如pause、goOn、stop等,不再细说,等到最后一篇文章里面把所有的源码开放

相关推荐
EasyCVR15 小时前
EHOME视频平台EasyCVR视频融合平台使用OBS进行RTMP推流,WebRTC播放出现抖动、卡顿如何解决?
人工智能·算法·ffmpeg·音视频·webrtc·监控视频接入
简鹿办公16 小时前
使用 FFmpeg 进行音视频转换的相关命令行参数解释
ffmpeg·简鹿视频格式转换器·ffmpeg视频转换
EasyCVR20 小时前
萤石设备视频接入平台EasyCVR多品牌摄像机视频平台海康ehome平台(ISUP)接入EasyCVR不在线如何排查?
运维·服务器·网络·人工智能·ffmpeg·音视频
runing_an_min20 小时前
ffmpeg 视频滤镜:屏蔽边框杂色- fillborders
ffmpeg·音视频·fillborders
岁月小龙1 天前
如何让ffmpeg运行时从当前目录加载库,而不是从/lib64
ffmpeg·origin·ffprobe·rpath
行者记3 天前
ffmpeg命令——从wireshark包中的rtp包中分离h264
测试工具·ffmpeg·wireshark
EasyCVR3 天前
国标GB28181视频平台EasyCVR私有化视频平台工地防盗视频监控系统方案
运维·科技·ffmpeg·音视频·1024程序员节·监控视频接入
hypoqqq3 天前
使用ffmpeg播放rtsp视频流
ffmpeg
cuijiecheng20183 天前
音视频入门基础:FLV专题(24)——FFmpeg源码中,获取FLV文件视频信息的实现
ffmpeg·音视频
QMCY_jason3 天前
黑豹X2 armbian 编译rkmpp ffmpeg 实现CPU视频转码
ffmpeg