美文网首页Android开发Android开发经验谈Android技术知识
「Android音视频编码那点破事」第四章,使用MediaCod

「Android音视频编码那点破事」第四章,使用MediaCod

作者: Alimin利民 | 来源:发表于2018-06-16 22:57 被阅读49次

      本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。
    本系列文章涉及的项目HardwareVideoCodec已经开源到Github。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。


      说到Android的视频硬编码,很多新人首先会想到MediaRecorder,这可以说是Android早期版本视频硬编码的唯一选择。这个类的使用很简单,只需要给定一个Surface(输入)和一个File(输出),它就给你生成一个标准的mp4文件。
      但越是简单的东西便意味着越难以控制,MediaRecorder的缺点很明显。相信很多人在接触到断点视频录制这个需求的时候,首先会想到使用MediaRecorder,很遗憾,这个东西并不能给你很多期待,就像一开始的我一样。
      首先,MediaRecorder并没有断点录制的API,当然你可以使用一些“小技巧”,每次录制的时候,都把MediaRecorder stop掉,然后再次初始化,这样就会生成一系列的视频,最后把它们拼接起来。然而问题在于,每次初始化MediaRecorder都需要消耗很长时间,这意味着,当用户快速点击录制按钮的时候可能会出现问题。对于这个问题,你可以等到MediaRecorder初始化完成才让用户点击开始录制,但是这样往往会因为等待时间过长,导致用户体验极差。
      这种情况下,一个可控的视频编码器是必须的。虽然在Android 4.4以前我们没得选择,但是在Android 4.4之后,我们有了MediaCodec,一个完全可控的视频编码器,虽然无法直接输出mp4(需要配合MediaMuxer来对音视频进行混合,最终输出mp4,或者其它封装格式)。如今的Android生态,大部分手机都已经是Android 5.0系统,完全可以使用MediaCodec来进行音视频编码的开发,而MediaRecorder则降级作为一个提高兼容性的备选方案。
      废话不多说,我们直接步入正题。要想正确的使用MediaCodec,我们首先得先了解它的工作流程,关于这个,强烈大家去看一下Android文档。呃呃,相信在这个快速开发为王道的环境,没几个人会去看,所以还是在这里简单介绍一下。

    MediaCodec工作流程
    1. 首先,通过MediaCodec的工厂方法createEncoderByTypecreateByCodecName创建实例,这时候MediaCodec处于Uninitialized状态。
    2. 接下来,调用configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)设置编码器参数,这时候MediaCodec处于Configured状态。
    3. 正确设置各种参数之后,调用start方法,让MediaCodec开始编码,这时候MediaCodec处于Running状态。
    4. 最后顺序调用signalEndOfInputStreamstoprelease来结束编码。

      流程很简单,相信大家都能看懂。难点在于running状态,也就是上图右侧绿色部分的流程。
      当MediaCodec处于Running状态时,内部会持有两个缓冲区队列,一个输入缓冲区,一个输出缓冲区。当我们向输入缓冲区输入数据后,MediaCodec会从中取出数据,送到硬件进行编码,编码结束后送到缓冲区,这是一个异步过程,这时候我们可以从输出缓冲区取出编码后的数据。这个过程在更高版本有更好的API,新版MediaCodec可以通过回调返回编码后的数据。由于我们可以控制什么时候给编码器输入数据,所以可以随时暂停或者开始编码。
      理论讲的差不多了,接下来我们看一下具体实现。

        //初始化一个编码器配置MediaFormat
        fun createVideoFormat(parameter: Parameter, ignoreDevice: Boolean = false): MediaFormat? {
            val codecInfo = getCodecInfo(parameter.video.mime, true)
            if (!ignoreDevice && null == codecInfo) {//Unsupport codec type
                return null
            }
            val mediaFormat = MediaFormat()
            //使用H264编码
            mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC)
            //设置视频宽度
            mediaFormat.setInteger(MediaFormat.KEY_WIDTH, width)
            //设置视频高度
            mediaFormat.setInteger(MediaFormat.KEY_HEIGHT, height)
            //设置视频输入颜色格式,这里选择使用Surface作为输入,可以忽略颜色格式的问题,并且不需要直接操作输入缓冲区。
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
            //设置视频码率,这里计算公式选择一个中等码率,把3改为更大的值可以开启更高码率,通常不建议超过5
            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * fps * 3)
            //设置视频fps
            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
            //设置视频关键帧间隔,这里设置两秒一个关键帧
            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                /**
                 * 可选配置,设置码率模式
                 * BITRATE_MODE_VBR:恒定质量
                 * BITRATE_MODE_VBR:可变码率
                 * BITRATE_MODE_CBR:恒定码率
                 */
                mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)
                /**
                 * 可选配置,设置H264 Profile
                 * 需要做兼容性检查
                 */
                mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh)
                /**
                 * 可选配置,设置H264 Level
                 * 需要做兼容性检查
                 */
                mediaFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel31)
            }
            return mediaFormat
        }
    
        //初始化并配置编码器
        private fun initCodec() {
            val format = CodecHelper.createVideoFormat(parameter)
            debug_v("create codec: ${format.getString(MediaFormat.KEY_MIME)}")
            try {
                codec = MediaCodec.createEncoderByType(format.getString(MediaFormat.KEY_MIME))
                /**
                 * 配置编码器
                 */
                codec!!.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
                /**
                 * 由于我们使用Surface作为输入,所以不需要直接操作输入缓冲区,只需要把MediaCodec生成的Surface绑定到OpenGL即可,所以这里使用了一个纹理封装CodecTextureWrapper,请参考前几章的CameraTextureWrapper和ScreenTextureWrapper,或者直接查看文章末尾给出的源码。
                 */
                codecWrapper = CodecTextureWrapper(codec!!.createInputSurface(), textureId, eglContext)
                codecWrapper?.egl?.makeCurrent()
                codec!!.start()
            } catch (e: Exception) {
                debug_e("Can not create codec")
            } finally {
                if (null == codec)
                    debug_e("Can not create codec")
            }
        }
    
        /**
         * 从编码器循环取出编码数据,通过OpenGL来控制数据输入,省去了直接控制输入缓冲区的步骤,所以这里直接操控输出缓冲区即可
         */
        private fun dequeue(): Boolean {
            try {
                /**
                 * 从输出缓冲区取出一个Buffer,返回一个状态
                 * 这是一个同步操作,所以我们需要给定最大等待时间WAIT_TIME,一般设置为10000ms
                 */
                val flag = codec!!.dequeueOutputBuffer(mBufferInfo, WAIT_TIME)
                when (flag) {
                    MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {//输出缓冲区改变,通常忽略
                        debug_v("INFO_OUTPUT_BUFFERS_CHANGED")
                    }
                    MediaCodec.INFO_TRY_AGAIN_LATER -> {//等待超时,需要再次等待,通常忽略
    //                    debug_v("INFO_TRY_AGAIN_LATER")
                        return false
                    }
                /**
                 * 输出格式改变,很重要
                 * 这里必须把outputFormat设置给MediaMuxer,而不能不能用inputFormat代替,它们时不一样的,不然无法正确生成mp4文件
                 */
                    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                        debug_v("INFO_OUTPUT_FORMAT_CHANGED")
                        //这里通过回调把outputFormat送出去
                        onSampleListener?.onFormatChanged(codec!!.outputFormat)
                    }
                    else -> {
                        if (flag < 0) return@dequeue false//如果小于零,则跳过
                        val data = codec!!.outputBuffers[flag]//否则代表便阿门成功,可以从输出缓冲区队列取出数据
                        if (null != data) {
                            val endOfStream = mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            if (endOfStream == 0) {//如果没有收到BUFFER_FLAG_END_OF_STREAM信号,则代表输出数据时有效的
                                mBufferInfo.presentationTimeUs = pTimer.presentationTimeUs
                                //这里把编码后的数据通过回调送出去
                                onSampleListener?.onSample(mBufferInfo, data)
                            }
                            //缓冲区使用完后必须把它还给MediaCodec,以便再次使用,至此一个流程结束,再次循环
                            codec!!.releaseOutputBuffer(flag, false)
    //                        if (endOfStream == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
    //                            return true
    //                        }
                            return true
                        }
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return false
        }
    
        //编码结束后,停止编码器
        private fun stop(){
            while (dequeue()) {//取出编码器输出缓冲区中剩余的帧数据
            }
            debug_e("Video encoder stop")
            //编码结束,发送结束信号,让surface不在提供数据
            codec!!.signalEndOfInputStream()
            codec!!.stop()
            codec!!.release()
        }
    

      以上就是本章关于MediaCodec的全部学习内容,如果有疑问或者错误,欢迎在评论区留言。

    本章知识点:

    1. MediaCodec的工作流程。
    2. MediaCodec的使用。

    本章相关源码·HardwareVideoCodec项目

    相关文章

      网友评论

        本文标题:「Android音视频编码那点破事」第四章,使用MediaCod

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