Android开发Android开发经验谈Android技术知识

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

2018-06-16  本文已影响49人  Alimin利民

  本章仅对部分代码进行讲解,以帮助读者更好的理解章节内容。
本系列文章涉及的项目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项目

上一篇下一篇

猜你喜欢

热点阅读