深耕ffmpeg系列之解锁音视频数据格式精准描述的 “隐藏技能”

298 阅读21分钟

引言

FFmpeg 作为强大的音视频处理工具,能应对形形色色的音视频格式。在音频方面,涉及采样率、采样位深、声道布局等参数;视频领域,则有诸如 rgba888、yuv420p、nv12 等不同的颜色空间和数据布局。那么,FFmpeg 究竟是怎样对这些格式的数据进行描述的?怎样计算存储它们所需的 buffer 大小?又是如何存放这些不同格式的数据的呢?

音频格式

采样率

采样率指的是在对模拟音频信号进行数字化采样时,每秒钟所采集的样本数量,单位是赫兹(Hz)。可以把它理解成在一段时间内对音频信号进行 “拍照” 的次数。采样率越高,单位时间内采集的样本就越多,数字化后的音频就越接近原始的模拟音频,声音的还原度也就越高。不过,这也会使数据量增大。

常见的采样率有:

  • 8000Hz:常用于电话通信,因电话语音频带较窄,此采样率足以满足基本语音通信需求。
  • 44100Hz:音频 CD 所采用的采样率,能覆盖人耳可听频率范围(20Hz - 20000Hz),可提供高质量的音频录制与播放。
  • 48000Hz:广泛应用于数字音频设备,像 DVD、数字电视以及专业音频制作领域。

ffmpeg AVFrame中sample_rate字段描述采样率。

int sample_rate;

采样格式

采样位深也叫采样精度,它代表每个音频样本所使用的比特数。比特数越多,能够表示的声音幅度的精度就越高,声音的动态范围也就越大。动态范围指的是音频信号中最强音与最弱音的强度差值。

常见的采样位深有:

  • 8 位:可以表示 2^8 = 256 个不同的声音幅度等级,精度较低,声音质量有限,多见于早期的音频设备。
  • 16 位:能够表示 2^16 = 65536 个不同的声音幅度等级,是目前较为常见的采样位深,能提供较好的声音质量,广泛应用于消费级音频产品。
  • 24 位和 32 位:主要用于专业音频制作领域,能表示的声音幅度等级更多,可捕捉到更细微的声音变化,适用于对声音质量要求极高的场景。

ffmpeg AVFrame中format字段描述采样位深,其具体的值由枚举AVSampleFormat中定义。 如: AV_SAMPLE_FMT_S16 每个采样点占用2个字节,不同声道交错存放 AV_SAMPLE_FMT_S32 每个采样点占用4个字节,不同声道交错存放 AV_SAMPLE_FMT_S16P 每个采样点占用2个字节,不同声道独立存放

int format;

声道布局

声道布局指的是音频录制或播放时所使用的声道数量以及这些声道的空间位置分布。不同的声道布局能营造出不同的声音空间感和立体感。

常见的声道布局有:

  • 单声道(Mono):只有一个声道,所有声音信号都通过这一个声道播放,声音没有明显的空间感,就像在一个点上发声,常用于广播、有声读物等场景。
  • 立体声(Stereo):包含左声道(Left)和右声道(Right)两个声道,通过两个声道声音的差异可以营造出一定的空间感和立体感,让听众感觉声音来自不同的方向,是音乐播放、电影等领域最常用的声道布局。
  • 5.1 声道:由五个全频声道(左声道、右声道、中置声道、左环绕声道、右环绕声道)和一个低频效果声道(.1 声道,通常用于重低音)组成。这种声道布局能够提供更加丰富的声音空间感和包围感,广泛应用于家庭影院系统。
  • 7.1 声道:在 5.1 声道的基础上增加了两个侧环绕声道,进一步增强了声音的包围感和空间感,常用于高端家庭影院和专业音频制作环境。

ffmpeg AVFrame中AVChannelLayout ch_layout字段描述。

 AVChannelLayout ch_layout;
 
 typedef struct AVChannelLayout {
    enum AVChannelOrder order;
    int nb_channels;
    union {
        uint64_t mask;
         AVChannelCustom *map;
    } u;
     void *opaque;
} AVChannelLayout;

enum AVChannelOrder {
    AV_CHANNEL_ORDER_UNSPEC,
    AV_CHANNEL_ORDER_NATIVE,
    AV_CHANNEL_ORDER_CUSTOM,
    AV_CHANNEL_ORDER_AMBISONIC,
}

