剖析AVFrame

2023-05-16

AVFrame是FFmpeg中非常重要的数据结构,其封装了解码后的媒体数据。在FFmpeg之中,有几个比较重要的音视频概念:

pixel_format:

表示像素格式,图像像素在内存中的排列格式。一种像素格式包含有:色彩空间、采样方式、存储模式、位深等信息;

bit_depth:

表示位深,指每个分量(Y、U、V、R、G、B 等)单个采样点所占的位宽度;

plane:

表示存储图像中一个或多个分量的一片内存区域:

当处于planar 存储模式中,至少有一个分量占用单独的一个 plane,planar 存储模式至少有两个 plane,具体到 yuv420p 格式有 Y、U、V 三个 plane,nv12 格式有 Y、UV 两个 plane,gbrap 格式有 G、B、R、A 四个 plane。

当处于packed 存储模式中,因为所有分量的像素是交织存放的,所以 packed 存储模式只有一个 plane;

slice:

表示图像中一片连续的行,必须是连续的,顺序由顶部到底部或由底部到顶部;

stride/pitch:

一行图像中某个分量(如亮度分量或色度分量)所占的字节数, 也就是一个 plane 中一行数据的宽度。有对齐要求,计算公式如下:

stride = width * 分量数 * 单样本位宽度 / 水平子采样因子 / 8

其中,图像宽度(width)表示图像宽度是多少个像素,分量数指当前 plane 包含多少个分量(如 rgb24 格式一个 plane 有 R、G、B 三个分量),单位本位宽度指某分量的一个样本在考虑对齐后在内存中占用的实际位数(例如位深 8 占 8 位宽,位深 10 实际占 16 位宽,对齐值与平台相关),水平子采样因子指在水平方向上每多少个像素采样出一个色度样本(亮度样本不进行下采样,所以采样因子总是 1)

AVFrame

AVFrame 中存储的是解码后的原始数据(raw)。在解码中,AVFrame 是解码器的输出;在编码中,AVFrame 是编码器的输入。

必须使用av_frame_alloc进行初始化,该函数仅会对AVFrame进行初始化而不会分配任何缓存空间。相反,需要通过av_frame_free进行释放销毁。大部分实际使用中,AVFrame在同一属性下只会初始化一次,然后不断变更缓存内容,保存不同内容。av_frame_unref用来释放内部保留的所有引用,并使其于再次被引用前重置为“干净”状态。


 _______              ______________
|       |            |              |
| input |  demuxer   | encoded data |   decoder
| file  | ---------> | packets      | -----+
|_______|            |______________|      |
                                           v
                                       _________
                                      |         |
                                      | decoded |
                                      | frames  |
                                      |_________|
 ________             ______________       |
|        |           |              |      |
| output | <-------- | encoded data | <----+
| file   |   muxer   | packets      |   encoder
|________|           |______________|
      

剖析重要成员

data、linesize、extended_data

其中,data 存储原始帧数据,未编码的原始图像或音频数据,作为解码器的输出或编码器的输入。data是一个指针数组,元素指向视频图像的某一plane或音频中某声道的某一plane。无论是视频还是音频,都有 PacketPlanar 两种存储方式。

