前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用MediaCodeC将图片集编码为视频

使用MediaCodeC将图片集编码为视频

原创
作者头像
AiLo
修改2019-11-25 14:26:01
2.3K0
修改2019-11-25 14:26:01
举报
文章被收录于专栏:Android-30秒Android-30秒Android-30秒

**原创文章,转载请联系作者**

绿生莺啼春正浓,钗头青杏小,绿成丛。

玉船风动酒鳞红。歌声咽,相见几时重?

MediaLearn

欢迎大家关注我的项目MediaLearn,这是一个以学习分享音视频知识为目的建立的项目,目前仅局限于Android平台,后续会逐渐扩展。 对音视频领域知识感兴趣的朋友,欢迎一起来学习!!!

提要

这是MediaCodeC系列的第三章,主题是如何使用MediaCodeC将图片集编码为视频文件。在Android多媒体的处理上,MediaCodeC是一套非常有用的API。此次实验中,所使用的图片集正是MediaCodeC硬解码视频,并将视频帧存储为图片文件文章中,对视频解码出来的图片文件集,总共332张图片帧。 若是对MediaCodeC视频解码感兴趣的话,也可以浏览之前的文章:MediaCodeC解码视频指定帧,迅捷、精确

核心流程

MediaCodeC的常规工作流程是:拿到可用输入队列,填充数据;拿到可用输出队列,取出数据,如此往复直至结束。在一般情况下,填充和取出两个动作并不是即时的,也就是说并不是压入一帧数据,就能拿出一帧数据。当然,除了编码的视频每一帧都是关键帧的情况下。

一般情况下,输入和输出都使用buffer的代码写法如下:

for (;;) {

    //拿到可用InputBuffer的id

  int inputBufferId = codec.dequeueInputBuffer(timeoutUs);

  if (inputBufferId >= 0) {

    ByteBuffer inputBuffer = codec.getInputBuffer(…);

    // inputBuffer 填充数据

    codec.queueInputBuffer(inputBufferId, …);

  }

  // 查询是否有可用的OutputBuffe

  int outputBufferId = codec.dequeueOutputBuffer(…);

本篇文章的编码核心流程,和以上代码相差不多。只是将输入Buffer替换成了Surface,使用Surface代替InputBuffer来实现数据的填充。

为什么使用Surface

MediaCodeC官方文档里有一段关于Data Type的描述:

CodeC接受三种类型的数据,压缩数据(compressed data)、原始音频数据(raw audio data)以及原始视频数据(raw video data)。这三种数据都能被加工为ByteBuffer。但是对于原始视频数据,应该使用Surface去提升CodeC的性能。

在本次项目中,使用的是MediaCodeCcreateInputSurface函数创造出Surface,搭配OpenGL实现Surface数据输入。 这里我画了一张简单的工作流程图:

整体流程上其实和普通的MediaCodeC工作流程差不多,只不过是将输入源由Buffer换成了Surface。

知识点

在代码中,MediaCodeC只负责数据的传输,而生成MP4文件主要靠的类是MediaMuxer。整体上,项目涉及到的主要API有:

  • MediaCodeC,图片编码为帧数据
  • MediaMuxer,帧数据编码为Mp4文件
  • OpenGL,负责将图片绘制到Surface

接下来,我将会按照流程工作顺序,详解各个步骤:

流程详解

*在详解流程前,有一点要注意的是,工作流程中所有环节都必须处在同一线程。*

配置

首先,启动子线程。配置MediaCodeC:

var codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE\_VIDEO\_AVC)

// mediaFormat配置颜色格式、比特率、帧率、关键帧间隔

// 颜色格式默认为MediaCodecInfo.CodecCapabilities.COLOR\_FormatSurface

var mediaFomat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE\_VIDEO\_AVC, size.width, size.height)

            .apply {

                setInteger(MediaFormat.KEY\_COLOR\_FORMAT, colorFormat)

                setInteger(MediaFormat.KEY\_BIT\_RATE, bitRate)

                setInteger(MediaFormat.KEY\_FRAME\_RATE, frameRate)

                setInteger(MediaFormat.KEY\_I\_FRAME\_INTERVAL, iFrameInterval)

            }

codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE\_FLAG\_ENCODE)

var inputSurface = codec.createInputSurface()

codec.start()

将编码器配置好之后,接下来配置OpenGL的EGL环境以及GPU Program。由于OpenGL涉及到比较多的知识,在这里便不再赘述。视频编码项目中,为方便使用,我将OpenGL环境搭建以及GPU program搭建封装在了GLEncodeCore类中,感兴趣的可以看一下。 EGL环境在初始化时,可以选择两种和设备连接的方式,一种是eglCreatePbufferSurface;另一种是eglCreateWindowSurface,创建一个可实际显示的windowSurface,需要传一个Surface参数,毫无疑问选择这个函数。

var encodeCore = GLEncodeCore(...)