order:指声道顺序,比如3个声道,可以是3 channel也可以是2.1 channel。ffmpeg中定义了四种order类型:

  • AV_CHANNEL_ORDER_UNSPEC: 此枚举值意味着仅指定了声道的数量,却没有提供关于声道排列顺序的更多信息。在这种情况下,无法确切知晓各个声道所代表的含义,像左声道、右声道等。当音频数据的声道顺序不明确或者在处理过程中尚未确定声道顺序时,就会使用这个值。
  • AV_CHANNEL_ORDER_NATIVE:它表示使用原生的声道顺序。这里的声道顺序和 AVChannel 枚举类型里定义的顺序一致。AVChannel 枚举应该是预先定义了各个声道(如左声道、右声道等)的标识符。如:AV_CHAN_FRONT_LEFT_OF_CENTER,AV_CHAN_FRONT_LEFT
  • AV_CHANNEL_ORDER_CUSTOM:该枚举值表明声道顺序不对应任何预定义的顺序,而是以显式映射的方式存储。这意味着需要额外的数据结构来明确每个声道的具体位置和含义。
  • AV_CHANNEL_ORDER_AMBISONIC:此枚举值表示音频采用球面谐波分解的方式来表示声场。每个声道对应一个展开分量,声道按照 ACN(Ambisonic Channel Number,Ambisonics 声道编号)顺序排列。
  1. 若已知声道索引 n,可依据公式 l = floor(sqrt(n)) 和 m = n - l * (l + 1) 计算出对应的球面谐波的阶数 l 和次数 m
  2. 若已知球面谐波的阶数 l 和次数 m,则可通过公式 n = l * (l + 1) + m 计算出对应的声道索引 n
  3. 并且,这里假设采用的归一化方式是 SN3D(Schmidt Semi - Normalization),这是 AmbiX 格式中定义的一种归一化方法。Ambisonics 是一种用于 3D 音频录制、处理和播放的技术,这种声道顺序能让音频在 3D 空间中呈现出更精确的声场效果。

mask:一个位掩码(bitmask),它用于表示音频声道布局中各个声道的存在与否。该字段主要在两种声道order下使用:AV_CHANNEL_ORDER_NATIVEAV_CHANNEL_ORDER_AMBISONIC。每个二进制位对应一个 AVChannel 枚举值。具体来说:

  • 如果 mask 中的第 n 位被设置为 1(即 mask & (1 << AV_CHAN_FOO) 非零),则表示 AV_CHAN_FOO 声道存在于当前的声道布局中。
  • 如果 mask 中的第 n 位被设置为 0(即 mask & (1 << AV_CHAN_FOO) 为零),则表示 AV_CHAN_FOO 声道不存在于当前的声道布局中。

数据存储实例

int main() {
    AVFrame *frame = av_frame_alloc();
    
    // 设置帧的参数
    frame->format = AV_SAMPLE_FMT_S16P;
    // channel_layout.order为AV_CHANNEL_ORDER_NATIVE
    // channel_layout.u.mask为AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT
    // channel_layout.nb_channels为2
    frame->channel_layout = av_get_default_channel_layout(2);
    frame->sample_rate = 44100;
    frame->nb_samples = 1024;

    // 分配帧的缓冲区
    // 由于是AV_SAMPLE_FMT_S16P这个P表示每个声道独立存放,
    // 因此frame->data[0]存放左声道,frame->data[1]存放右声道
    // frame->linesize[0]表示data[0]和data[1]的size
    // frame->linesize[0] = 44100 * 2 = 88200字节
    
    //如果是AV_SAMPLE_FMT_S16,左右声道的数据交错存放在frame->data[0]
    //此时frame->linesize[0] = 44100(采样率) * 2(采样位深) * 2(声道数) = 176400 (字节)
    av_frame_get_buffer(frame, 0);


    // 模拟写入音频数据
    for (int ch = 0; ch < frame->channel_layout.nb_channels; ch++) {
        // extended_data与data字段指向的是相同的内存区域
        int16_t *samples = (int16_t *)frame->extended_data[ch];
        for (int i = 0; i < frame->nb_samples; i++) {
            // 这里简单地用 i 作为示例数据
            samples[i] = (int16_t)i;
        }
    }


    // 释放帧
    av_frame_free(&frame);

    return 0;
}    

视频格式

分辨率

图像的分辨率为视频宽 * 视频高。在AVFrame中宽,高用下面的字段表示:

int width, height;

图像格式

AVFrame中使用已下字段表示图像格式,具体的值在enum AVPixelFormat中定义的。常见的图像格式有: AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12 AV_PIX_FMT_ARGB,

