系列文章目录
- 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 文件
前言
在教程五中,我们学习了如何使用 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_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循环,不断执行以下操作:
- 调用输入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进行高效图像渲染的高级方法,这种方法虽然复杂,但提供了更高的绘制效率和图像处理能力。最终,开发者可以根据需求选择适合的编码方式,以实现高效的视频编码。