分享

Android MediaCodec硬编码H264文件(四),安卓kotlin

 新用户6594HGU4 2021-11-17

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

} 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 方法进入 Unin

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

浏览器打开:qq.cn.hn/FTe 开源分享

itialized 状态

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);

}

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多