Android MediaCodec 简明教程(六):使用 EGL 和 OpenGL 绘制图像到 Surface 上,并通过 MediaCodec 编码 Surface 数据,并保存到 MP4 文件

系列文章目录

  1. Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
  2. Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
  3. Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
  4. Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频
  5. Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件


前言

教程五中,我们学习了如何使用 ByteBuffer 对数据进行编码。这种编码方式直观且简单:

  1. onInputBufferAvailable 回调函数中,我们通过 queueInputBuffer 方法将图像数据传递给 MediaCodec。
  2. onOutputBufferAvailable 回调函数中,我们通过 getOutputBuffer 方法获取编码后的数据,并将其写入文件。

然而,这种方法的效率并不高。在本章节,我们将介绍如何使用 Surface 对 MediaCodec 进行编码,这是一种更高效的方法,也是官方推荐的方法。

Surface 编码的流程

首先,MediaCodec 提供一个 Surface 对象,你可以将它视为一张空白的画布,我们需要在这个画布上绘制图像信息。完成绘制后,我们通知 MediaCodec:“好了,你可以开始编码了”。 然后,MediaCodec 开始执行其任务,它从 Surface 对象中获取图像的像素信息,接着进行视频编码。编码完成后,通过 onOutputBufferAvailable 回调,返回编码后的数据。

具体到代码上,通过 createInputSurface 获取 MediaCodec 的 Surface 对象

val surface = encoder.createInputSurface()

在将图像绘制到Surface上的过程中,我们通常会使用OpenGL和EGL来完成这项工作。当然,我们也可以采用其他的方法,比如从Surface中获取canvas,然后在canvas上进行图像的绘制。

在图像绘制完成后,我们需要通知MediaCodec进行编码。在EGL中,有一个叫做eglSwapBuffers的方法,它的作用是交换前后缓冲区的内容,也就是将渲染好的图像显示在屏幕上。通过调用这个方法,我们可以告诉MediaCodec有新的图像数据需要进行编码。

虽然在Canvas中我们无法直接调用eglSwapBuffers方法,但我们可以通过Surface的unlockCanvasAndPost方法来实现类似的效果。

下面,我将分别介绍这两种方法。需要指出的是,使用OpenGL和EGL是一种更常见的做法,尽管它的实现过程可能更为复杂。而使用Canvas的方法,主要是在不使用OpenGL和EGL的情况下,为了演示MediaCodec的使用流程而提供的一种方式。

使用 canvas 绘制图像到 Surface 上

这部分完整代码在 EncodeUsingSurfaceActivity 。整体流程包括:

  1. 创建 MediaCodec 编码器,用于视频编码
  2. 创建 MediaMuxer 封装器,用于将编码后的数据写入视频文件中
  3. 调用 createInputSurface 获取 Surface
  4. 在一个循环中,不停地往 Surface 中绘制图像。
  5. onOutputBufferAvailable 回调函数中,获取编码后的数据,并将其写入视频文件中。
  6. 编码结束,释放资源

其中 1,2,3 和 5 步,我们在前面一章已经说得很清楚了,此处不再赘述。

val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val encodeCodecName = codecList.findEncoderForFormat(format)
val encoder = MediaCodec.createByCodecName(encodeCodecName)
encoder.setCallback(object: MediaCodec.Callback(){
    override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
        // do nothing
    }
    override fun onOutputBufferAvailable(
        codec: MediaCodec,
        index: Int,
        info: MediaCodec.BufferInfo
    ) {
        Log.d(TAG, "encoder output buffer available: $index")
        // output eos
        val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
        if(isDone || frameIndex == NUM_FRAMES)
        {
            Log.d(TAG, "encoder output eos")
            outputEnd.set(true)
            info.size = 0
        }
        // got encoded frame, write it to muxer
        if(info.size > 0){
            if (!isMuxerStarted) {
                throw RuntimeException("muxer hasn't started");
            }
            val encodedData = codec.getOutputBuffer(index)
            encodedData?.position(info.offset)
            encodedData?.limit(info.offset + info.size)
            info.presentationTimeUs = computePresentationTime(frameIndex)
            muxer.writeSampleData(videoTrackIndex, encodedData!!, info)
            Log.d(TAG, "encoder output buffer: $index, size: ${info.size}, pts: ${info.presentationTimeUs}")
            codec.releaseOutputBuffer(index, false)
            frameIndex++
        }
    }
    override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
        e.printStackTrace()
    }
    override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
        Log.d(TAG, "encoder output format changed: $format")
        videoTrackIndex = muxer.addTrack(format)
        muxer.start()
        isMuxerStarted = true
    }
})
// configure the encoder
Log.d(TAG, "codec info: ${MediaClassJsonUtils.toJson(encoder.codecInfo).toString()}")
val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
assert(encoder.codecInfo.getCapabilitiesForType(mimeType).colorFormats.contains(colorFormat))
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)
Log.d(TAG, "format: $format")
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// create input surface
val inputSurface = encoder.createInputSurface()
// create muxer
val outputDir = externalCacheDir
val outputName = "test_0.mp4"
val outputFile = File(outputDir, outputName)
Log.d(TAG, "output file: ${outputFile.absolutePath}")
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// start encoder and wait it to finish
encoder.start()