typedef struct AVFrame {
    /**
     * 存储原始帧数据,未编码的原始图像或音频数据,作为解码器的输出或编码器的输入
     * data是一个指针数组,元素指向视频图像的某一plane或音频中某声道的某一plane
     * 视频:
     *   Packet: Y,U,V交织存储在一个plane中,如YUVYUV...data[0]指向这个plane
     *   Planar: Y,U,V存储在三个plane中,data[0]指向Y-plane,data[1]指向U-plane,data[2]指向V-plane
     * 音频:
     *   Packet: L,R交织存储在一个plane中,形如LRLRLR...data[0]指向这个plane
     *   Planar: L,R存储在两个plane中,data[0]指向L-plane,data[1]指向R-plane
     */
    uint8_t *data[AV_NUM_DATA_POINTERS];
    /**
     * linesize是一个数组
     * 视频:
     *   每个元素是一个图像plane中一行图像的大小(字节数),注意有对齐要求linesize对应stride值
     *   packed: 只有一个plane,linesize[0]表示一行图像所占的存储空间大小     
     *   planar: 有多个plane,每个plane的linesize[i]表示一行图像在当前plane中所占的存储空间大小
     * 音频:
     *   每个元素是一个音频plane的大小(字节数)
     *   packed: 多声道音频只有一个plane,linesize[0]表示所有数据的存储空间大小
     *   planar: 有多个plane,但只使用一个linesize[0],每个plane大小都是linesize[0]
     * 事实上,linesize可能因性能考虑而填充一些额外的数据,故可能比实际对应的音视频数据要大
     */
    int linesize[AV_NUM_DATA_POINTERS];
    /**
     * 在一个正常的AVFrame中,data与extended_data通常都会被设置
     * 但是对于一个plannar且有多个通道且data无法装下所有通道的数据的时候,extended_data必须被使用,用来存储多出来的通道的数据的指针
     */
    uint8_t **extended_data;
	// ...
} AVFrame;

怎么应用呢?

当我们读取数据时,按width与height来读取吗?如下面:

fwrite(ofrm->data[0], 1, ofrm->width * ofrm->height, outfile)

显然是错误的,因为这样的写法,没有考虑到“字节对齐”的问题,比如宽是1280,但实际上该图片的切片宽可能是1200,只不过在内存区的总大小是1280,正确的写法应该是:

// 读取方式
// |___________|___|___________|___|___________|___| : 内存区域
// |___________| : linesize
// |_______________| : width
// Y分量
for (int j = 0; j < ofrm->height; j++) {
    fwrite(ofrm->data[0]+j*ofrm->linesize[0], 1, ofrm->width, outfile);
}
// U分量
for (int j = 0; j < ofrm->height / 2; j++) {
    fwrite(ofrm->data[1]+j*ofrm->linesize[1], 1, ofrm->width/2, outfile);
}
// V分量
for (int j = 0; j < ofrm->height / 2; j++) {
    fwrite(ofrm->data[2]+j*ofrm->linesize[2], 1, ofrm->width/2, outfile);
}

那么,我们如何判断这个数据是 packed 还是 planar 的呢?答案是通过ofrm->format来判断,对音频而言类型是 AVSampleFormat,对视频而言类型是 AVPixelFormat

width、height、sample_aspect_ratio

视频的宽与高,以及宽高比

typedef struct AVFrame {
    // 宽与高
    int width, height;
    // 视频帧的宽高比
    AVRational sample_aspect_ratio;
    // ...
}

key_frame、pict_type

关键帧标识以及帧类型,都是比较好理解的。

typedef struct AVFrame {
    // ...
    // 是否关键帧? 当值==1时,表示该视频帧为关键帧
    int key_frame;
    // 视频帧类型:I,B,P....
    enum AVPictureType pict_type;
    // ...
}

nb_samples、sample_rate、channels

音频声道采样数

typedef struct AVFrame {
    // ...
    // 音频每个声道内帧包含采样数
    int nb_samples;
    // 音频采样率
    int sample_rate;
    // 音频声道数
    int channels;
}

channel_layout

typedef struct AVFrame {
    // ...
    // 音频声道布局,每bit代表一个特定的声道
    uint64_t channel_layout;
}

// Layout
#define AV_CH_FRONT_LEFT        0x00000001
#define AV_CH_FRONT_RIGHT       0x00000002
#define AV_CH_FRONT_CENTER      0x00000004
#define AV_CH_LOW_FREQUENCY     0x00000008
// ...
#define AV_CH_LAYOUT_MONO       (AV_CH_FRONT_CENTER)
#define AV_CH_LAYOUT_STEREO     (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
#define AV_CH_LAYOUT_2POINT1    (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)

