美文网首页音视频那点破事Android进阶之路Android进阶
【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频

【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频

作者: 开发的猫 | 来源:发表于2019-10-15 08:59 被阅读0次

    【声 明】

    首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
    其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
    最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

    码字不易,转载请注明出处!

    教程代码:【Github传送门

    目录

    一、Android音视频硬解码篇:
    二、使用OpenGL渲染视频画面篇
    三、Android FFmpeg音视频解码篇
    • 1,FFmpeg so库编译
    • 2,Android 引入FFmpeg
    • 3,Android FFmpeg视频解码播放
    • 4,Android FFmpeg+OpenSL ES音频解码播放
    • 5,Android FFmpeg+OpenGL ES播放视频
    • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
    • 7,Android FFmpeg视频编码

    本文你可以了解到

    本文主要讲解音视频的解封和封装过程,但不涉及音视频的编解码,涉及到音视频编解码的完整流程,将在下一篇章讲解完OpenGL后。主要是对音视频的重新封装有个基本了解。

    一、音视频解封

    在本篇章的第二篇文章【音视频硬解码流程】,已经讲过,Android使用的是MediaExtractor对音视频数据流进行解封。这里,我们简单再过一遍。

    • 第一步,初始化MediaExtractor
    init {
        mExtractor = MediaExtractor()
        mExtractor?.setDataSource(path)
    }
    
    • 第二步,获取音频或视频的格式
    /**
     * 获取视频格式参数
     */
    fun getVideoFormat(): MediaFormat? {
        for (i in 0 until mExtractor!!.trackCount) {
            val mediaFormat = mExtractor!!.getTrackFormat(i)
            val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
            if (mime.startsWith("video/")) {
                mVideoTrack = i
                break
            }
        }
        return if (mVideoTrack >= 0)
            mExtractor!!.getTrackFormat(mVideoTrack)
        else null
    }
    
    /**
     * 获取音频格式参数
     */
    fun getAudioFormat(): MediaFormat? {
        for (i in 0 until mExtractor!!.trackCount) {
            val mediaFormat = mExtractor!!.getTrackFormat(i)
            val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
            if (mime.startsWith("audio/")) {
                mAudioTrack = i
                break
            }
        }
        return if (mAudioTrack >= 0) {
            mExtractor!!.getTrackFormat(mAudioTrack)
        } else null
    }
    
    • 第三步,读取(分离)音视频数据
    /**
     * 读取音视频数据
     */
    fun readBuffer(byteBuffer: ByteBuffer): Int {
        byteBuffer.clear()
        selectSourceTrack()
        var readSampleCount = mExtractor!!.readSampleData(byteBuffer, 0)
        if (readSampleCount < 0) {
            return -1
        }
        //记录当前帧的时间戳
        mCurSampleTime = mExtractor!!.sampleTime
        //进入下一帧
        mExtractor!!.advance()
        return readSampleCount
    }
    
    /**
     * 选择通道
     */
    private fun selectSourceTrack() {
        if (mVideoTrack >= 0) {
            mExtractor!!.selectTrack(mVideoTrack)
        } else if (mAudioTrack >= 0) {
            mExtractor!!.selectTrack(mAudioTrack)
        }
    }
    

    二、音视频封装

    Android原生提供了一个封装器MediaMuxer,用于将已经编码好的音视频流数据封装到指定格式的文件中,MediaMuxer支持MP4、Webm、3GP三种封装格式。一般使用MP4格式。

    使用也比较简单,同样分为几个步骤:

    • 第一步,初始化
    
    class MMuxer {
        private val TAG = "MMuxer"
    
        private var mPath: String
    
        private var mMediaMuxer: MediaMuxer? = null
    
        private var mVideoTrackIndex = -1
        private var mAudioTrackIndex = -1
    
        private var mIsAudioTrackAdd = false
        private var mIsVideoTrackAdd = false
    
        private var mIsStart = false
    
        init {
            val fileName = "LVideo_" + SimpleDateFormat("yyyyMM_dd-HHmmss").format(Date()) + ".mp4"
            val filePath = Environment.getExternalStorageDirectory().absolutePath.toString() + "/"
            mPath = filePath + fileName
            mMediaMuxer = MediaMuxer(mPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
        }
        
        //......
    }
    

    这里指定了视频并保存路径和保存的格式。

    • 第二步,添加音视频轨道,设置音视频数据流格式,并启动封装器
    class MMuxer {
    
        //......
        
        fun addVideoTrack(mediaFormat: MediaFormat) {
            if (mMediaMuxer != null) {
                mVideoTrackIndex = try {
                    mMediaMuxer!!.addTrack(mediaFormat)
                } catch (e: Exception) {
                    e.printStackTrace()
                    return
                }
                mIsVideoTrackAdd = true
                startMuxer()
            }
        }
    
        fun addAudioTrack(mediaFormat: MediaFormat) {
            if (mMediaMuxer != null) {
                mAudioTrackIndex = try {
                    mMediaMuxer!!.addTrack(mediaFormat)
                } catch (e: Exception) {
                    e.printStackTrace()
                    return
                }
                mIsAudioTrackAdd = true
                startMuxer()
            }
        }
        
        /**
         *忽略音频轨道
         */
        fun setNoAudio() {
            if (mIsAudioTrackAdd) return
            mIsAudioTrackAdd = true
            startMuxer()
        }
    
        /**
         *忽略视频轨道
         */
        fun setNoVideo() {
            if (mIsVideoTrackAdd) return
            mIsVideoTrackAdd = true
            startMuxer()
        }
        
        private fun startMuxer() {
            if (mIsAudioTrackAdd && mIsVideoTrackAdd) {
                mMediaMuxer?.start()
                mIsStart = true
                Log.i(TAG, "启动混合器,等待数据输入...")
            }
        }
    
        
        //......
    }
    

    在开启封装器前,首先需要设置音视频对应的数据格式,这个格式来源于音视频解封获取的那个MediaFormat,即

    MMExtractor#getVideoFormat()

    MMExtractor#getAudioFormat()

    通过mMediaMuxer!!.addTrack(mediaFormat)后,会返回音视频数据对应的轨道索引,用于封装数据时,将数据写到正确的数据轨道中。

    最后,判断音视频轨道是否都已经配置完毕,启动封装器。

    • 第三步,写入数据,也很简单,将解封得到的数据写入即可。
    class MMuexer {
        
        //......
        
        fun writeVideoData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            if (mIsStart) {
                mMediaMuxer?.writeSampleData(mVideoTrackIndex, byteBuffer, bufferInfo)
            }
        }
    
        fun writeAudioData(byteBuffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo) {
            if (mIsStart) {
                mMediaMuxer?.writeSampleData(mAudioTrackIndex, byteBuffer, bufferInfo)
            }
        }
        
        //......
    }
    
    • 第四步,释放封装器,完成封装过程

    ==这一步非常重要,必须要释放之后,才能生成可用的完整的MP4文件==

    class MMuxer {
    
        //......
        
        fun release() {
            mIsAudioTrackAdd = false
            mIsVideoTrackAdd = false
            try {
                mMediaMuxer?.stop()
                mMediaMuxer?.release()
                mMediaMuxer = null
                Log.i(TAG, "混合器退出...")
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        
        //......
    }
    

    三、整合解封和封装流程

    通过上面两个步骤,就已经完成了最基本的工具封装,接下来只需要将它们整合起来就可以了。

    新建一个重打包类MP4Repack

    
    class MP4Repack(path: String) {
    
        private val TAG = "MP4Repack"
    
        //初始化音视频分离器
        private val mAExtractor: AudioExtractor = AudioExtractor(path)
        private val mVExtractor: VideoExtractor = VideoExtractor(path)
        
        //初始化封装器
        private val mMuxer: MMuxer = MMuxer()
    
        /**
         *启动重封装
         */
        fun start() {
            val audioFormat = mAExtractor.getFormat()
            val videoFormat = mVExtractor.getFormat()
    
            //判断是否有音频数据,没有音频数据则告诉封装器,忽略音频轨道
            if (audioFormat != null) {
                mMuxer.addAudioTrack(audioFormat)
            } else {
                mMuxer.setNoAudio()
            }
            //判断是否有视频数据,没有音频数据则告诉封装器,忽略视频轨道
            if (videoFormat != null) {
                mMuxer.addVideoTrack(videoFormat)
            } else {
                mMuxer.setNoVideo()
            }
    
            //启动线程
            Thread {
                val buffer = ByteBuffer.allocate(500 * 1024)
                val bufferInfo = MediaCodec.BufferInfo()
                
                //音频数据分离和写入
                if (audioFormat != null) {
                    var size = mAExtractor.readBuffer(buffer)
                    while (size > 0) {
                        bufferInfo.set(0, size, mAExtractor.getCurrentTimestamp(),
                                       mAExtractor.getSampleFlag())
                                       
                        mMuxer.writeAudioData(buffer, bufferInfo)
                        
                        size = mAExtractor.readBuffer(buffer)
                    }
                }
                
                //视频数据分离和写入
                if (videoFormat != null) {
                    var size = mVExtractor.readBuffer(buffer)
                    while (size > 0) {
                        bufferInfo.set(0, size, mVExtractor.getCurrentTimestamp(),
                                       mVExtractor.getSampleFlag())
                                       
                        mMuxer.writeVideoData(buffer, bufferInfo)
                        
                        size = mVExtractor.readBuffer(buffer)
                    }
                }
                mAExtractor.stop()
                mVExtractor.stop()
                mMuxer.release()
                Log.i(TAG, "MP4 重打包完成")
            }.start()
        }
    }
    

    首先,定义了音频和视频分离器,以及封装器;

    接着,判断要重封装的视频是否包含有音视频数据,没有则忽略相应的轨道;

    最后,启动线程,开始解封和封装,分为两部分:

    1. 音频数据分离和写入
    2. 视频数据分离和写入

    其中有一个要注意的就是BufferInfo的参数

    val bufferInfo = MediaCodec.BufferInfo()
    
    bufferInfo.set(0, size, mVExtractor.getCurrentTimestamp(),
                    mVExtractor.getSampleFlag())
    
    

    第一个为offset,一般为0
    第二个为数据大小,就是Extractor提取的当前帧的数据大小
    第三个为当前帧对应的时间戳,这个时间戳非常重要,影响到视频能不能正常播放,通过Extractor获取
    第四个为当前帧类型,如视频I/P/B帧,也可通过Extractor获取

    四、调用MediaRepack重封装工具实现重封装

    调用就非常简单了,如下:

    private fun repack() {
        val path = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_2.mp4"
    
        val repack = MP4Repack(path)
        repack.start()
    }
    

    到这里,本篇章【音视频硬解码篇】系列文章就结束了,本系列共四篇文章,从【音视频基础知识介绍】->【Android音解码流程】->【音视频播放与同步】->【视频解封与封装】,比较全面的介绍了Android应用系统提供的硬解码能力,实现音视频的解码。

    接下来,将进入OpenGL渲染篇系列文章,将进一步介绍音视频渲染、重编码、封装等内容,敬请关注。

    相关文章

      网友评论

        本文标题:【Android 音视频开发打怪升级:音视频硬解码篇】四、音视频

        本文链接:https://www.haomeiwen.com/subject/nsvructx.html