encodeCore.buildEGLSurface(inputSurface)



fun buildEGLSurface(surface: Surface) {

        // 构建EGL环境

        eglEnv.setUpEnv().buildWindowSurface(surface)

        // GPU program构建

        encodeProgram.build()

}
图片数据传入,并开始编码

在各种API配置好之后,开启一个循环,将File文件读取的Bitmap传入编码。

val videoEncoder = VideoEncoder(640, 480, 1800000, 24)

videoEncoder.start(Environment.getExternalStorageDirectory().path

                    + "/encodeyazi640${videoEncoder.bitRate}.mp4")

val file = File(图片集文件夹地址)

file.listFiles().forEachIndexed { index, it ->

    BitmapFactory.decodeFile(it.path)?.apply {

            videoEncoder.drainFrame(this, index)

        }

}

videoEncoder.drainEnd()

在提要里面也提到了,编码项目使用的图片集是之前MediaCodeC硬解码视频,并将视频帧存储为图片文件中的视频文件解码出来的,332张图片。 循环代码中,我们逐次将图片Bitmap传入drainFrame(...)函数,用于编码。当所有帧编码完成后,使用drainEnd函数通知编码器编码完成。

视频帧编码

接着我们再来看drameFrame(...)函数中的具体实现。

 /\*\*

     \*

     \* @b : draw bitmap to texture

     \*

     \* @presentTime: frame current time

     \* \*/

    fun drainFrame(b: Bitmap, presentTime: Long) {

        encodeCore.drainFrame(b, presentTime)

        drainCoder(false)

    }



    fun drainFrame(b: Bitmap, index: Int) {

        drainFrame(b, index \* mediaFormat.perFrameTime \* 1000)

    }

    

    fun drainCoder(...){

        伪代码:MediaCodeC拿到输出队列数据,使用MediaMuxer编码为

        Mp4文件

    }

首先使用OpenGL将Bitmap绘制纹理上,将数据传输到Surface上,并且需要将这个Bitmap所代表的时间戳传入。在传入数据后使用drainCoder函数,从MediaCodeC读取输出数据,使用MediaMuxer编码为Mp4视频文件。drainCoder函数具体实现如下:

loopOut@ while (true) {

        //  获取可用的输出缓存队列

        val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)

        Log.d("handleOutputBuffer", "output buffer id : $outputBufferId ")

        if (outputBufferId == MediaCodec.INFO\_TRY\_AGAIN\_LATER) {

            if (needEnd) {

                // 输出无响应

                break@loopOut

            }

        } else if (outputBufferId == MediaCodec.INFO\_OUTPUT\_FORMAT\_CHANGED) {

            // 输出数据格式改变,在这里启动mediaMuxe

        } else if (outputBufferId >= 0) {

            // 拿到相应的输出数据

            if (bufferInfo.flags and MediaCodec.BUFFER\_FLAG\_END\_OF\_STREAM != 0) {

                break@loopOut

            }

        }

    }

就像之前提到过的,并不是压入一帧数据就能即时得到一帧数据。在使用OpenGL将Bitmap绘制到纹理上,并传到Surface之后。要想得到输出数据,必须在一个无限循环的代码中,去拿MediaCodeC输出数据。 也就是在这里的代码中,当输出数据格式改变时,为MediaMuxer加上视频轨,并启动。

 trackIndex = mediaMuxer!!.addTrack(codec.outputFormat)

 mediaMuxer!!.start()

整体上的工作流程就是以上这些代码了,传入一帧数据到Surface-->MediaCodeC循环拿输出数据--> MediaMuxer写入Mp4视频文件。 当然,后两步的概念已经相对比较清晰,只有第一步的实现是一个难点,也是当时比较困扰我的一点。接下来我们将会详解,如何将一个Bitmap通过OpenGL把数据传输到Surface上。

Bitmap --> Surface

项目中,将Bitmap数据传输到Surface上,主要靠这一段代码:

fun drainFrame(b: Bitmap, presentTime: Long) {

        encodeProgram.renderBitmap(b)

        // 给渲染的这一帧设置一个时间戳

        eglEnv.setPresentationTime(presentTime)

        eglEnv.swapBuffers()

}

其中encodeProgram是显卡绘制程序,它内部会生成一个纹理,然后将Bitmap绘制到纹理上。此时这个纹理就代表了这张图片,再将纹理绘制到窗口上。 之后,使用EGL的swapBuffer提交当前渲染结果,在提交之前,使用setPresentationTime提交当前帧代表的时间戳。

更加具体的代码实现,都在我的Github项目中。GLEncodeCore以及EncodeProgram GPU Program还有EGL 环境构建

结语

此处有项目地址,点击传送

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • MediaLearn
  • 提要
  • 核心流程
    • 为什么使用Surface
    • 知识点
    • 流程详解
      • 配置
        • 图片数据传入,并开始编码
          • 视频帧编码
          • Bitmap --> Surface
          • 结语
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档