// draw image to surface
val options = BitmapFactory.Options()
options.inScaled = false
val bitmap = BitmapFactory.decodeResource(resources, R.raw.test_img_1280x853, options)
while (!outputEnd.get())
{
    val canvas = inputSurface.lockCanvas(null)
    canvas.drawBitmap(bitmap, 0f, 0f, null)
    inputSurface.unlockCanvasAndPost(canvas)
    Thread.sleep(10)
}

encoder.stop()
muxer.stop()
encoder.release()

上面是完整代码,我们重点看 3 和 4 两步。

val inputSurface = encoder.createInputSurface()

通过 createInputSurface 创建编码器使用的 Surface。能否创建多个 InputSurface 呢?我实验下来是不行的,只能创建一个,否则编码过程会卡死在什么地方。

val options = BitmapFactory.Options()
options.inScaled = false
val bitmap = BitmapFactory.decodeResource(resources, R.raw.test_img_1280x853, options)
while (!outputEnd.get())
{
    val canvas = inputSurface.lockCanvas(null)
    canvas.drawBitmap(bitmap, 0f, 0f, null)
    inputSurface.unlockCanvasAndPost(canvas)
    Thread.sleep(10)
}

首先,通过BitmapFactory.Options()创建一个新的BitmapFactory.Options对象,并设置inScaled属性为false,这意味着位图将不会进行缩放。

然后,使用BitmapFactory.decodeResource方法从资源中加载一张位图。这里的资源ID是R.raw.test_img_1280x853,表示这是一张存储在raw资源目录下的名为test_img_1280x853的图片。

然后,通过一个while循环,不断执行以下操作:

  1. 调用输入Surface的lockCanvas方法获取一个Canvas对象,用于在Surface上进行绘制。
  2. 在Canvas上使用drawBitmap方法将之前获取的图片绘制到Surface上。
  3. 调用输入Surface的unlockCanvasAndPost方法将Canvas对象解锁并提交绘制内容,从而触发eglSwapBuffers方法,告知MediaCodec有输入的图像数据需要进行编码。

最后,通过Thread.sleep(10)让当前线程暂停10毫秒,这样可以控制绘制的速度。

使用 EGL 和 OpenGL 绘制图像到 Surface 上

EGL 是什么?

EGL(Embedded System Graphics Library)是一个嵌入式系统图形库,它是OpenGL ES和底层原生窗口系统之间的接口。在Android中,EGL用于管理OpenGL ES的上下文和表面,以及与Android系统中的Surface进行交互。它提供了一种标准的方式来创建和管理OpenGL ES上下文,并将渲染结果显示在屏幕上。EGL还提供了一些与窗口系统交互的API,例如创建和管理窗口、处理事件等。在Android中,EGL是实现高性能图形渲染和显示的关键技术之一。

OpenGL 是什么?

OpenGL(Open Graphics Library)是一个跨平台的图形渲染API,可以用于创建高性能的2D和3D图形应用程序。在Android中,OpenGL ES(OpenGL for Embedded Systems)是OpenGL的一个子集,专门用于嵌入式系统中的图形渲染。OpenGL ES提供了一些轻量级的API,可以在移动设备等嵌入式系统中实现高性能的图形渲染和显示。在Android中,OpenGL ES通常与EGL一起使用,通过EGL来管理OpenGL ES的上下文和表面,并将渲染结果显示在屏幕上。使用OpenGL ES可以实现各种复杂的图形效果,例如光影、反射、阴影等,是Android游戏和图形应用程序开发中不可或缺的技术之一。

EGL 与 OpenGL 之间有什么关系?

