zoukankan      html  css  js  c++  java
  • 游戏陪玩源码开发,音视频硬解码流程-封装基础解码框架

    既然在游戏陪玩源码开发时使用了实时音视频技术,那就涉及到了音视频传输流程,其中音视频数据的编解码是很关键的一个环节,所以接下来我们要了解的就是音视频硬解码的相关流程,封装基础解码框架。

    定义解码器

    因此,我们将整个解码流程抽象为一个解码基类: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个步骤,看起很复杂,实际很简单。

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

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

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

    初始化渲染器:视频不需要,音频为AudioTracker

    初始化解码器:初始化MediaCodec
    在initCodec()中,

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

    初始化MediaCodec的时候:

    首先,通过Extractor获取到游戏陪玩源码音视频数据的编码信息MediaFormat;
    然后,查询MediaFormat中的编码类型(如video/avc,即H264;audio/mp4a-latm,即AAC);
    最后,调用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
        }
    }

    调用了以下方法:

    查询是否有可用的输入缓冲,返回缓冲索引。其中参数2000为等待2000ms,如果填入-1则无限等待。

    var inputBufferIndex = mCodec!!.dequeueInputBuffer(2000)

    通过缓冲索引 inputBufferIndex 获取可用的缓冲区,并使用Extractor提取待解码数据,填充到缓冲区中。

    val inputBuffer = mInputBuffers!![inputBufferIndex]
    val sampleSize = mExtractor!!.readBuffer(inputBuffer)

    调用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等)不再细说,可查看工程源码。以上就是游戏陪玩源码开发,音视频硬解码流程-封装基础解码框架的全部内容了,希望对大家有帮助。

    本文转载自网络,转载仅为分享干货知识,如有侵权欢迎联系云豹科技进行删除处理
    链接:https://juejin.cn/post/6844903952165634055

  • 相关阅读:
    php分享三十:php版本选择
    php分享二十九:命名空间
    高性能mysql读书笔记(一):Schema与数据类型优化
    php分享二十八:mysql运行中的问题排查
    php分享二十七:批量插入mysql
    php分享二十六:读写日志
    Python | 一行命令生成动态二维码
    Python-获取法定节假日
    GoLang-字符串
    基础知识
  • 原文地址:https://www.cnblogs.com/yunbao/p/14955175.html
Copyright © 2011-2022 走看看