MediaCodec 处理完了之后,再从输出队列中出队得到一个可用的缓冲区,这个缓冲里面的数据就是编码或者解码后的数据了,把这些数据进行相应的处理之后,还需要释放这个缓冲区,让它回到队列中去,可供下一次使用。
MediaCodec 生命周期
另外,MediaCodec 也存在相应的 生命周期,如下图所示:
当创建了 MediaCodec 之后,是处于未初始化的 Uninitialized
状态,调用 configure 方法之后就处于 Configured
状态,调用了 start 方法之后,就处于 Executing
状态。
在 Executing
状态下开始处理数据,它又有三个子状态,分别是:
-
Flushed
-
Running
-
End of Stream
当一调用 start 方法之后,就进入了 Flushed
状态,从输入缓冲区队列中取出一个缓冲区就进入了 Running
状态,当入队的缓冲区带有 EOS
标志时, 就会切换到 End of Stream
状态, MediaCodec 不再接受入队的缓冲区,但是仍然会对已入队的且没有进行编解码操作的缓冲区进行操作、输出,直到输出的缓冲区带有 EOS
标志,表示编解码操作完成了。
在 Executing
状态下可以调用 flush 方法,使 MediaCodec 切换到 Flushed
状态。
在 Executing
状态下可以调用 stop 方法,使 MediaCodec 切换到 Uninitialized
状态,然后再次调用 configure 方法进入 Configured
状态。另外,当调用 reset 方法也会进入到 Uninitialized
状态。
当不再需要 MediaCodec 时,调用 release 方法将它释放掉,进入 Released
状态。
当 MediaCodec 工作发生异常时,会进入到 Error
状态,此时还是可以通过 reset 方法恢复过来,进入 Uninitialized
状态。
MediaCodec 调用流程
理解了 MediaCodec 的生命周期和工作流程之后,就可以上手来进行编码工作了。
以 MediaCodec 同步调用为例,使用过程如下:
// 创建 MediaCodec,此时是 Uninitialized 状态
MediaCodec codec = MediaCodec.createByCodecName(name);
// 调用 configure 进入 Configured 状态
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
// 调用 start 进入 Executing 状态,开始编解码工作
codec.start();
for (;😉 {
// 从输入缓冲区队列中取出可用缓冲区,并填充数据
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
// 从输出缓冲区队列中拿到编解码后的内容,进行相应操作后释放,供下一次使用
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
// 调用 stop 方法进入 Uninitialized 状态
codec.stop();
// 调用 release 方法释放,结束操作
codec.release();
MediaFormat 设置
首先需要创建并设置好 MediaFormat 对象,它表示媒体数据格式的相关信息,对于视频主要有以下信息要设置:
-
颜色格式
-
码率
-
码率控制模式
-
帧率
-
I 帧间隔
其中,码率就是指单位传输时间传送的数据位数,一般用 kbps
即千位每秒来表示。而帧率就是指每秒显示的帧数。
其实对于码率有三种模式可以控制:
-
BITRATE_MODE_CQ
-
表示不控制码率,尽最大可能保证图像质量
-
BITRATE_MODE_VBR
-
表示 MediaCodec 会根据图像内容的复杂度来动态调整输出码率,图像负责则码率高,图像简单则码率低
-
BITRATE_MODE_CBR
-
表示 MediaCodec 会把输出的码率控制为设定的大小
对于颜色格式,由于是将 YUV 数据编码成 H264,而 YUV 格式又有很多,这又涉及到机型兼容性问题。在对相机编码时要做好格式的处理,比如相机使用的是 NV21
格式,MediaFormat 使用的是 COLOR_FormatYUV420SemiPlanar
,也就是 NV12
模式,那么就得做一个转换,把 NV21
转换到 NV12
。
对于 I 帧间隔,也就是隔多久出现一个 H264 编码中的 I 帧。
完整 MediaFormat 设置示例:
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
// 马率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
// 调整码率的控流模式
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
// 设置帧率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
// 设置 I 帧间隔
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
当开始编解码操作时,开启编解码线程,处理相机预览返回的 YUV 数据。
在这里用到了相机的一个封装库:
https://github.com/glumes/EzCameraKit
编解码操作
编解码操作代码如下:
while (isEncoding) {
// YUV 颜色格式转换
if (!mEncodeDataQueue.isEmpty()) {
input = mEncodeDataQueue.poll();
byte[] yuv420sp = new byte[mWidth * mHeight * 3 / 2];
NV21ToNV12(input, yuv420sp, mWidth, mHeight);
input = yuv420sp;
}
if (input != null) {
try {
// 从输入缓冲区队列中拿到可用缓冲区,填充数据,再入队
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mMediaCodec.getOutputBuffers();
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
// 计算时间戳
pts = computePresentationTime(generateIndex);
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
inputBuffer.put(input);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, pts, 0);
generateIndex += 1;
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
// 从输出缓冲区队列中拿到编码好的内容,对内容进行相应处理后在释放
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
byte[] outData = new byte[bufferInfo.size];
outputBuffer.get(outData);
// flags 利用位操作,定义的 flag 都是 2 的倍数
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { // 配置相关的内容,也就是 SPS,PPS
mOutputStream.write(outData, 0, outData.length);
} else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { // 关键帧
mOutputStream.write(outData, 0, outData.length);
} else {
// 非关键帧和SPS、PPS,直接写入文件,可能是B帧或者P帧
mOutputStream.write(outData, 0, outData.length);
}
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
}
} catch (IOException e) {
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
[外链图片转存中…(img-DQ1kJN3S-1714653126570)]
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
[外链图片转存中…(img-OIyeYMYI-1714653126571)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!