5、视频H.264编码实战:H.264编码参数详解与编码器配置
各位同学,今天我们来聊聊H.264编码实战。说实话,H.264这块内容,我在刚入行那会儿也觉得挺头疼的。参数多,配置复杂,稍不注意就踩坑。但别担心,今天我把这些年的经验都掏出来,咱们一起捋一遍。
5.1 H.264编码核心参数:Profile、Level、Bitrate
这三个参数,说白了就是编码器的"三驾马车"。你配置得好,视频又清晰又流畅;配不好,要么画质糊成一片,要么码率飙得飞起。
5.1.1 Profile(档次)
Profile定义了编码器支持哪些工具集。H.264主要有三个档次:
| Profile | 特点 | 适用场景 |
|---|---|---|
| Baseline | 最基础,支持I/P帧,不支持B帧 | 视频通话、低延迟场景 |
| Main | 支持B帧,压缩率更高 | 标准视频播放 |
| High | 支持8x8变换,压缩率最高 | 高清视频、蓝光 |
我个人习惯在直播场景用Baseline Profile。为什么?因为B帧会引入延迟,而直播最怕的就是延迟。我曾经在一个项目中,为了追求压缩率用了Main Profile,结果推流延迟从1秒飙到了3秒,用户直接炸了。嗯,从那以后我就记住了:低延迟场景,老老实实用Baseline。
5.1.2 Level(级别)
Level限制了视频的分辨率、帧率和码率上限。比如Level 3.0支持的最大分辨率是720x576@30fps,码率上限10Mbps。Level 4.0则支持1920x1080@30fps,码率上限20Mbps。
关键点:Level不是越高越好。你想想看,如果手机只支持Level 3.0解码,你推个Level 4.0的流,对方直接黑屏。所以要根据目标设备来选。
5.1.3 Bitrate(码率)
码率决定了视频的清晰度。码率越高,画质越好,但文件也越大。这里有个常见的误区:很多人以为码率越高越好。其实不是的,码率超过一定阈值后,画质提升就微乎其微了。
我建议的码率配置参考:
- 480p:500-800 kbps
- 720p:1-2 Mbps
- 1080p:3-5 Mbps
当然,这取决于视频内容。如果是静态画面,码率可以低一些;如果是运动剧烈的画面(比如球赛),码率得往上调。
5.2 编码器配置与启动
好了,理论说完了,咱们直接上代码。Android平台上,我们通常用MediaCodec来做硬件编码。
5.2.1 创建编码器
MediaCodec encoder = MediaCodec.createEncoderByType("video/avc");
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
// 配置参数
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 关键帧间隔2秒
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
// 设置Profile和Level
format.setInteger("profile", MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
format.setInteger("level", MediaCodecInfo.CodecLevel.AVCLevel31);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
小技巧:KEY_I_FRAME_INTERVAL这个参数很关键。直播场景我一般设2秒,这样用户切换频道时能快速看到画面。但如果你做的是视频录制,可以设5秒甚至更长,压缩率会更好。
5.2.2 启动编码器
encoder.start();
// 获取输入Surface
Surface inputSurface = encoder.createInputSurface();
// 将Camera预览数据送到Surface
camera.setPreviewTexture(new SurfaceTexture(10));
camera.startPreview();
这里有个坑,我曾经踩过。createInputSurface()必须在start()之后调用,否则会报错。嗯,别问我怎么知道的,都是血泪史。
5.3 编码数据回调与封装
编码器启动后,我们需要不断从输出缓冲区取数据。这个过程,说白了就是个生产者-消费者模型。
5.3.1 获取编码数据
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
while (outputIndex >= 0) {
ByteBuffer outputBuffer = encoder.getOutputBuffer(outputIndex);
// 判断是否为关键帧
boolean isKeyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
// 处理编码数据
processEncodedData(outputBuffer, bufferInfo, isKeyFrame);
encoder.releaseOutputBuffer(outputIndex, false);
outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
}
5.3.2 封装成RTMP包
拿到H.264裸数据后,我们需要封装成RTMP能识别的格式。RTMP要求每个视频包前面加上4字节的长度前缀。
private void processEncodedData(ByteBuffer buffer, MediaCodec.BufferInfo info, boolean isKeyFrame) {
byte[] data = new byte[info.size];
buffer.position(info.offset);
buffer.get(data, 0, info.size);
if (isKeyFrame) {
// 关键帧前面需要加上SPS/PPS
byte[] sps = getSpsData();
byte[] pps = getPpsData();
sendRtmpPacket(sps);
sendRtmpPacket(pps);
}
// 发送视频数据
sendRtmpPacket(data);
}
注意:SPS和PPS是解码器需要的参数集,必须在关键帧之前发送。我见过不少新手在这里栽跟头,关键帧发了但没带SPS/PPS,结果解码器直接罢工。
5.4 知识体系总览
为了让大家更直观地理解整个编码流程,我画了一张图:
这张图把整个流程串起来了。你从左边开始,先配好参数,然后操作编码器,最后把数据封装发送。每一步都有坑,但只要你理解了原理,其实都不难。
好了,今天的实战内容就到这里。记住,编码这件事,理论要懂,代码要练,踩坑要记。下次遇到问题,你就能从容应对了。
公众号:蓝海资料掘金营,微信deep3321