请选择 进入手机版 | 继续访问电脑版
 找回密码

上海老站长门户

点击联系客服
客服QQ: 客服微信:
查看: 21|回复: 4

游戏伴随播放源开发、音频视频硬解码过程-软件包基本解码框架

[复制链接]

1

主题

1

帖子

-7

积分

限制会员

积分
-7
发表于 2021-6-30 16:56:31 | 显示全部楼层 |阅读模式
游戏在伴随源代码开发的时候使用了实时音频视频技术,因此音频视频数据编解码器是重要的一部分,涉及音频视频传输过程。因此,接下来需要知道的是与音频视频硬解码相关的过程,并封装基本解码框架。

定义解码器

因此,将整个解码过程抽象为基本解码类。BaseDecoder,为了标准化游戏附带源代码和提高可扩展性,首先定义IDecoder,然后继承Runnable。

Interfaceidecoder  : runnable  {

/* *

*暂停解码

*/

Funpause()

/* *

*继续解码

*/

FungoOn()

/* *

*停止解码

*/

Funstop()

/* *

*正在解码

*/

Funisdecoding  () :布尔型

/* *

*正在快进吗

*/

Funisseeking  () :布尔型

/* *

*是否停止解码

*/

funi  stop():布尔型

/* *

*设置状态侦听器

*/

FunsetStateL
istener(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[B]? = null
    /**
     * 解码输出缓存区
     */
    protected var mOutputBuffers: Array[B]? = 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
调用了以下方法:
查询是否有可用的输入缓冲,返回缓冲索引。其中参数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
当接收到这个标志后,游戏陪玩源码中的解码器就知道所有数据已经接收完毕,在所有数据解码完成以后,会在最后一帧数据加上结束标记信息,即
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等)不再细说,可查看工程源码。以上就是游戏陪玩源码开发,音视频硬解码流程-封装基础解码框架的全部内容了,希望对大家有帮助。
本文转载自网络,转载仅为分享干货知识,如有侵权欢迎联系云豹科技进行删除处理
链接:
回复

使用道具 举报

1

主题

659

帖子

172

积分

注册会员

Rank: 2

积分
172
发表于 2021-6-30 17:03:00 | 显示全部楼层
前排支持下
回复

使用道具 举报

1

主题

624

帖子

132

积分

注册会员

Rank: 2

积分
132
发表于 2021-6-30 17:43:28 | 显示全部楼层
学习了,不错,讲的太有道理了
回复

使用道具 举报

0

主题

617

帖子

166

积分

注册会员

Rank: 2

积分
166
发表于 2021-6-30 18:15:33 | 显示全部楼层
看帖回帖是美德!
回复

使用道具 举报

0

主题

621

帖子

98

积分

注册会员

Rank: 2

积分
98
发表于 2021-6-30 18:49:05 | 显示全部楼层
LZ说的很不错
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ| 无图版|手机版|小黑屋| 上海@IT精英团

Copyright © 2001-2015 Comsenz Inc.   All Rights Reserved.

Powered by Discuz! X3.4

快速回复 返回顶部 返回列表