int format

下面来深入理解下两种颜色空间的表示:

  • RGB 颜色空间

    1. 定义:RGB 颜色空间基于红(Red)、绿(Green)、蓝(Blue)三种基本颜色,通过这三种颜色的不同强度组合,可以表示出几乎所有人类视觉可见的颜色。这三种颜色被称为三基色,是现代彩色显示和数字成像系统的基础。
    2. 原理:在 RGB 颜色模型中,每种颜色通道的强度通常用一个数值来表示,这个数值的范围取决于所使用的位数。例如,在常见的 8 位表示中,每个通道的取值范围是 0 - 255,其中 0 表示该颜色通道完全关闭(没有该颜色的成分),255 表示该颜色通道完全打开(该颜色的强度达到最大)。通过调整这三个通道的数值,可以混合出不同的颜色。例如,红色(255, 0, 0)、绿色(0, 255, 0)、蓝色(0, 0, 255)、白色(255, 255, 255)和黑色(0, 0, 0)。
    3. 应用场景:RGB 颜色空间广泛应用于各种需要直接显示颜色的设备和系统中,如计算机显示器、电视机、数码相机、扫描仪等。因为这些设备的显示原理通常是基于发光元件(如像素点)发出红、绿、蓝三种颜色的光来混合出各种颜色。
  • YUV 颜色空间

    1. 定义:YUV 颜色空间是一种将亮度信息(Y)和色度信息(U 和 V)分离的颜色表示方法。其中,Y 表示亮度(Luminance 或 Luma),它包含了图像的灰度信息;U 和 V 表示色度(Chrominance 或 Chroma),分别代表了蓝色和红色的色差信息。
    2. 原理:将亮度和色度分离的好处是可以在不损失太多视觉质量的前提下,对色度信息进行更高效的压缩。因为人眼对亮度的变化比色度的变化更为敏感,所以在传输和存储图像或视频时,可以降低色度信息的采样率,从而减少数据量。常见的 YUV 采样格式有 YUV4:4:4、YUV4:2:2 和 YUV4:2:0 等。例如,YUV4:2:0 表示在水平和垂直方向上,色度信息的采样率都是亮度信息的一半。
    3. 应用场景:YUV 颜色空间在视频编码、传输和存储领域有着广泛的应用,如电视广播、视频会议、视频监控等。许多视频编码标准(如 H.264、H.265 等)都采用了 YUV 颜色空间,因为它可以更有效地利用带宽和存储空间。此外,在一些图像处理和计算机视觉任务中,YUV 颜色空间也可以提供比 RGB 更有用的信息,例如在肤色检测、图像增强等方面。

FFMPEG如何描述图像格式

FFMPEG描述

FFMPEG中使用struct AVPixFmtDescriptor来描述每种图形格式,

typedef struct AVPixFmtDescriptor {
    //格式名称,如"yuv420p"
    const char *name;
    //分量个数,如: yuv的话就是3, RGBA就是4
    uint8_t nb_components; 
    
    //代表需要yuv格式中将亮度分量的宽度右移多少位才能得到色度分量的宽度, 
    // chroma_width = AV_CEIL_RSHIFT(luma_width, log2_chroma_w)
    // 如果是rgb格式,这个值为0
    uint8_t log2_chroma_w;

    //代表需要yuv格式中将亮度分量的高度右移多少位才能得到色度分量的高度,
    // chroma_height= AV_CEIL_RSHIFT(luma_height, log2_chroma_h)
    // 如果是rgb格式,这个值为0
    uint8_t log2_chroma_h;
    
    // frame标记为,如:AV_PIX_FMT_FLAG_RGB等这些值的组合
    uint64_t flags;
    //描述每一个像素点如何存放
    AVComponentDescriptor comp[4];

    const char *alias;
} AVPixFmtDescriptor;

下面再来看下AVComponentDescriptor的定义:

typedef struct AVComponentDescriptor {
    //平面的index
    int plane;

    // 表示在水平方向上,相邻两个像素的该颜色分量之间的元素间隔数量。
    int step;

    //该颜色分量距离像素起始位置的像素偏移量
    int offset;

    // 表示在获取颜色分量的实际值时,需要将存储的值向右移动的位数。
    // 这是因为在一些像素格式中,颜色分量的值可能会被打包存储,通过移位操作可以提取出正确的数值。
    // 一般是0
    int shift;

    // 颜色分量的深度
    int depth;
} AVComponentDescriptor;

YUV420P定义

