3. 视频基础:色彩空间与YUV、分辨率与帧率、视频编解码原理、码率控制
各位同学,今天我们聊聊视频的底层基础。说实话,很多做Android应用开发的朋友,一碰到视频就头疼。为什么?因为视频涉及的概念太杂了。色彩空间、YUV、分辨率、帧率、编解码、码率……每个词背后都有一堆坑。
我个人习惯是,先把这些基础概念吃透。不然你后面做视频采集、编码、推流,出了问题都不知道怎么查。好,我们一个一个来。
3.1 色彩空间与YUV
先问大家一个问题:你在屏幕上看到的彩色图像,计算机是怎么存储的?
最常见的答案是RGB。每个像素用红、绿、蓝三个分量表示。这没错。但视频领域,几乎不用RGB。为什么?
因为人眼对亮度的敏感度远高于对颜色的敏感度。RGB三个分量都携带亮度信息,如果直接存储RGB,会浪费大量带宽。你想想看,一个1920x1080的画面,每个像素3个字节,一帧就是6MB多。30帧每秒,带宽接近200MB/s。这谁受得了?
所以视频领域用YUV色彩空间。Y代表亮度(Luma),UV代表色度(Chroma)。
核心思想:人眼对亮度敏感,对颜色不敏感。所以我们可以把亮度和颜色分开,颜色信息可以压缩。
YUV有很多变种。最常见的是YUV420。什么意思?就是每4个像素共享一组UV值。说白了,亮度信息每个像素都保留,颜色信息4个像素才存一份。这样数据量直接减半。
我在项目中遇到过一个问题:用Camera2采集预览数据,默认格式是YUV420_888。但很多开发者不知道,这个格式在不同设备上实现有差异。有的设备是YV12,有的是NV21,有的是NV12。如果你写死了格式,换个设备就崩了。
避坑指南:我曾经在某个项目里,直接写死了NV21的解析逻辑。结果在华为某款手机上,采集出来的画面颜色完全不对。后来查了半天,发现那台设备输出的是NV12。Y和UV的排列顺序不一样。从那以后,我每次都会先查一下设备的输出格式。
YUV和RGB的转换公式,大家了解一下就行。实际开发中,Android提供了ColorMatrix和RenderScript来做转换。别自己手写,容易出错。
// YUV420 转 RGB 的简化逻辑
// 注意:这里只展示核心思路,实际代码要处理 stride 和行对齐
public int yuvToRgb(byte[] yuv, int width, int height, int x, int y) {
int yIndex = y * width + x;
int uvIndex = (y / 2) * (width / 2) * 2 + (x / 2) * 2;
int Y = yuv[yIndex] & 0xFF;
int U = yuv[uvIndex] & 0xFF;
int V = yuv[uvIndex + 1] & 0xFF;
// 转换公式(简化版)
int R = (int)(Y + 1.402 * (V - 128));
int G = (int)(Y - 0.344 * (U - 128) - 0.714 * (V - 128));
int B = (int)(Y + 1.772 * (U - 128));
return 0xFF000000 | (clamp(R) << 16) | (clamp(G) << 8) | clamp(B);
}
3.2 分辨率与帧率
这两个概念大家应该不陌生。分辨率就是图像的宽高像素数,帧率就是每秒显示多少帧。
但我要说的是,这两个参数不是随便选的。它们直接决定了视频的清晰度和流畅度,也决定了编码的复杂度和码率。
| 分辨率 | 常见用途 | 单帧数据量(YUV420) |
|---|---|---|
| 480p (854x480) | 低码率直播、监控 | 约 614 KB |
| 720p (1280x720) | 主流视频通话 | 约 1.38 MB |
| 1080p (1920x1080) | 高清视频、直播 | 约 3.11 MB |
| 4K (3840x2160) | 超高清内容 | 约 12.44 MB |
帧率方面,常见的就几种:24fps(电影)、30fps(电视/直播)、60fps(游戏/运动画面)。
我个人建议,做Android视频采集时,不要盲目追求高帧率。为什么?因为编码器处理不过来。我曾经在一个项目里,把帧率设到60fps,结果手机发热严重,编码器丢帧,实际输出只有30fps。白白浪费了功耗。
经验之谈:如果你做的是视频通话类应用,30fps完全够用。如果是游戏录屏,可以考虑60fps。但一定要测试目标设备的编码能力。
3.3 视频编解码原理
视频编解码,说白了就是压缩和解压缩。为什么要压缩?因为原始视频数据量太大了。一段10分钟的1080p视频,30fps,YUV420格式,原始数据量是:
1920 x 1080 x 1.5 x 30 x 600 ≈ 55.9 GB
这谁受得了?所以必须压缩。
视频压缩的核心思想有两个:
- 空间冗余:同一帧内,相邻像素的颜色往往很接近。可以用变换编码(DCT)来压缩。
- 时间冗余:相邻帧之间,画面变化很小。可以用运动估计和运动补偿来压缩。
常见的视频编码标准有H.264、H.265、VP9、AV1。Android平台上,最常用的是H.264。兼容性好,几乎所有设备都支持硬件编码。
H.264编码器会生成三种帧:
- I帧(关键帧):完整的一帧图像。解码时不需要依赖其他帧。数据量大。
- P帧(预测帧):只记录与前一帧的差异。数据量小。
- B帧(双向预测帧):记录与前后帧的差异。压缩率最高,但编码复杂度高。
我建议,在实时通信场景下,尽量少用B帧。因为B帧需要参考后面的帧,会增加延迟。我曾经在WebRTC项目里,默认配置用了B帧,结果端到端延迟多了几十毫秒。后来关掉B帧,延迟就降下来了。
关键点:I帧间隔(GOP大小)直接影响视频的随机访问能力和码率波动。GOP越大,压缩率越高,但遇到丢包时恢复越慢。一般实时通信场景,GOP设为1-2秒。
3.4 码率控制
码率,就是视频每秒的数据量。单位是bps(bits per second)。码率控制,就是决定编码器怎么分配这些比特。
常见的码率控制模式有三种:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| CBR(恒定码率) | 码率始终保持在目标值附近 | 直播、视频通话(带宽稳定) |
| VBR(可变码率) | 码率根据画面复杂度动态变化 | 本地录制、点播(画质优先) |
| CRF(恒定质量) | 保持画面质量恒定,码率自动调整 | 本地转码、后期处理 |
在Android平台上,MediaCodec支持通过BitrateMode来设置码率控制模式。
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
// 设置码率控制模式
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
// 或者
format.setInteger(MediaFormat.KEY_BITRATE_MODE,
MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
这里有个坑。很多开发者以为设置了CBR,码率就真的恒定不变了。其实不是。编码器内部的码率控制算法,只能尽量接近目标值。遇到快速运动的画面,码率还是会飙升。我曾经在直播项目里,设置CBR 2Mbps,结果运动场景下码率飙到了3.5Mbps,导致推流卡顿。
避坑指南:如果你对码率有严格要求(比如直播推流到CDN),建议在编码器外面再加一层码率控制。比如用自适应码率算法,根据网络状况动态调整编码参数。不要完全依赖编码器内部的码率控制。
码率的选择,跟分辨率和帧率有关。一般经验值:
- 480p @ 30fps:500-800 Kbps
- 720p @ 30fps:1-2 Mbps
- 1080p @ 30fps:3-5 Mbps
- 4K @ 30fps:15-25 Mbps
当然,这只是参考。具体选多少,要看你的应用场景和网络条件。
知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以看到,视频从采集到显示,每一步都跟这些基础概念相关。
好了,这一章的内容就到这里。色彩空间、分辨率帧率、编解码原理、码率控制,这四个概念是视频开发的基石。你把这些搞清楚了,后面学MediaCodec、Camera2、OpenGL渲染,都会轻松很多。
记住,做视频开发,不要只盯着API怎么调。多想想底层为什么这么设计。这样出了问题,你才能快速定位。