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

当然,这只是参考。具体选多少,要看你的应用场景和网络条件。

知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以看到,视频从采集到显示,每一步都跟这些基础概念相关。

视频基础核心知识体系 视频基础 色彩空间与YUV 分辨率与帧率 视频编解码原理 码率控制 YUV420 / NV12 / NV21 RGB ↔ YUV 转换 色度采样 480p / 720p / 1080p 24fps / 30fps / 60fps 数据量计算 H.264 / H.265 / VP9 I帧 / P帧 / B帧 空间冗余 / 时间冗余 CBR / VBR / CRF 码率与画质权衡 自适应码率 理解这些基础,才能做好视频采集、编码、推流 每个知识点背后都有实际项目中的坑

好了,这一章的内容就到这里。色彩空间、分辨率帧率、编解码原理、码率控制,这四个概念是视频开发的基石。你把这些搞清楚了,后面学MediaCodec、Camera2、OpenGL渲染,都会轻松很多。

记住,做视频开发,不要只盯着API怎么调。多想想底层为什么这么设计。这样出了问题,你才能快速定位。


公众号:蓝海资料掘金营,微信deep3321