行业知识
Android MediaCodec 简明教程(六):使用 EGL 和 OpenGL 绘制图像到 Surface 上,并通过 MediaCodec 编码 Surface 数据,并保存到 MP4 文件
系列文章目录
- Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
- Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
- Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
- Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频
- Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件
文章目录
- 系列文章目录
- 前言
- Surface 编码的流程
- 使用 canvas 绘制图像到 Surface 上
- 使用 EGL 和 OpenGL 绘制图像到 Surface 上
- 总结
- 参考
前言
在教程五中,我们学习了如何使用 ByteBuffer 对数据进行编码。这种编码方式直观且简单:
- 在 onInputBufferAvailable 回调函数中,我们通过 queueInputBuffer 方法将图像数据传递给 MediaCodec。
- 在 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 。整体流程包括:
- 创建 MediaCodec 编码器,用于视频编码
- 创建 MediaMuxer 封装器,用于将编码后的数据写入视频文件中
- 调用 createInputSurface 获取 Surface
- 在一个循环中,不停地往 Surface 中绘制图像。
- 在 onOutputBufferAvailable 回调函数中,获取编码后的数据,并将其写入视频文件中。
- 编码结束,释放资源
其中 1,2,3 和 5 步,我们在前面一章已经说得很清楚了,此处不再赘述。
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVCval 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 encoderLog.d(TAG, "codec info: ${MediaClassJsonUtils.toJson(encoder.codecInfo).toString()}")val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurfaceassert(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 surfaceval inputSurface = encoder.createInputSurface()// create muxerval outputDir = externalCacheDirval 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 finishencoder.start()// draw image to surfaceval options = BitmapFactory.Options()options.inScaled = falseval 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 = falseval 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循环,不断执行以下操作:
- 调用输入Surface的lockCanvas方法获取一个Canvas对象,用于在Surface上进行绘制。
- 在Canvas上使用drawBitmap方法将之前获取的图片绘制到Surface上。
- 调用输入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则负责将渲染好的图形显示到设备的屏幕上。
个人通俗的理解:
- OpenGL 是一组绘图的 API,它规定了这些 API 的行为,通过这些 API 你可以进行绘画。你可以把OpenGL想象成一个非常熟练的画家,他擅长绘制各种2D和3D的图像。但是,他只负责创作,不管怎么将这些作品展示出来。
- EGL 负责管理窗口,给 OpenGL 提供绘制图像的地方,并管理这些画像。EGL就像是画家的助手,他负责把画家创作的作品挂到画框里,并把画框安装到墙上。不同的墙(比如在手机、电视或电脑上)可能需要不同的安装方式,但是无论在哪里,EGL都能找到合适的方法把画作展示出来。
使用 EGL 和 OpenGL 将图像绘制到 Surface 上是一件相当复杂的事情,但它有很多好处:
- OpenGL 绘制操作将运行在 GPU 上,这使得绘制效率特别高
- 使用 OpenGL 能够非常容易地对图片添加各种变化,例如滤镜、大小缩放等等
因此想要理解这一节的内容,你需要对 OpenGL 有所了解,但这超过了本文的所要讨论的范围,这部分知识需要读者自行学习。
为了简化绘制过程,我们创建了两个辅助类:
- InputSurface,它的主要职责是将OpenGL、EGL和Surface三者连接起来:通过OpenGL绘制的图像,会通过EGL传递给Surface。
- 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进行高效图像渲染的高级方法,这种方法虽然复杂,但提供了更高的绘制效率和图像处理能力。最终,开发者可以根据需求选择适合的编码方式,以实现高效的视频编码。
参考
- vid-proc
- Create Video From Images - Time-lapse on Android
- EncodeUsingSurfaceActivity
- EncodeUsingEGLAndSurfaceActivity