10、MediaCodec实战:视频硬解码、视频硬编码、音视频数据分离与合成
好,咱们今天聊点硬核的。MediaCodec,这个名字你肯定不陌生。Android上做音视频开发,绕不开它。说白了,它就是系统提供给咱们的一个底层多媒体编解码器接口。你可以用它来硬解码、硬编码,还能做音视频数据的分离与合成。
我个人习惯把MediaCodec比作一个“黑盒子”。你往里扔原始数据,它给你吐出解码后的数据。或者反过来,你扔原始图像,它给你编码成压缩流。嗯,听起来简单,但实际用起来,坑不少。我刚开始接触的时候,也被它的异步回调机制搞得晕头转向。
10.1 视频硬解码:从压缩流到原始帧
视频硬解码,就是把H.264、H.265这些压缩过的视频流,还原成原始的YUV或RGB帧。为什么要用硬解码?省电、效率高。CPU干这事太累了,交给GPU或专门的解码芯片,又快又稳。
核心流程其实就四步:
- 创建解码器:根据你的视频格式,比如H.264,创建一个MediaCodec实例。
- 配置解码器:设置好输入输出格式,比如分辨率、码率这些。
- 启动解码器:调用start()方法,开始干活。
- 循环处理数据:从输入缓冲区塞数据,从输出缓冲区取数据。
来看一段核心代码,我习惯这么写:
// 创建解码器,指定MIME类型
MediaCodec decoder = MediaCodec.createDecoderByType("video/avc");
// 配置解码器,MediaFormat是关键
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
decoder.configure(format, surface, null, 0);
// 启动
decoder.start();
// 循环处理
while (!isFinished) {
// 获取输入缓冲区索引
int inputIndex = decoder.dequeueInputBuffer(10000);
if (inputIndex >= 0) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
// 从文件或网络读取数据,填充到inputBuffer
int sampleSize = readSampleData(inputBuffer);
// 塞给解码器
decoder.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);
}
// 获取输出缓冲区索引
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex >= 0) {
// 输出到Surface,或者自己处理数据
decoder.releaseOutputBuffer(outputIndex, true);
}
}
关键点:如果你只是想把视频渲染到屏幕上,直接把Surface传给configure()方法就行。解码器会自动把数据渲染上去,省事。但如果你要拿到原始帧做处理,比如滤镜、裁剪,那就得用ImageReader了。
我的经验:dequeueInputBuffer和dequeueOutputBuffer的超时时间,我一般设10毫秒。太短了CPU空转,太长了界面卡顿。这个值你可以根据实际场景微调。
10.2 视频硬编码:从原始帧到压缩流
编码和解码,流程是对称的。只不过数据流向反过来了。你给编码器原始帧,它给你吐出压缩后的H.264或H.265流。
编码的场景很多,比如录屏、摄像头采集、视频编辑。我记得有一次做直播推流,需要把摄像头采集的NV21数据编码成H.264,然后推送到服务器。那时候对MediaCodec的输入格式不熟,踩了不少坑。
编码器的配置稍微复杂一点:
// 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
// 配置编码器
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000); // 2Mbps
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔1秒
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
// 获取输入Surface
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// 然后你就可以通过这个Surface往编码器送数据了
// 比如用OpenGL ES渲染,或者用MediaCodec的输入缓冲区
注意:编码器的输入格式,我强烈建议用Surface模式。为什么?因为效率高。你通过Surface送数据,编码器内部会做格式转换和内存管理。如果你自己手动填ByteBuffer,很容易遇到颜色格式不匹配的问题。我曾经就被COLOR_FormatYUV420Planar和COLOR_FormatYUV420SemiPlanar搞到头大。
编码后的数据,从输出缓冲区取出来,就是标准的H.264码流。你可以直接存成文件,或者通过网络发送。
10.3 音视频数据分离:解封装的艺术
一个MP4文件里,视频和音频是交织在一起的。要分别处理它们,得先分离。Android上做这事,用MediaExtractor。
MediaExtractor的作用,就是从容器文件中提取出压缩后的音视频数据包。它不负责解码,只负责“拆包”。
流程很简单:
- 创建MediaExtractor实例。
- 设置数据源,可以是文件路径,也可以是FileDescriptor。
- 遍历轨道,找到视频轨和音频轨。
- 选中轨道,然后循环读取数据包。
看代码:
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);
int videoTrackIndex = -1;
int audioTrackIndex = -1;
// 遍历轨道
for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
videoTrackIndex = i;
} else if (mime.startsWith("audio/")) {
audioTrackIndex = i;
}
}
// 选中视频轨,开始读取
extractor.selectTrack(videoTrackIndex);
while (true) {
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) break;
long presentationTimeUs = extractor.getSampleTime();
int flags = extractor.getSampleFlags();
// 这里拿到的是压缩后的数据包,可以送给解码器
// ...
extractor.advance();
}
核心要点:MediaExtractor读取出来的数据,是压缩后的。也就是说,视频轨读出来的是H.264包,音频轨读出来的是AAC包。这些数据可以直接喂给MediaCodec解码器。
10.4 音视频数据合成:重新封装
合成,就是把处理后的音视频数据,重新打包成一个容器文件,比如MP4。Android上做这事,用MediaMuxer。
MediaMuxer的使用,比MediaExtractor稍微复杂一点。因为它需要你先添加轨道,然后一帧一帧地写入数据。
基本步骤:
- 创建MediaMuxer实例,指定输出文件路径和格式。
- 添加视频轨和音频轨,得到轨道索引。
- 循环写入数据包,注意要按时间戳顺序写入。
- 写入完成后,调用stop()和release()。
代码示例:
MediaMuxer muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 假设你已经有了视频和音频的MediaFormat
int videoTrackIndex = muxer.addTrack(videoFormat);
int audioTrackIndex = muxer.addTrack(audioFormat);
muxer.start();
// 写入视频数据
while (hasVideoData) {
ByteBuffer buffer = getVideoData();
MediaCodec.BufferInfo info = getVideoBufferInfo();
muxer.writeSampleData(videoTrackIndex, buffer, info);
}
// 写入音频数据
while (hasAudioData) {
ByteBuffer buffer = getAudioData();
MediaCodec.BufferInfo info = getAudioBufferInfo();
muxer.writeSampleData(audioTrackIndex, buffer, info);
}
muxer.stop();
muxer.release();
重要提醒:MediaMuxer要求写入的数据包,时间戳必须是单调递增的。如果你乱序写入,生成的文件可能会播放异常。我遇到过一个问题,编码器输出的B帧时间戳不是递增的,导致合成后的视频花屏。解决办法是在写入前,先按时间戳排序。
10.5 实战:音视频分离与合成完整流程
把上面这些串起来,就是一个完整的音视频处理流程。我画了一张图,帮你理清思路:
你看,流程很清晰。从输入文件开始,用MediaExtractor分离出视频轨和音频轨。然后分别用MediaCodec解码,得到原始帧和原始音频数据。这时候你可以做各种处理,比如加滤镜、调音量、变速。处理完之后,再用MediaCodec编码回去,最后用MediaMuxer合成新的MP4文件。
我的建议:如果你只是做简单的裁剪或拼接,可以不经过解码编码,直接用MediaExtractor和MediaMuxer操作压缩数据。这样效率更高,而且不会损失画质。但如果你要做滤镜、特效,那就必须解码到原始帧了。
好了,这一章的内容就到这里。MediaCodec的用法,说白了就是“配置-启动-循环处理”这个套路。你多写几遍,自然就熟了。遇到问题别慌,先检查格式配置,再检查时间戳,大部分坑都能填上。