format

帧格式,如果是未知格式或未设置,则值为-1;

AVSampleFormat 名称里带P的表示 Packet 内存模式,而不带P的,则表示 Planar 内存模式;

typedef struct AVFrame {
    /** 
     * 对于视频帧,此值对应于: enum AVPixelFormat
     * 对于音频帧,此值对应于: enum AVSampleFormat
     */
    int format;
	// ...
}

enum AVPixelFormat {
    AV_PIX_FMT_NONE = -1,
    AV_PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp
    AV_PIX_FMT_YUYV422,   ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
    AV_PIX_FMT_RGB24,     ///< packed RGB 8:8:8, 24bpp, RGBRGB...
    AV_PIX_FMT_BGR24,     ///< packed RGB 8:8:8, 24bpp, BGRBGR...
    // ...
}

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8,          ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16,         ///< signed 16 bits
    AV_SAMPLE_FMT_S32,         ///< signed 32 bits
    AV_SAMPLE_FMT_FLT,         ///< float
    AV_SAMPLE_FMT_DBL,         ///< double

    AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar
    AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar
    AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar
    AV_SAMPLE_FMT_FLTP,        ///< float, planar
    AV_SAMPLE_FMT_DBLP,        ///< double, planar
    AV_SAMPLE_FMT_S64,         ///< signed 64 bits
    AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar

    AV_SAMPLE_FMT_NB
};

pts、pkt_dts

typedef struct AVFrame {
    // 帧显示时间戳,单位是:time_base
    int64_t pts;
    // 此frame对应的packet中的解码时间戳,是从对应packet中拷贝DTS得到此值
    // 如果对应的packet中只有dts而未设置pts,则此值也是此frame的pts
	int64_t pkt_dts;
}

buf、extended_buf

对于视频来说,buf[] 包含所有 AVBufferRef 指针。对于具有多于 AV_NUM_DATA_POINTERS 个声道的 planar 音频来说,可能 buf[] 存不下所有的 AVBbufferRef 指针,多出的 AVBufferRef 指针存储在 extended_buf 数组中。

typedef struct AVFrame {
    /**
     * 此帧的数据可以由AVBufferRef管理,AVBufferRef提供AVBuffer引用机制,
     * 这里涉及到缓冲区引用计数概念....
     * AVBuffer: 是FFmpeg中很常用的一种缓冲区,缓冲区使用“引用计数”机制
     * AVBufferRef: 则对AVBuffer缓冲区提供了一层封装,作引用计数处理,实现安全机制
     * 用户不应直接访问AVBuffer,应通过AVBufferRef来访问AVBuffer,以保证安全。
     */
    AVBufferRef *buf[AV_NUM_DATA_POINTERS];
    // 扩展缓冲区
    AVBufferRef **extended_buf;
	// ...
}

pkt_pos、pkt_duration、pkt_size

typedef struct AVFrame {
    // ...
    // 记录最后一个扔进解码器的packet在输入文件中的位置偏移量
    int pkt_size;
	// 对应packet的时长,单位是AVStream->time_base
    int64_t pkt_duration;
    // 对应packet的大小
    int pkt_size;
}

剖析重要函数接口

av_frame_alloc()、av_frame_free()

构造一个 frame,对象各成员被设为默认值,此函数只分配 AVFrame 对象本身,而不分配 AVFrame 中的数据缓冲区。

/**
 * 构造AVFrame,默认配置,不分配缓冲区
 */
AVFrame *av_frame_alloc(void);
/**
 * 释放AVFrame,并且也会释放所有缓冲区,例如extended_data
 * 如果正被引用计数,则解除引用
 */
void av_frame_free(AVFrame **frame);

av_frame_ref()、av_frame_unref()

