Android视频编码

13,209 阅读3分钟

大家好,我是程序员kenney,今天给大家说说在android上如何做视频编码。 所谓视频编码就是将每帧的图片内容通过某种方式编码成视频,今天给大家介绍的是用android自带的MediaCodec进行硬编码,与前一篇文章的硬解码类似,硬编码就是利用硬件进行编码。

下面我们就来看看如何一步步实现视频硬编码:

1. 创建并配置MediaCodec

private val MIME_TYPE = "video/avc"
...
val format = MediaFormat.createVideoFormat("video/avc", width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, 5120000)
setInteger(MediaFormat.KEY_FRAME_RATE, 25)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
}
mediaCodec = MediaCodec.createEncoderByType(MIME_TYPE)
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
mediaCodec.start()

这里我们通过将帧渲染到surface上的方式向MediaCodec提供编码数据,因此KEY_COLOR_FORMATCOLOR_FormatSurface,另外码率、帧率及关键帧间隔可根据需要设置。

2. 创建EGL

什么是EGL?关于EGL的概念可以参考我的一篇文章《OpenGL ES 高级进阶:EGL及GL线程》,这里EGL的作用主要是2个,一个是向MediaCodec提供编码帧是通过将帧内容渲染到一个EGL Surface上,这个EGL Surface需要通过MediaCodec给出的input surface来创建,另一个作用是做texture的共享,因为编码通常会放到另一个线程里,和提供帧texture的线程不是同一个线程。

egl = EncodeEGL(shareContext, mediaCodec.createInputSurface()).apply {
    init()
    makeCurrent()
}

然后将这个EGL绑定到调用线程中,我的demo是将编码放到一个独立的线程上,所以只makeCurrent就可以了,不需要之后再restore回来。

3. 创建MediaMuxer

这个东西是用来进行视频容器封装的,编码只是得到了一堆视频帧数据,那播放器怎么知道怎样去播这堆数据呢?这时就需要容器这个东西,这里用的是mp4

mediaMuxer = MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

4. 渲染编码帧

这里渲染编码帧就是很简单地渲染一个texture,没什么好说的,然后还要设置帧时间戳,这样播放器播的时候才知道什么时间播哪帧,最后swapBuffers完成渲染,这样就向MediaCodec提供了一个待编码的帧。

encodeRenderer.drawFrame(texture)
egl.setTimestamp(timestamp)
egl.swapBuffers()

5. 获取编码好的帧并将数据写入文件

这一步有点复杂,因为要处理的情况比较多,这里只把一些关键的步骤列出来。

首先dequeueOutputBuffer()将编码好的一帧的index拿出来,可以设置超时时间,如果在超时时间到达时还未获取到编好的一帧,就会返回,可以通过返回的值来判断是否成功拿到了编码好的帧index

得到了index就从encoderOutputBuffers中取出编码好的帧数据,然后通过MediaMuxer将数据写入文件。

最后把dequeue出来的buffer再归还回去。

val ret = mediaCodec.dequeueOutputBuffer(bufferInfo, 0)
...
encoderOutputBuffers = mediaCodec.outputBuffers
...
val encodedData = encoderOutputBuffers[ret]
...
encodedData.position(bufferInfo.offset)
encodedData.limit(bufferInfo.offset + bufferInfo.size)
mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo)
mediaCodec.releaseOutputBuffer(ret, false)

现在来看一下demo

Thread {
    val egl = EGL().apply {
        init()
        bind()
    }
    val bitmap = decodeBitmapFromAssets("test.png")
    Thread {
        val videoEncoder = VideoEncoder()
        videoEncoder.init("/sdcard/test.mp4", 540, 540, egl.eglContext)
        for (i in 0 until 100) {
            val texture = GLUtil.bitmap2Texture(rotateBitmap(bitmap, i * 2f))
            videoEncoder.encodeFrame(texture, 100 * i * 1000000L)
            GLUtil.deleteTexture(texture)
        }
        videoEncoder.encodeFrame(0, 0)
        videoEncoder.release()
    }.start()
}.start()

视频编码比较常见的使用场景是相机录像和将一个视频生成带特效的视频,简单起见,这里我把一张图旋转成不的角度来做为视频的帧,编码出一个540*540mp4视频,帧间隔为100ms,编好的视频就可以用播放器来播放了,效果是这样的:

代码在我的github上:github.com/kenneycode/…

感谢阅读!