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

栏目: 后端 · 发布时间: 5年前

内容简介:这是若是对

这是 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, …);
  }
  // 查询是否有可用的OutputBuffer
  int outputBufferId = codec.dequeueOutputBuffer(…);
复制代码

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

为什么使用Surface

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

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

在本次项目中,使用的是MediaCodeC createInputSurface 函数创造出Surface,搭配OpenGL实现Surface数据输入。

这里我画了一张简单的工作流程图:

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

整体流程上其实和普通的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) {
            // 输出数据格式改变,在这里启动mediaMuxer
        } 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 环境构建


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Python for Everyone

Python for Everyone

Cay S. Horstmann、Rance D. Necaise / John Wiley & Sons / 2013-4-26 / GBP 181.99

Cay Horstmann's" Python for Everyone "provides readers with step-by-step guidance, a feature that is immensely helpful for building confidence and providing an outline for the task at hand. "Problem S......一起来看看 《Python for Everyone》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具