为 src 中的数据建立一个新的引用。将 src 中帧的各属性拷到 dst 中,并且为 src 中每个 AVBufferRef 创建一个新的引用。如果 src 未使用引用计数,则 dst 中会分配新的数据缓冲区,将将 src 中缓冲区的数据拷贝到 dst 中的缓冲区。

/**
 * 新的AVFrame从源AVFrame对象中引用,引用计数+1
 */
int av_frame_ref(AVFrame *dst, const AVFrame *src);
/**
 * 解除一个引用,并让引用计数-1
 */
void av_frame_unref(AVFrame *frame);

av_frame_clone()

创建一个新的 frame,新的 frame 和 src 使用同一数据缓冲区,缓冲区管理使用引用计数机制。函数相当于 av_frame_alloc() + av_frame_ref()。

/**
 * 创建新的缓冲区并添加引用
 */
AVFrame *av_frame_clone(const AVFrame *src);

av_frame_get_buffer()

为音频或视频数据分配新的缓冲区。调用本函数前,帧中的如下成员必须先设置好:

  • format:视频像素格式或音频采样格式
  • width、height:视频画面和宽和高
  • nb_samples、channel_layout:音频单个声道中的采样点数目和声道布局

本函数会填充 AVFrame.dataAVFrame.linesizeAVFrame.buf 数组,如果有需要,还会分配和填充 AVFrame.extended_dataAVFrame.extended_buf。对于 planar 格式,会为每个 plane 分配一个缓冲区。

// 根据AVFrame属性分配缓冲区
int av_frame_get_buffer(AVFrame *frame, int align);

av_frame_copy

将 src 中的帧数据拷贝到 dst 中。本函数并不会有任何分配缓冲区的动作,调用此函数前 dst 必须已经使用了和 src 同样的参数完成了初始化。

注意其和 av_frame_clone 的区别。本函数只拷贝帧中的数据缓冲区的内容(data/extended_data 数组中的内容),而不涉及帧中任何其他的属性,类似于 memcpy

// 拷贝缓存区
int av_frame_copy(AVFrame *dst, const AVFrame *src);

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

剖析AVFrame 的相关文章