是不是看了上面的解释,还是有点懵逼。下面来看个实际的例子,对比下:

YUV420P

AVPixFmtDescriptor yuv420p = {
    .name = "yuv420p",
    .nb_components = 3,
    .log2_chroma_w = 1,
    .log2_chroma_h = 1,
    .comp = {
        { 0, 1, 0, 0, 8 },   /* Y */
        { 1, 1, 0, 0, 8 },   /* U */
        { 2, 1, 0, 0, 8 },   /* V */
    },
    .flags = AV_PIX_FMT_FLAG_PLANAR,
};

上面是YUV420P的定义,看下每个字段的含义:

  • .name

    • 含义:像素格式的名称,用于标识该像素格式。
  • .nb_components

    • 含义:像素格式中包含的颜色分量数量。

    • 示例解释: .nb_components = 3,这意味着它们都包含三个颜色分量,分别是亮度分量 Y 以及两个色度分量 U 和 V。

  • .log2_chroma_w 和 .log2_chroma_h

    • 含义:.log2_chroma_w 表示将亮度分量(Y)的宽度右移多少位可以得到色度分量(U 和 V)的宽度。 .g2_chroma_h 表示将亮度分量(Y)的高度右移多少位可以得到色度分量(U 和 V)的高度。

    • 示例解释:log2_chroma_w = 1 和 .log2_chroma_h = 1,这表明在水平和垂直方向上,色度分量的分辨率都是亮度分量的一半,即 YUV420 采样格式。

  • .flags

    • 含义:用于描述像素格式的一些特性的标志位。

    • 示例解释: .flags = AV_PIX_FMT_FLAG_PLANAR 表示这两种像素格式都是平面格式,即 Y、U、V 分量分别存储在不同的平面中。

  • .comp 是一个 AVComponentDescriptor 结构体数组,用于描述每个颜色分量的详细信息。

.comp = {
    { 0, 1, 0, 0, 8 },        /* Y */
    { 1, 1, 0, 0, 8 },        /* U */
    { 2, 1, 0, 0, 8 },        /* V */
},
  • 第一个分量(Y)

    • { 0, 1, 0, 0, 8 } 分别对应 plane = 0step = 1offset = 0shift = 0depth = 8
    • plane = 0 表示 Y 分量存储在第 0 个平面。
    • step = 1 表示在水平方向上,相邻两个像素的 Y 分量之间间隔 1 个字节。
    • offset = 0 表示 Y 分量在像素数据中的起始偏移为 0 字节。
    • shift = 0 表示不需要对存储的 Y 分量值进行移位操作。
    • depth = 8 表示 Y 分量使用 8 位来表示。
  • 第二个分量(U)

    • plane = 1 表示 U 分量存储在第 1 个平面。
    • 其他参数含义与 Y 分量类似,同样是 step = 1offset = 0shift = 0depth = 8
  • 第三个分量(V)

    • plane = 2 表示 V 分量存储在第 2 个平面。
    • 其他参数含义与 Y 分量类似,同样是 step = 1offset = 0shift = 0depth = 8

YUV420P采样示意图

现在假设有一张分辨率为8 * 4的yuv420p的图像,我们来看看采样示意图:

图像尺寸和采样比例

  • 亮度分量(Y):由于亮度分量是全采样的,所以 Y 分量的分辨率与图像的原始分辨率相同,即 8 * 4,共有 8×4=32 个采样点。
  • 色度分量(U 和 V):根据 log2_chroma_w = 1 和 log2_chroma_h = 1,可知在水平和垂直方向上,色度分量的分辨率都是亮度分量的一半。即U和V分量的分辨率是(8 / 2) * (4 / 2) = 8个采样点。

亮度分量(Y)采样

亮度分量(Y)对每个像素点都进行采样,即覆盖图像的每一个像素。我们可以将 8 * 4 的图像看作一个二维矩阵,其中每个元素代表一个 Y 采样点。

Y 分量采样点布局(8 * 4): 
| Y00 Y01 Y02 Y03 Y04 Y05 Y06 Y07 |
| Y10 Y11 Y12 Y13 Y14 Y15 Y16 Y17 |
| Y20 Y21 Y22 Y23 Y24 Y25 Y26 Y27 |
| Y30 Y31 Y32 Y33 Y34 Y35 Y36 Y37 |

色度分量(U 和 V)采样

色度分量(U 和 V)在水平和垂直方向上进行下采样,即每隔一个像素进行一次采样。我们可以将 4 * 2 的 U 和 V 分量采样点看作是从 8 * 4 的 Y 分量采样点中按照一定规则选取的。