OpenGL(Open Graphics Library)是一个用于渲染2D和3D图形的跨平台图形API。它提供了一套丰富的图形渲染函数,开发者可以通过这些函数来创建复杂的2D或3D图形。
EGL(Embedded-System Graphics Library)则是一个在OpenGL和本地平台窗口系统之间的中间层。它提供了一种方式,使得OpenGL可以在不同的设备和窗口系统上进行图形渲染。EGL主要负责管理OpenGL的图形上下文和渲染表面,以及处理渲染表面的交换。
简单来说,OpenGL负责图形的渲染,而EGL则负责将渲染好的图形显示到设备的屏幕上。

个人通俗的理解:

  1. OpenGL 是一组绘图的 API,它规定了这些 API 的行为,通过这些 API 你可以进行绘画。你可以把OpenGL想象成一个非常熟练的画家,他擅长绘制各种2D和3D的图像。但是,他只负责创作,不管怎么将这些作品展示出来。
  2. EGL 负责管理窗口,给 OpenGL 提供绘制图像的地方,并管理这些画像。EGL就像是画家的助手,他负责把画家创作的作品挂到画框里,并把画框安装到墙上。不同的墙(比如在手机、电视或电脑上)可能需要不同的安装方式,但是无论在哪里,EGL都能找到合适的方法把画作展示出来。

使用 EGL 和 OpenGL 将图像绘制到 Surface 上是一件相当复杂的事情,但它有很多好处:

  1. OpenGL 绘制操作将运行在 GPU 上,这使得绘制效率特别高
  2. 使用 OpenGL 能够非常容易地对图片添加各种变化,例如滤镜、大小缩放等等

因此想要理解这一节的内容,你需要对 OpenGL 有所了解,但这超过了本文的所要讨论的范围,这部分知识需要读者自行学习。

为了简化绘制过程,我们创建了两个辅助类:

  1. InputSurface,它的主要职责是将OpenGL、EGL和Surface三者连接起来:通过OpenGL绘制的图像,会通过EGL传递给Surface。
  2. TextureRenderer,它的主要功能是利用OpenGL API将一张图片渲染到GPU上。

完整代码你可以在 EncodeUsingEGLAndSurfaceActivity 找到

//..
val inputSurface = InputSurface(encoder.createInputSurface())
inputSurface.makeCurrent()
val renderer = TextureRenderer()
// ...
while (!outputEnd.get())
{
    renderer.draw(videoWidth, videoHeight, bitmap, getMvp())
    val nanoPts = computePresentationTime(inputFrameIndex) * 1000 // us
    inputSurface.setPresentationTime(nanoPts)
    inputSurface.swapBuffers()
    Thread.sleep(10)
    inputFrameIndex++
}
//..

代码整体结构与上一个例子类似,区别在于如何绘制图片。上面的代码使用了InputSurface和TextureRenderer类来将一张图片绘制到编码器的输入Surface上,并不断重复这个过程,直到输出结束。具体解释如下:

首先,通过encoder.createInputSurface()方法创建一个输入Surface,并使用InputSurface类将其封装为一个方便操作的对象inputSurface。然后,通过inputSurface.makeCurrent()方法将当前线程的EGL上下文切换到inputSurface的上下文中。

接着,创建一个TextureRenderer对象renderer,并使用它的draw方法将之前获取的图片绘制到输入Surface上。具体来说,draw方法会将图片绑定到OpenGL纹理上,并使用OpenGL ES的API将纹理绘制到输入Surface上。

然后,计算出当前帧的时间戳nanoPts,并使用inputSurface.setPresentationTime(nanoPts)方法将时间戳设置到输入Surface上。最后,使用inputSurface.swapBuffers()方法将渲染好的图像显示在屏幕上,并告知MediaCodec有输入的图像数据需要进行编码。

总结

本博客介绍了使用Surface进行高效的MediaCodec视频编码技术。与ByteBuffer编码相比,Surface编码能更好地利用硬件加速,提升性能。文章首先概述了Surface编码流程,即通过MediaCodec提供的Surface绘制图像,然后编码器提取像素信息进行编码。接着,文章详细说明了使用Canvas在Surface上绘制图像的基本方法,包括创建编码器、获取Surface,并在一个循环中提交图像数据给MediaCodec。此外,还探讨了结合EGL和OpenGL进行高效图像渲染的高级方法,这种方法虽然复杂,但提供了更高的绘制效率和图像处理能力。最终,开发者可以根据需求选择适合的编码方式,以实现高效的视频编码。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值