原创文章,未经作者允许不得转载

秋风清,秋月明

叶叶梧桐槛外声

难教归梦成

在上一篇文章Camera2录制视频(一):音频的录制及编码,主要分享了使用Camera2搭配MediaCodeC和MediaMuxer进行视频录制中的音频录制部分。那么在这篇文章中呢,就着手分析使用MediaCodeC完成视频的录制编码和MediaMuxer完成Mux视频合成模块。有关使用MediaCodeC硬编码对视频编解码的相关视频,我之前也有分享,想看的朋友们可以点击以下传送门回顾。

MediaCodeC硬编码将图片集编码为视频Mp4文件MediaCodeC编码视频

MediaCodeC将视频完整解码,并存储为图片文件。使用两种不同的方式,硬编码解码视频

MediaCodeC解码视频指定帧硬编码解码指定帧

概述

项目中使用的摄像头API为Camera2

在文章开始之前,依然是老规矩,我们从结果导向,梳理流程。看看在视频录制这个阶段,流程是如何运作的,数据在这其中发生了什么变化。

流程梳理

当设备的摄像头在运转时,sensor【传感器】会将光信号转为电信号,再转为数字信号。sensor会输出四种格式的图片格式:YUV、RGB、RAW RGB DATA、JPEG。YUV是最常用的一种格式,YUV输出的数据中亮度信号是无损的,RGB会有一定的损耗会丢掉一些原始信息。而RAW DATA是最原始的信息,但是存储空间会变大,而且需要一些特定软件才能打开。
在废弃的摄像头API——Camera中,默认的预览回调数据格式就是NV21。而在Camera2中,函数只提供了Surface作为桥接对象。若是想获取YUV、或者JPEG和RAW_SENSOR的话,可以使用ImageReader提供的Surface,再通过监听获取图像信息。
不管是Camera还是Camera2,都是支持设置Surface的。通过Surface,我们可以将Camera拿到的数据直接输送到GPU通过OpenGL来渲染处理。这样可以不用再CPU中处理Camera帧数据,从而节省大量时间。好了,接下来我用一张流程图,来展示MediaCodeC如何编码Camera帧数据的。

  • 1、因为我们需要的是H264数据,所以需要给MediaCodeC配置Mime为video/avc
  • 2、MediaCodeC配置好之后,通过createInputSurface创建出一个作输入的Input—Surface
  • 3、将Input-surface作为参数,配置Android平台EGL环境的windowSurface
  • 4、创建OpenGL的program程序,得到一个可用的纹理ID,从而构建出一个SurfaceTexture。这个对象可以提供给已经废弃的CameraAPI,也可以构建出一个Surface提供给Camera2API。

通过以上操作,我们就可以把Camera采集的数据直接传入到GPU,不用在CPU中费力的处理一番。

代码实现

在上一篇文章我提到过,会将整个视频录制中涉及到的各个功能模块化,以供后续复用。那么在视频的录制编码这块,我将它分装为了一个Runnable——VideoRecorderVideoRecorder的内部职责为,封装了MediaCodeC+OpenGL编码的流程。对外提供OpenGL纹理的Surface,和硬编码编码后的ByteBuffer、以及BufferInfo和其他视频帧相关的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// MediaCodeC配置
codec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

val s = codec.createInputSurface()
val surfaceTexture = encodeCore.buildEGLSurface(s)
inputSurface = Surface(surfaceTexture)
// 构建一个搭载了OpenGL纹理的Surface,然后回调出去
readySurface.invoke(inputSurface)
// 开始编码
codec.start()

// 计时
val startTime = System.nanoTime()

// 使用数组来保持视频录制线程和音频录制线程以及Mux线程的同步
while (isRecording.isNotEmpty()) {
// 编码数据
drainEncoder(false)
frameCount++
// OpenGL 绘制
encodeCore.draw()
val curFrameTime = System.nanoTime() - startTime
encodeCore.swapData(curFrameTime)
}
// 发送编码结束信号
drainEncoder(true)

以上伪代码代表了视频编码的全部流程,首先我们需要配置一个合适的MediaCodeC,通过MediaCodeC的codec.createInputSurface函数得到一个Surface对象,前文我称之为InputSurface。然后配置EGL环境,构建OpenGLProgram。【有关OpenGL相关的代码,我都封装到了SurfaceEncodeCore这个类里面。SurfaceEncodeCore的内部职责主要是:构建EGL环境,配置OpenGL程序,绘制纹理】。
OpenGL本身是不负责窗口管理和上下文环境管理的,这个功能由各自平台提供。Android里负责为OpenGL提供窗口管理和上下文环境管理的就是EGL。在EGL里,是使用EGLSurface将输出渲染到设备屏幕。而创建EGLSurface有两种方式,一种是创建一个可实际显示的Surface,通过eglCreateWindowSurface函数,而这个函数需要一个Surface作为参数。另一个是通过eglCreatePbufferSurface创建一个离屏Surface。至此,MediaCodeC的输入渠道就搭建完毕,这个渠道会在录制期间,不停接受Camera回调的数据,并通过OpenGLProgram处理。我们只需要从MediaCodeC源源不断地提取出已经编码好的H264码流,对外回调视频帧数据即可。
函数drainEncoder的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
fun MediaCodec.handleOutputBuffer(bufferInfo: MediaCodec.BufferInfo, defTimeOut: Long,
formatChanged: () -> Unit = {},
render: (bufferId: Int) -> Unit,
needEnd: Boolean = true) {
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) {
formatChanged.invoke()
} else if (outputBufferId >= 0) {
render.invoke(outputBufferId)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break@loopOut
}
}
}
}

