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

  • 相关阅读:
    DNNClassifier 深度神经网络 分类器
    浏览器对MP4视频 帧宽度 高度的兼容性
    UnicodeEncodeError:'latin-1' codec can't encode character
    文件夹下 文件计数
    the largest value you actually can transmit between the client and server is determined by the amount of available memory and the size of the communications buffers.
    the “identity” of an object
    广告特征 用户特征
    如果一个维度全覆盖,则有效维度应该对该维度全覆盖
    a high-level neural networks AP
    使用 LDA 挖掘的用户喜好主题
  • 原文地址:https://www.cnblogs.com/yunbao/p/14955175.html
Copyright © 2011-2022 走看看