AVFrame的data数组数据结构详解
一、AVFrame结构概览
1.1 AVFrame定义(简化)
cpp
typedef struct AVFrame {
// 数据指针数组(核心存储)
uint8_t *data[AV_NUM_DATA_POINTERS]; // AV_NUM_DATA_POINTERS = 8
// 行大小(字节对齐后每行的字节数)
int linesize[AV_NUM_DATA_POINTERS];
// 图像/音频属性
int width, height; // 视频:宽高
int format; // 像素格式或采样格式
int nb_samples; // 音频:采样点数
int channels; // 音频:声道数
int64_t pts; // 显示时间戳
// ... 其他字段
} AVFrame;
二、data数组的布局(按媒体类型)
2.1 视频帧(YUV420P格式示例)
内存布局:
data[0] → Y分量(亮度)的起始地址
data[1] → U分量(Cb,蓝色色差)的起始地址
data[2] → V分量(Cr,红色色差)的起始地址
data[3..7] → NULL(未使用)
具体示例(352×288 YUV420P):
cpp
// 分配后的内存状态
frame->format = AV_PIX_FMT_YUV420P;
frame->width = 352;
frame->height = 288;
// 各分量大小
size_y = linesize[0] * height; // Y平面大小
size_u = linesize[1] * height/2; // U平面大小(1/4分辨率)
size_v = linesize[2] * height/2; // V平面大小(1/4分辨率)
// 实际数据布局
内存地址: data[0] → [Y00 Y01 ... Y(351,287)] // 101,376字节
data[1] → [U00 U01 ... U(175,143)] // 25,344字节
data[2] → [V00 V01 ... V(175,143)] // 25,344字节
2.2 音频帧(FLTP格式示例)
内存布局(浮点平面格式):
data[0] → 左声道数据起始地址(浮点数组)
data[1] → 右声道数据起始地址(浮点数组)
data[2..7] → NULL(未使用)
具体示例(立体声FLTP):
cpp
frame->format = AV_SAMPLE_FMT_FLTP;
frame->nb_samples = 1024;
frame->channels = 2;
frame->sample_rate = 44100;
// 内存布局
data[0] → [L0, L1, L2, ..., L1023] // 1024个float,左声道
data[1] → [R0, R1, R2, ..., R1023] // 1024个float,右声道
// 每个声道大小
size_per_channel = nb_samples * sizeof(float); // 4096字节
三、不同像素格式的data布局
3.1 YUV420P(最常见的格式)
cpp
// 分量分离存储(planar)
data[0] = Y平面 (width×height)
data[1] = U平面 (width/2 × height/2)
data[2] = V平面 (width/2 × height/2)
// 访问示例:读取像素(100,50)的YUV值
int y = data[0][y * linesize[0] + x]; // x=100, y=50
int u = data[1][(y/2) * linesize[1] + (x/2)]; // x=50, y=25
int v = data[2][(y/2) * linesize[2] + (x/2)]; // x=50, y=25
3.2 YUV422P
cpp
// 4:2:2采样,UV水平分辨率减半
data[0] = Y平面 (width×height)
data[1] = U平面 (width/2 × height) // 注意:高度不减半
data[2] = V平面 (width/2 × height)
// 访问UV分量
int u = data[1][y * linesize[1] + (x/2)];
3.3 YUV444P
cpp
// 4:4:4采样,UV全分辨率
data[0] = Y平面 (width×height)
data[1] = U平面 (width×height) // 全分辨率
data[2] = V平面 (width×height) // 全分辨率
// 访问UV分量
int u = data[1][y * linesize[1] + x]; // 坐标不减半
3.4 NV12(半平面YUV)
cpp
// Y分量平面存储,UV交错存储
data[0] = Y平面 (width×height)
data[1] = UV交错平面 (width×height/2)
// 访问示例:像素(100,50)
int y = data[0][y * linesize[0] + x];
// UV交错:每2字节为一组[U,V]
int uv_index = (y/2) * linesize[1] + (x/2)*2;
int u = data[1][uv_index]; // 字节0: U分量
int v = data[1][uv_index + 1]; // 字节1: V分量
3.5 RGB24(打包格式)
cpp
// RGB分量打包存储
data[0] = 所有像素的RGB数据
// 内存布局:[R0,G0,B0, R1,G1,B1, ...]
// linesize[0] = width * 3 + 填充字节
// 访问像素(x,y)
int pixel_index = y * linesize[0] + x * 3;
uint8_t r = data[0][pixel_index]; // 红色分量
uint8_t g = data[0][pixel_index + 1]; // 绿色分量
uint8_t b = data[0][pixel_index + 2]; // 蓝色分量
四、不同音频采样格式的data布局
4.1 AV_SAMPLE_FMT_FLTP(浮点平面)
cpp
// 每个声道单独存储为float数组
frame->format = AV_SAMPLE_FMT_FLTP;
frame->nb_samples = 1024;
frame->channels = 2;
// 访问左声道第i个采样
float left_sample = ((float*)data[0])[i];
// 访问右声道第i个采样
float right_sample = ((float*)data[1])[i];
4.2 AV_SAMPLE_FMT_S16(有符号16位打包)
cpp
// 所有声道交错打包存储
frame->format = AV_SAMPLE_FMT_S16;
frame->nb_samples = 1024;
frame->channels = 2;
// 数据布局:[L0,R0, L1,R1, L2,R2, ...]
// 访问第i个采样
int16_t* audio_data = (int16_t*)data[0];
int16_t left_sample = audio_data[i * 2]; // 左声道
int16_t right_sample = audio_data[i * 2 + 1]; // 右声道
4.3 AV_SAMPLE_FMT_S16P(有符号16位平面)
cpp
// 每个声道单独存储为int16_t数组
frame->format = AV_SAMPLE_FMT_S16P;
// 访问左声道第i个采样
int16_t left_sample = ((int16_t*)data[0])[i];
// 访问右声道第i个采样
int16_t right_sample = ((int16_t*)data[1])[i];
五、linesize的重要性
5.1 linesize与width的关系
cpp
// 对于352×288 YUV420P视频
frame->width = 352;
frame->height = 288;
// 实际的linesize可能为:
frame->linesize[0] = 352; // 或对齐到384、512等
frame->linesize[1] = 176; // U平面,可能对齐到192
frame->linesize[2] = 176; // V平面,可能对齐到192
// 重要:linesize ≥ width(考虑对齐)
5.2 为什么需要linesize?
cpp
// 错误访问(可能越界)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// ❌ 错误:假设每行就是width字节
uint8_t pixel = data[0][y * width + x];
}
}
// 正确访问(使用linesize)
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// ✅ 正确:考虑内存对齐
uint8_t pixel = data[0][y * linesize[0] + x];
}
}
六、实际内存布局示例
6.1 YUV420P帧的内存映像
cpp
// 假设352×288,linesize对齐到384
内存地址: 0x1000 [Y00 Y01 ... Y351 填充...填充] // 384×288 = 110,592字节
0x1B000 [U00 U01 ... U175 填充...填充] // 192×144 = 27,648字节
0x21800 [V00 V01 ... V175 填充...填充] // 192×144 = 27,648字节
// data指针指向
data[0] = 0x1000 // Y分量
data[1] = 0x1B000 // U分量
data[2] = 0x21800 // V分量
// linesize值
linesize[0] = 384 // 实际每行384字节(352有效+32填充)
linesize[1] = 192 // U平面每行192字节(176有效+16填充)
linesize[2] = 192 // V平面每行192字节(176有效+16填充)
6.2 音频FLTP帧的内存映像
cpp
// 立体声,1024采样点
内存地址: 0x2000 [L0 L1 L2 ... L1023] // 1024×4 = 4096字节
0x3000 [R0 R1 R2 ... R1023] // 1024×4 = 4096字节
// data指针指向
data[0] = 0x2000 // 左声道float数组
data[1] = 0x3000 // 右声道float数组
// linesize值(对于音频,linesize表示每个声道的数据大小)
linesize[0] = 4096 // 左声道数据大小
linesize[1] = 4096 // 右声道数据大小
七、data数组的初始化和管理
7.1 分配帧缓冲区
cpp
// 方法1:自动分配
AVFrame* frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P;
frame->width = 352;
frame->height = 288;
av_frame_get_buffer(frame, 32); // 32字节对齐
// 方法2:手动设置外部缓冲区
uint8_t* yuv_buffer = malloc(total_size);
frame->data[0] = yuv_buffer; // Y分量
frame->data[1] = yuv_buffer + y_size; // U分量
frame->data[2] = yuv_buffer + y_size + u_size; // V分量
frame->linesize[0] = width;
frame->linesize[1] = width/2;
// 需要设置引用计数避免提前释放
7.2 检查帧是否可写
cpp
// 重要:编码器可能持有帧的引用
if (!av_frame_is_writable(frame)) {
// 需要复制帧或等待引用释放
AVFrame* tmp = av_frame_clone(frame);
av_frame_unref(frame);
av_frame_move_ref(frame, tmp);
av_frame_free(&tmp);
}
八、常见操作示例
8.1 遍历视频帧所有像素
cpp
void process_yuv_frame(AVFrame* frame) {
int width = frame->width;
int height = frame->height;
// 处理Y分量
for (int y = 0; y < height; y++) {
uint8_t* y_line = frame->data[0] + y * frame->linesize[0];
for (int x = 0; x < width; x++) {
y_line[x] = process_pixel(y_line[x]);
}
}
// 处理UV分量(分辨率减半)
int uv_height = height / 2;
int uv_width = width / 2;
for (int y = 0; y < uv_height; y++) {
uint8_t* u_line = frame->data[1] + y * frame->linesize[1];
uint8_t* v_line = frame->data[2] + y * frame->linesize[2];
for (int x = 0; x < uv_width; x++) {
u_line[x] = process_chroma(u_line[x]);
v_line[x] = process_chroma(v_line[x]);
}
}
}
8.2 音频帧处理
cpp
void process_audio_frame(AVFrame* frame) {
int nb_samples = frame->nb_samples;
int channels = frame->channels;
if (frame->format == AV_SAMPLE_FMT_FLTP) {
// 平面格式:每个声道单独处理
for (int ch = 0; ch < channels; ch++) {
float* channel_data = (float*)frame->data[ch];
for (int i = 0; i < nb_samples; i++) {
channel_data[i] = process_sample(channel_data[i]);
}
}
} else if (frame->format == AV_SAMPLE_FMT_S16) {
// 打包格式:声道交错
int16_t* audio_data = (int16_t*)frame->data[0];
for (int i = 0; i < nb_samples; i++) {
for (int ch = 0; ch < channels; ch++) {
int index = i * channels + ch;
audio_data[index] = process_sample(audio_data[index]);
}
}
}
}
九、总结
AVFrame的data数组设计体现了FFmpeg的灵活性和高效性:
关键特点:
- 多平面存储:支持YUV/RGB的平面和打包格式
- 内存对齐:通过linesize支持硬件优化
- 格式无关:统一接口处理各种像素/采样格式
- 零拷贝:支持外部缓冲区,避免数据复制
使用原则:
- 永远使用linesize而非width进行内存访问
- 检查帧可写性后再修改数据
- 理解格式布局才能正确访问数据
- 利用FFmpeg工具函数 如
av_image_fill_arrays()