private fun drainEncoder(isEnd: Boolean = false) {
if (isEnd) {
codec.signalEndOfInputStream()
}
codec.handleOutputBuffer(bufferInfo, 2500, {
if (!isFormatChanged) {
outputFormatChanged.invoke(codec.outputFormat)
isFormatChanged = true
}
}, {
val encodedData = codec.getOutputBuffer(it)
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
bufferInfo.size = 0
}
if (bufferInfo.size != 0) {
Log.d(TAG, "buffer info offset ${bufferInfo.offset} time is ${bufferInfo.presentationTimeUs} ")
encodedData.position(bufferInfo.offset)
encodedData.limit(bufferInfo.offset + bufferInfo.size)
Log.d(TAG, "sent " + bufferInfo.size + " bytes to muxer")
dataCallback.invoke(frameCount, bufferInfo.presentationTimeUs, bufferInfo, encodedData)
}
codec.releaseOutputBuffer(it, false)
}, !isEnd)
}

这是MediaCodeC处理输出数据的老一套代码了,根据dequeueOutputBuffer返回的ID,确认目前编码器处于何种状态。再分别加以处理,将得到的原始数据对外回调。

混合器Mux模块

至此为止,整个视频录制功能中,视频录制编码模块完成、音频录制编码模块完成,只需要一个Mux模块。将其余两个模块提供的数据,串联起来输出Mp4文件即可。
在Mux模块中,已经没有什么技术含量了,具体工作就是,维护了两个数据队列。一个是视频帧队列,另一个是音频帧队列。Mux模块无限循环地从两个队列中提取队列首端数据。然后比较视频帧数据和音频帧数据中的时间戳大小,将时间小的先行封装即可。具体代码可参考Muxer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
mediaMuxer = MediaMuxer(p, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
// 添加视频轨
val videoTrackId = mediaMuxer!!.addTrack(videoTrack)
// 添加音频轨
val audioTrackId = mediaMuxer!!.addTrack(audioTrack)
mediaMuxer!!.start()

while (isRecording.isNotEmpty()) {
// 从队列中提取首端数据
val videoFrame = videoQueue.firstSafe
val audioFrame = audioQueue.firstSafe

val videoTime = videoFrame?.bufferInfo?.presentationTimeUs ?: -1L
val audioTime = audioFrame?.bufferInfo?.presentationTimeUs ?: -1L

// 比较音频帧和视频帧的时间戳
if (videoTime == -1L && audioTime != -1L) {
writeAudio(audioTrackId)
} else if (audioTime == -1L && videoTime != -1L) {
writeVideo(videoTrackId)
} else if (audioTime != -1L && videoTime != -1L) {
// 先写小一点的时间戳的数据
if (audioTime < videoTime) {
// 封装音频帧数据
writeAudio(audioTrackId)
} else {
// 封装视频帧数据
writeVideo(videoTrackId)
}
} else {
// do nothing
}
}

Camera2视频尺寸选择

好了。整个视频录制全部功能已全部整理完毕。接下来我们分析一个视频的尺寸选择问题,以及Camera2中使我迷惑的点————如何选择视频尺寸?

在Android官方文档中,要想获取Camera2摄像头数据,必须依靠Surface。To capture or stream images from a camera device, the application must first create a camera capture session with a set of output Surfaces for use with the camera device, with createCaptureSession(SessionConfiguration).。Camera2会根据你配置的Surface来匹配相应的尺寸,Each Surface has to be pre-configured with an appropriate size and format (if applicable) to match the sizes and formats available from the camera device,每一个Surface都必须提前配置好相应的尺寸,以便去匹配Camera2合适的Size。
Camera2在配置的时候,会返回一个可供选择的尺寸集合,表示当前设备摄像头所支持的所有尺寸。我在测试视频录制时,测试设备返回的尺寸列表如下:

那么OK。既然知道了支持的尺寸,那么我在配置MediaCodeC的时候,设置的宽高从这里面选择就ok了,就不用再进一步的图像处理了。可是实际我试验的结果却不理想,在MediaCodeC设置第一个尺寸的时候,录制的视频画面毫无变形。可选择其他尺寸譬如720 X 960、720 X 1280却变形严重。可当我选择1088 X 1088 或者 960 X 960,这种等宽高尺寸时,录制的画面却毫无变形。对此我也是毫无头绪,因为摸不清楚Surface匹配的尺寸机制问题,在OpenGL绘制的时候,就不知道该如何裁剪,这才是大问题。如果有解决这个问题的朋友,希望能给我提一些建议。

以上