写点什么

Android MediaCodec 硬编码 H264 文件 (四),八年 Android 开发心路历程

作者:嘟嘟侠客
  • 2021 年 11 月 27 日
  • 本文字数:3445 字

    阅读完需:约 11 分钟

在?Executing?状态下可以调用 flush 方法,使 MediaCodec 切换到?Flushed?状态。


在?Executing?状态下可以调用 stop 方法,使 MediaCodec 切换到?Uninitialized?状态,然后再次


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


调用 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, …);


} elseif (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 = newbyte[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 = newbyte[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);


} elseif ((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) {


Log.e(TAG, e.getMessage());


}


} else {


try {


Thread.sleep(500);


} catch (InterruptedException e) {


Log.e(TAG, e.getMessage());


}


}


}


首先,要把要把相机的?NV21?格式转换成?NV12?格式,然后 通过?dequeueInputBuffer?方法去从可用的输入缓冲区队列中出队取出缓冲区,填充完数据后再通过?queueInputBuffer?方法入队。


dequeueInputBuffer?返回缓冲区索引,如果索引小于 0 ,则表示当前没有可用的缓冲区。它的参数?timeoutUs?表示超时时间 ,毕竟用的是 MediaCodec 的同步模式,如果没有可用缓冲区,就会阻塞指定参数时间,如果参数为负数,则会一直阻塞下去。


queueInputBuffer?方法将数据入队时,除了要传递出队时的索引值,然后还需要传入当前缓冲区的时间戳?presentationTimeUs?和当前缓冲区的一个标识?flag?。


其中,时间戳通常是缓冲区渲染的时间,而标识则有多种标识,标识当前缓冲区属于那种类型:

学习分享

在当下这个信息共享的时代,很多资源都可以在网络上找到,只取决于你愿不愿意找或是找的方法对不对了


很多朋友不是没有资料,大多都是有几十上百个 G,但是杂乱无章,不知道怎么看从哪看起,甚至是看后就忘


如果大家觉得自己在网上找的资料非常杂乱、不成体系的话,我也分享一套给大家,比较系统,我平常自己也会经常研读。


2021 最新上万页的大厂面试真题



七大模块学习资料:如 NDK 模块开发、Android 框架体系架构…



只有系统,有方向的学习,才能在段时间内迅速提高自己的技术。


这份体系学习笔记,适应人群:**第一,**学习知识比较碎片化,没有合理的学习路线与进阶方向。**第二,**开发几年,不知道如何进阶更进一步,比较迷茫。**第三,**到了合适的年纪,后续不知道该如何发展,转型管理,还是加强技术研究。

由于文章内容比较多,篇幅不允许,部分未展示内容以截图方式展示 。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
Android MediaCodec硬编码H264文件(四),八年Android开发心路历程