随机推荐

  • Day_01_服务器硬件常识与redhat环境基础配置

    服务器的应用场景 常见的三种文件共享服务 xff1a SMB FTP CMS 数据库 xff1a 管理和使用 xff0c 增删改查 xff0c 授权 xff0c 改授权 邮件 xff1a 正式的沟通交流都是以邮件通知为主 web serve
  • Java基本数据类型

    四大类 1 整型 byte short int long 2 浮点型 float double 3 字符型 char 4 布尔型 boolean
  • 自动拆装箱

    自动装箱就是Java自动将原始类型值转换成对应的对象 xff0c 比如将int的变量转换成Integer对象 xff0c 这个过程叫做装箱 xff0c 反之将Integer对象转换成int类型值 xff0c 这个过程叫做拆箱 因为这里的装箱
  • string为会么不可变,String、StringBuilder、StringBuffer的区别

    String 类中使用 final 关键字字符数组保存字符串 xff0c 所以 String 对象是不可变的 而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类 xff0c
  • JAVA IO流

    IO常用类 文件流 xff1a FileInputStream FileOutputStream xff0c FileReader FileWriter 这四个类是专门操作文件流的 xff0c 用法高度相似 xff0c 区别在于前面两个是操
  • JAVA反射

    JAVA反射机制是在运行状态中 xff0c 对于任意一个类 xff0c 都能够知道这个类的所有属性和方法 xff1b 对于任意一个对象 xff0c 都能够调用它的任意一个方法和属性 xff1b 这种动态获取的信息以及动态调用对象的方法的功能
  • java 哪些源码需要细看

    String Integer Long Enum Big ThreadLocal CloseLoader ArrayList amp LinkedLis Map HashMap Set
  • 算法中时间复杂度概括——o(1)、o(n)、o(logn)、o(nlogn)

    O后面的括号中作为一个函数 xff0c 指明某个算法的耗时 耗空间与数据增长量之间的关系 其中的n代表输入数据的量 比如时间复杂度为O n xff0c 就代表数据量增大几倍 xff0c 耗时也增大几倍 比如常见的遍历算法 再比如时间复杂度O
  • 怎么禁止/开启Ubuntu自动更新升级

    当你打开Ubuntu系统时经常会弹出软件更新升级提示框 xff0c 因为Ubuntu包括上面装的很多软件也都是开源系统 xff0c 更新升级是很频繁的 xff0c 对于经常弹出的更新提示无非是两种应对措施 xff0c 要么安装 xff0c
  • 六大设计模式

    单一职责 开闭原则 李氏替换原则 LSP 门面的实现 依赖倒转原则 DIP 服务指向契约 契约绑定实现 接口隔离原则 ISP 接口对应一种角色 最少知道原则 类之间的弱耦合 需要反复度量
  • centos安装jdk

    1 下载自己系统对应版本 2 到该文件所在目录执行命令 rpm ivh jdk 8u221 linux x64 rpm 3 默认安装在 usr java jdk1 8 0 221 amd64目录下 4 环境变量配置 xff1a cd etc
  • ESC上搭建spring boot

    一 打包项目 a 单击IDEA右上角Maven b 依次双击 demo gt Lifecycle gt package xff0c 开始打包 执行结果如下 xff0c 图中标记位置为打包后jar包的路径 二 运行ECS上的Java项目 执行
  • win10 安装配置mysql8

    1 下载 https tomcat apache org 选择自己需要的版本 2解压 3配置环境变量 略 4配置my ini 在 MYSQL HOME 下新建my int文件 xff0c 内容如下 span class token punc
  • idea调用javap

    idea 配置javap 具体参数设置如下 program span class token variable JDKPath span span class token punctuation span bin span class to
  • Rust Web(一)—— 自建TCP Server

    前段时间小小学习了一下Rust的基础内容 xff0c 出于学习Web开发的需求 xff0c 也为巩固学过的Rust基础 xff0c 就尝试记录一下自己学习 Rust Web 的点滴 xff1b 实现环境 OS Ubuntu 14 0 IDE
  • ajax传递数组怎么传?ajax数组传递

    在我们平时的开发中 xff0c 经常会需要用到ajax xff0c 关于ajax是什么 xff0c 又该如何传递参数 xff0c 相信通过上几篇文章你们已经有所了解 但是 xff0c ajax中要如何传递数组你们又知道吗 xff1f 今天我
  • linux安装node和达梦数据库8

    PS 本次测试只是为了项目需要 xff0c 但是在部署和启动程序的时候发生了一系列的报错 xff0c 由此记录下来为日后作参考 安装达梦数据库 1 达梦数据库 DM8 简介 达梦数据库管理系统是武汉达梦公司推出的具有完全自主知识产权的高性能
  • pyqt5+mysql+多线程爬虫实现 python 携程机票爬虫 数据可视化

    基本目录 数据来源与获取方法数据来源网页分析 实现效果完整代码与说明文档 数据来源与获取方法 数据来源 携程机票查询https flights ctrip com online channel domestic 网页分析 我们的目的是要爬取
  • debian9.8添加iso为本地源

    1 临时添加 使用mount临时挂载 注意需要在root权限下操作 一 将系统镜像文件复制到电脑任意路径下 xff0c 我这里复制到 home路径下 二 自己创建一个挂载目录 xff0c 我创建的是 mnt cdrom目录 xff0c 命令
  • 剖析AVFrame

    AVFrame是FFmpeg中非常重要的数据结构 xff0c 其封装了解码后的媒体数据 在FFmpeg之中 xff0c 有几个比较重要的音视频概念 xff1a pixel format xff1a 表示像素格式 xff0c 图像像素在内存中