U 分量采样点布局(4 * 2):
| U00 U01 U02 U03 |
| U10 U11 U12 U13 |

V 分量采样点布局(4 * 2):
| V00 V01 V02 V03 |
| V10 V11 V12 V13 |

采样对应关系

U 和 V 分量的每个采样点对应 Y 分量中一个 2 * 2 的像素块。具体对应关系如下:

Y 分量与 U、V 分量的对应关系:
| Y00 Y01 | Y02 Y03 | Y04 Y05 | Y06 Y07 |
| Y10 Y11 | Y12 Y13 | Y14 Y15 | Y16 Y17 |  -->  | U00 U01 U02 U03 |
| Y20 Y21 | Y22 Y23 | Y24 Y25 | Y26 Y27 |
| Y30 Y31 | Y32 Y33 | Y34 Y35 | Y36 Y37 |  -->  | U10 U11 U12 U13 |

再回顾下里面字段的含义:
.comp = {
    // Y , .plane = 0 Y在frame->data[0], 
    //      .step = 1  Y分量每个采样点间隔1,
    //      .offset = 0 Y分量的起始偏移是0,
    //      .shift = 0 即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节
    { 0, 1, 0, 0, 8 },
    // U , .plane = 1 U在frame->data[1], 
    //      .step = 1 U分量每个采样点间隔1,
    //      .offset = 0 U分量的起始偏移是0,
    //      .shift = 0即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节*/
    { 1, 1, 0, 0, 8 },
    // V , .plane = 2 V在frame->data[2], 
    //      .step = 1  V分量每个采样点间隔1,
    //      .offset = 0 V分量的起始偏移是0,
    //      .shift = 0即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节*/
    { 2, 1, 0, 0, 8 }, 
},

NV12定义

nv12 NV12和YUV420P在采样方式上是一致的,均采用 4:2:0 的采样模式。所以log2_chroma_w和log2_chroma_h的值与YUV420P相同。 但存储结构不同,YUV420P中Y,U,V三个分量都存放在不同的平面,而NV12中Y分量存在一个平面,U,V分量交错存放在另一个平面。 所以可以看到,Y分量的.comp.plane = 0, U,Y分量的.comp.plane = 1。 由于U,V分量是交错存放,所以U,V分量的.comp.step = 2,而V分量的.comp.offset = 1

AVPixFmtDescriptor nv12 = {
    .name = "nv12",
    .nb_components = 3,
    .log2_chroma_w = 1,
    .log2_chroma_h = 1,
    .comp = {
            { 0, 1, 0, 0, 8 },        /* Y */
            { 1, 2, 0, 0, 8 },        /* U */
            { 1, 2, 1, 0, 8 },        /* V */
    },
    .flags = AV_PIX_FMT_FLAG_PLANAR,
}

NV12采样示意图

Y 分量(8×4)
| Y00 Y01 Y02 Y03 Y04 Y05 Y06 Y07 |
| Y10 Y11 Y12 Y13 Y14 Y15 Y16 Y17 |
| Y20 Y21 Y22 Y23 Y24 Y25 Y26 Y27 |
| Y30 Y31 Y32 Y33 Y34 Y35 Y36 Y37 |


UV 分量(4×2,U 和 V 交替存储)
| U00 V00 U01 V01 U02 V02 U03 V03 |
| U10 V10 U11 V11 U12 V12 U13 V13 |


Y 分量与 UV 分量的对应关系
| Y00 Y01 | Y02 Y03 | Y04 Y05 | Y06 Y07 |
| Y10 Y11 | Y12 Y13 | Y14 Y15 | Y16 Y17 |  -->  | U00 V00 U01 V01 U02 V02 U03 V03 |
| Y20 Y21 | Y22 Y23 | Y24 Y25 | Y26 Y27 |
| Y30 Y31 | Y32 Y33 | Y34 Y35 | Y36 Y37 |  -->  | U10 V10 U11 V11 U12 V12 U13 V13 |

