2. 多媒体基础
在 Android 多媒体开发中,无论是播放、录制、编辑还是推流,最终都要落到“数据如何表示”这一层。音频、视频、图像虽然感官形态不同,但在系统内部都被抽象为采样数据、色彩数据以及承载它们的帧与缓冲区。理解这三类表示方式,是后续理解 Buffer 复用、零拷贝与跨进程传输的前提。
一、音频:从连续波形到离散采样
声音本质是连续变化的压力波。Android 无法直接存储连续波形,只能按固定时间间隔“抓取”瞬时幅度,这个过程叫采样。采样后的数据再经过量化与编码,形成 PCM(Pulse Code Modulation,脉冲编码调制)数据。PCM 是最基础的音频表示,也是 AudioTrack、AudioRecord、MediaCodec 解码后最常见的中间格式。
描述一段 PCM 音频,需要四个核心参数:
- 采样率(Sample Rate):每秒采样次数,常见 44100 Hz、48000 Hz。采样率越高,可还原的频率上限越高。
- 位深(Bit Depth):每个采样点用多少位表示,常见 16 bit、24 bit、32 bit float。位深决定动态范围与量化噪声。
- 声道数(Channel Count):单声道 1、立体声 2、5.1 声道 6 等。
- 声道布局(Channel Mask):如左前、右前、中置、低频等,决定多声道数据在内存中的排列顺序。
由此可计算音频缓冲区大小:
bufferSize = 采样率 × 位深/8 × 声道数 × 时长(秒)
例:48000 × 2 × 2 × 0.02 = 3840 字节(20ms 立体声 16bit PCM)
Android 中常用 AudioFormat 描述这些参数,ENCODING_PCM_16BIT 表示 16 位有符号整数,ENCODING_PCM_FLOAT 表示 32 位浮点。PCM 数据在内存中通常是交错(Interleaved)排列:L0 R0 L1 R1 …,也有平面(Planar)排列:所有左声道在前、右声道在后。MediaCodec 输出的 AudioFormat 会明确标注是否为 planar,这直接影响后续 Buffer 的读写偏移计算。
二、图像与视频:像素、色彩空间与帧
图像是二维像素阵列,每个像素用若干分量描述颜色。最直观的是 RGB,但视频编码与显示更常用 YUV。Y 表示亮度,U/V 表示色度。由于人眼对亮度更敏感、对色度不敏感,YUV 可以对色度进行下采样,从而在几乎不损失主观画质的前提下大幅减少数据量。
常见 YUV 采样格式:
- YUV 4:4:4:每个像素都有独立 Y、U、V,无下采样,数据量最大。
- YUV 4:2:2:水平方向色度减半,每 2 个像素共享一组 U、V。
- YUV 4:2:0:水平和垂直方向色度都减半,每 4 个像素共享一组 U、V,是 Android 摄像头、H.264/H.265 解码最常用的格式。
Android 中常见的 4:2:0 布局有 I420(YUV420P)、NV12、NV21。I420 是三个平面:Y 平面、U 平面、V 平面;NV12/NV21 是 Y 平面加一个交错 UV 平面,区别仅在 U、V 先后顺序。Camera2 的 ImageFormat.YUV_420_888 是一种抽象格式,实际可能对应 I420、NV12 或 NV21,需要通过 Image.Plane 的 rowStride、pixelStride 判断真实布局。
视频则是按时间排列的图像序列。每一幅图像称为一帧(Frame),帧率(fps)表示每秒帧数。视频编码进一步利用帧间冗余,把帧分为 I 帧(关键帧,可独立解码)、P 帧(前向预测)、B 帧(双向预测)。解码器输出的是原始 YUV 帧,编码器输入的也是 YUV 帧,因此 YUV 帧缓冲区是视频链路中流转最频繁的对象。
三、帧与缓冲区:数据在内存中的载体
帧是逻辑概念,缓冲区(Buffer)是物理载体。一帧音频或视频数据,在内存中表现为一段连续或分块的内存区域。Android 多媒体框架中,缓冲区有多种形态:
| 缓冲区类型 | 所属层级 | 典型用途 | 是否可跨进程 |
|---|---|---|---|
| ByteBuffer | Java / NDK | AudioRecord、MediaCodec 输入输出 | 直接缓冲区可共享 |
| Image / Plane | Camera2 / ImageReader | 摄像头 YUV 帧读取 | 通过 Surface 可跨进程 |
| GraphicBuffer | Native / SurfaceFlinger | Surface 生产消费、GPU 纹理 | 可跨进程共享 |
| AHardwareBuffer | NDK / 系统 | 零拷贝跨进程、GPU/CPU 共享 | 可跨进程共享 |
理解缓冲区的关键在于三个属性:容量(capacity)、位置(position)、限制(limit)。读写时 position 移动,flip 后 limit 变为原 position,这些规则在音频 PCM 读写和 MediaCodec 输出处理中反复出现。若忽略 position/limit,极易出现“读不到数据”或“数据错位”的问题。
四、典型场景与常见误区
以“摄像头预览并编码推流”为例,数据链路为:Camera 采集 → ImageReader 得到 YUV_420_888 → 转换为编码器可接受的 NV12 → MediaCodec 编码 → 输出 H.264 码流。若在转换环节忽略 rowStride,把带 padding 的 Y 平面直接当紧凑平面处理,画面会出现斜纹或错位。正确做法是按 plane.getRowStride() 逐行拷贝,而不是整块 ByteBuffer 直接复制。
另一个高频误区是音频缓冲区大小设置。AudioTrack 的 getMinBufferSize() 返回的是最小可用值,若直接使用,在低端设备上容易 underrun 产生爆音。通常需要乘以 2~4 倍,并结合 WRITE_BLOCKING 或 WRITE_NON_BLOCKING 模式控制写入节奏。
掌握上述表示方式后,再去看 MediaCodec 的输入输出 Buffer、ImageReader 的 Plane、Surface 的 GraphicBuffer,就能明白它们并非孤立 API,而是同一套“帧—缓冲区—内存布局”模型在不同层级的具体实现。后续讨论零拷贝时,核心问题正是:能否让同一块缓冲区在采集、编码、显示之间直接传递,而不发生多余的内存拷贝。