再回顾下里面字段的含义:
.comp = {
    // Y , .plane = 0 Y在frame->data[0], 
    //      .step = 1 Y分量每个采样点间隔1,
    //      .offset = 0 Y分量的起始偏移是0,
    //      .shift = 0 即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节
    { 0, 1, 0, 0, 8 },
    // U , .plane = 1 U在frame->data[1], 
    //      .step = 2  U分量每个采样点间隔2,
    //      .offset = 0 U分量的起始偏移是0,
    //      .shift = 0 即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节*/
    { 1, 2, 0, 0, 8 },
    // V , .plane = 1 V在frame->data[1], 
    //      .step = 2  V分量每个采样点间隔2,
    //      .offset = 1 V分量的起始偏移是1,
    //      .shift = 0 即每个采样点的值不需要移位置
    //      .depth = 8 即每个采样点的采样位深是8bit,即一个字节*/
    { 1, 2, 1, 0, 8 }, 
},

av_image_get_linesize计算linesize

首先来看下源码:

int av_image_get_linesize(enum AVPixelFormat pix_fmt, int width, int plane)
{
    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(pix_fmt);
    int max_step     [4];       /* max pixel step for each plane */
    int max_step_comp[4];       /* the component for each plane which has the max pixel step */

    if (!desc || desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
        return AVERROR(EINVAL);
    //获取最大step,以及哪个平面是最大step
    av_image_fill_max_pixsteps(max_step, max_step_comp, desc);
    return image_get_linesize(width, plane, max_step[plane], max_step_comp[plane], desc);
}

// .comp = {
//            { 0, 1, 0, 0, 8 },        /* Y */
//            { 1, 2, 0, 0, 8 },        /* U */
//            { 1, 2, 1, 0, 8 },        /* V */
//    },
// 在这个例子中max_step就是2,max_pixstep_comps为plane 1
void av_image_fill_max_pixsteps(int max_pixsteps[4], int max_pixstep_comps[4],
                                const AVPixFmtDescriptor *pixdesc)
{
    int i;
    memset(max_pixsteps, 0, 4*sizeof(max_pixsteps[0]));
    if (max_pixstep_comps)
        memset(max_pixstep_comps, 0, 4*sizeof(max_pixstep_comps[0]));

    for (i = 0; i < 4; i++) {
        const AVComponentDescriptor *comp = &(pixdesc->comp[i]);
        if (comp->step > max_pixsteps[comp->plane]) {
            max_pixsteps[comp->plane] = comp->step;
            if (max_pixstep_comps)
                max_pixstep_comps[comp->plane] = i;
        }
    }
}

static inline
int image_get_linesize(int width, int plane,
                       int max_step, int max_step_comp,
                       const AVPixFmtDescriptor *desc)
{
    int s, shifted_w, linesize;
    //如果最大step为U分量或者V分量,s为desc->log2_chroma_w, NV12格式中s为1
    s = (max_step_comp == 1 || max_step_comp == 2) ? desc->log2_chroma_w : 0;
    //将width右移s位,如NV12,U,V分量是Y分量采样点的一半,所以是右移一位
    // (width + (1 << s) - 1)是为了向上取整
    shifted_w = ((width + (1 << s) - 1)) >> s;
    if (shifted_w && max_step > INT_MAX / shifted_w)
        return AVERROR(EINVAL);
    //如果max_step不为1,则说明此平面中有两个分量交错存放,所以要乘以max_step
    linesize = max_step * shifted_w;

    if (desc->flags & AV_PIX_FMT_FLAG_BITSTREAM)
        linesize = (linesize + 7) >> 3;
    return linesize;
}

linesize是指每个分量,每一行的字节数。以YUV420P为例,假设像素宽度是100,那么Y,U,V分量的line size分别是多少呢?

AVPixFmtDescriptor yuv420p = {
    .name = "yuv420p",
    .nb_components = 3,
    .log2_chroma_w = 1,
    .log2_chroma_h = 1,
    .comp = {
        { 0, 1, 0, 0, 8 },   /* Y */
        { 1, 1, 0, 0, 8 },   /* U */
        { 2, 1, 0, 0, 8 },   /* V */
    },
    .flags = AV_PIX_FMT_FLAG_PLANAR,
};

Y分量frame->linesize[0] = 100,因为YUV420P中Y分量是全采样,图像宽度是100,所以linesize就是100

U分量frame->linesize[1] = 100 >> 1 = 50,因为YUV420P中U分量采样点是Y的一半。

U分量frame->linesize[2] = 100 >> 1 = 50,因为YUV420P中V分量采样点是Y的一半。

那frame->data[]数组中各分量有多少字节呢?

Y分量buffer大小frame->data[0]的size = frame->linesize[0] * height

U分量buffer大小frame->data[1]的size = frame->linesize[0] * (height >> 1)

V分量buffer大小frame->data[2]的size = frame->linesize[0] * (height >> 1)