深耕ffmpeg系列之AVFrame

AVFrame的作用

FFmpeg 中,AVFrame 是一个非常核心的数据结构,主要用于存储原始音频或视频数据的帧(frame)。它的作用可以理解为承载媒体数据的容器,具体包括:

  1. 视频数据:存储解码后的视频图像,比如一帧的像素数据(YUV、RGB等格式)。
  2. 音频数据:存储解码后的音频样本数据。
  3. 元数据:包含帧的时间戳(PTS/DTS)、格式(像素格式、采样格式)、宽高、通道数、采样率等信息。

主要功能

  • 承载解码后的媒体数据,方便进行后续处理(编码、滤镜、显示等)。
  • 传递帧数据 :解码器输出的是AVFrame,编码器的输入也是AVFrame
  • 避免直接管理内存 :用户通过FFmpeg API读写AVFrame,内部统一管理数据内存。

AVFrame的定义

c 复制代码
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
    //用于存放音视频数据,
    // video yuv图像数据,data[0]存放y分量, data[1]存放u分量,data[2]存放v分量
    // 音频数据多声道数据如果是交错存放,就值存放在data[0], 如果是planar audio,则data[0]存放声到1,data[1]存放声道2, ...
    uint8_t *data[AV_NUM_DATA_POINTERS];
    // data数组中每个元素的长度,单位为字节
    int linesize[AV_NUM_DATA_POINTERS];
    // 当音频的声道数大于8的时候,就需要使用extended_data来存放
    uint8_t **extended_data;
    // 视频帧的宽高
    int width, height;
    // 音频sample个数
    int nb_samples;
    // 视频图像格式,值为枚举AVPixelFormat定义。或者音频采样格式,值为枚举AVSampleFormat锁定义
    int format;
    // 视频帧的像素宽高比
    AVRational sample_aspect_ratio;
    
    int64_t pts;
    int64_t pkt_dts;
    AVRational time_base;
    //表示此帧可以重复的次数,一般为视频帧使用
    int repeat_pict;
    //音频的采样率
    int sample_rate;
    // 此framework中AVBuffer的引用,用于引用计数的计算,当引用计数为0时,自动释放buffer
    AVBufferRef *buf[AV_NUM_DATA_POINTERS];
    // extenddata的引用
    AVBufferRef **extended_buf;
    int        nb_extended_buf;
    //存放音视频帧的附加信息,如:视频帧中加的字幕信息,HDR信息等
    AVFrameSideData **side_data;
    int            nb_side_data;
    // 音频声道布局
    AVChannelLayout ch_layout;
    // 当前帧的时长
    int64_t duration;
    
    /**
     * Frame flags, a combination of @ref lavu_frame_flags
     */
    int flags;

    /**
     * MPEG vs JPEG YUV range.
     * - encoding: Set by user
     * - decoding: Set by libavcodec
     */
    enum AVColorRange color_range;

    enum AVColorPrimaries color_primaries;

    enum AVColorTransferCharacteristic color_trc;

    /**
     * YUV colorspace type.
     * - encoding: Set by user
     * - decoding: Set by libavcodec
     */
    enum AVColorSpace colorspace;

    enum AVChromaLocation chroma_location;

    /**
     * frame timestamp estimated using various heuristics, in stream time base
     * - encoding: unused
     * - decoding: set by libavcodec, read by user.
     */
    int64_t best_effort_timestamp;

AVFrame的基本用法

AVFrame中的buffer如何分配中以及如何写数据到AVFrame中?

c 复制代码
int main() {  
    // 假定参数  
    int sample_rate = 44100;  
    int channels = 2;  
    AVSampleFormat sample_fmt = AV_SAMPLE_FMT_S16; // 16bit整型  
    int nb_samples = 1024; // 每帧样本数  
    int bytes_per_sample = av_get_bytes_per_sample(sample_fmt);

    // 分配frame  
    AVFrame *frame = av_frame_alloc();   
    frame->format = sample_fmt;  
    frame->channels = channels;  
    frame->channel_layout = av_get_default_channel_layout(channels);  
    frame->sample_rate = sample_rate;  
    frame->nb_samples = nb_samples;  

    // 分配缓冲给frame->data, 
    // 其内部会根据nb_samples,format,channel_layout,sample_rate计算frame->data的空间 
    int ret = av_frame_get_buffer(frame, 0);  
    if (ret < 0) {  
        fprintf(stderr, "Failed to allocate frame buffer\n");  
        av_frame_free(&frame);  
        return -1;  
    }  

    // 这里需要有pcm_data数据准备好,这里只是个演示
    int frame_bytes = nb_samples * channels * bytes_per_sample;
    uint8_t *pcm_data = malloc(frame_bytes);    

    //AV_SAMPLE_FMT_S16是交错存放方式,直接放置在data[0]中
    //frame_bytes与 frame->linesize[0]相同
    memcpy(frame->data[0], pcm_data, frame_bytes);

    // 使用完毕释放资源  
    av_frame_free(&frame);  
    free(pcm_data);  

    return 0;  
}

上面的sample code中步骤:

  1. 通过av_frame_alloc分配AVFrame
  2. 通过av_frame_get_buffer函数给frame->data分配空间
  3. 将数据拷贝到frame->data[0]中
  4. av_frame_free将AVFrame以及其内部的data释放掉

AVFrame中data与extended_data有什么关系?

我们通过av_frame_get_buffer函数的源码来解答这个问题

c 复制代码
int av_frame_get_buffer(AVFrame *frame, int align)
{
    if (frame->format < 0)
        return AVERROR(EINVAL);

    if (frame->width > 0 && frame->height > 0)
        return get_video_buffer(frame, align);
    else if (frame->nb_samples > 0 &&
             (av_channel_layout_check(&frame->ch_layout)))
        //只有audio数据才使用extended_data
        return get_audio_buffer(frame, align);

    return AVERROR(EINVAL);
}

static int get_audio_buffer(AVFrame *frame, int align)
{
    //如: AV_SAMPLE_FMT_S16就是交错存放数据,AV_SAMPLE_FMT_S16P就是独立存放各声道的数据
    // AV_SAMPLE_FMT_S16 双声道: data[0]={sampleL0, sampleR0,sampleL1, sampleR1...}
    // AV_SAMPLE_FMT_S16P 双声道: data[0]={sampleL0, sampleL1, sampleL2...}
    //                           data[1]={sampleR0, sampleR1, sampleR2...}
    int planar   = av_sample_fmt_is_planar(frame->format);
    int channels, planes;
    int ret;

    channels = frame->ch_layout.nb_channels;
    //是否每个声道独立存放数据
    planes   = planar ? channels : 1;
    if (!frame->linesize[0]) {
        //计算data[]数组中每个元素的size
        // AV_SAMPLE_FMT_S16 双声道, 100个sample: linesize = 2(sample_size) * 2(channel) * 100(sample_count) = 400 byte
        // AV_SAMPLE_FMT_S16 双声道,100个sample:linesize = 2(sample_size)  * 100(sample_count) = 200 byte
        ret = av_samples_get_buffer_size(&frame->linesize[0], channels,
                                         frame->nb_samples, frame->format,
                                         align);
    }
    //如果声道数大于8个,则会给extended_data分配空间用于存放地址
    if (planes > AV_NUM_DATA_POINTERS) {
        frame->extended_data = av_calloc(planes,
                                          sizeof(*frame->extended_data));
        frame->extended_buf  = av_calloc(planes - AV_NUM_DATA_POINTERS,
                                          sizeof(*frame->extended_buf));
        frame->nb_extended_buf = planes - AV_NUM_DATA_POINTERS;
    } else //声道数小于8,extended_data就是data
        frame->extended_data = frame->data; 

    for (int i = 0; i < FFMIN(planes, AV_NUM_DATA_POINTERS); i++) {
        //分配AVBufferRef以及AVBuffer
        frame->buf[i] = av_buffer_alloc(frame->linesize[0]);
        //前8个声道extended_data和data指向同一个buffer
        frame->extended_data[i] = frame->data[i] = frame->buf[i]->data;
    }

    for (int i = 0; i < planes - AV_NUM_DATA_POINTERS; i++) {
        //分配AVBufferRef以及AVBuffer
        frame->extended_buf[i] = av_buffer_alloc(frame->linesize[0]);
        //超出的部分extended_data指向额外的buffer
        frame->extended_data[i + AV_NUM_DATA_POINTERS] = frame->extended_buf[i]->data;
    }
    return 0;
}

通过上面的代码可以看出,只有在声道数大于AV_NUM_DATA_POINTERS(当前为8)的audio数据的时候,extended_data才与data字段略有不同。下面看下示意图:

js 复制代码
交错存放数据时,只有data[0]有数据:
data[0] = extended_data[0] --> data buffer

平铺存放时,声道数小于8,假设为3:
data[0] = extended_data[0] --> data buffer A
data[1] = extended_data[1] --> data buffer B
data[2] = extended_data[2] --> data buffer C

平铺存放时,声道数大于8,假设为10:
data[0] = extended_data[0] --> data buffer A
data[1] = extended_data[1] --> data buffer B
data[2] = extended_data[2] --> data buffer D
data[3] = extended_data[3] --> data buffer E
data[4] = extended_data[4] --> data buffer F
data[5] = extended_data[5] --> data buffer G
data[6] = extended_data[6] --> data buffer H
data[7] = extended_data[7] --> data buffer I
//没有data[8]和data[9]
extended_data[9] --> data buffer J
extended_data[10] --> data buffer K

AVFrame中如何给视频帧分配空间比较复杂,稍后会单独出一章来阐述。

AVFrame关键函数介绍

函数 作用
AVFrame *av_frame_alloc(void); 分配AVFrame这个外壳,其内部的Buffer此函数内并没有分配
void av_frame_free(AVFrame **frame) 释放AVFrame内部任何动态分配的资源,如果buffer有引用计数,则先引用计数减一
int av_frame_ref(AVFrame *dst, const AVFrame *src); src拷贝所有属性,并创建 一个新的buffer ref来引用src的buffer
void av_frame_unref(AVFrame *frame); 取消引用帧所引用的所有缓冲区,并重置帧的字段
void av_frame_move_ref(AVFrame *dst, AVFrame *src) src 中包含的所有内容移动到 dst 中,并重置 src
int av_frame_replace(AVFrame *dst, const AVFrame *src) 先释放dst内部引用,然后创建新的引用来指向src内部的buffer
AVFrame *av_frame_clone(const AVFrame *src) 相当于av_frame_alloc()+av_frame_ref()
int av_frame_copy(AVFrame *dst, const AVFrame *src) 从src拷贝数据到dst,不拷贝属性
int av_frame_copy_props(AVFrame *dst, const AVFrame *src) 仅仅从src拷贝metadata字段到dst
int av_frame_get_buffer(AVFrame *frame, int align) 按照align字节对齐根据AVFrame内部的数据格式计算buffer size并分配buffer
AVBufferRef *av_frame_get_plane_buffer(const AVFrame *frame, int plane) 获取某个平台数据的引用

av_frame_ref & av_frame_unref

c 复制代码
int main() {
    // 分配源帧
    AVFrame *src_frame = av_frame_alloc();

    // 设置源帧的属性
    src_frame->format = AV_PIX_FMT_YUV420P;
    src_frame->width = 640;
    src_frame->height = 480;

    // 为源帧分配缓冲区
    av_frame_get_buffer(src_frame, 0);

    // 分配目标帧
    AVFrame *dst_frame = av_frame_alloc();
    
    // 使用 av_frame_ref 复制源帧的引用到目标帧
    // 此函数执行完之后,dst_frame和src_frame都指向同一个缓冲区,此时此缓冲区的引用计数为2
    av_frame_ref(dst_frame, src_frame)
    printf("Successfully referenced source frame to destination frame.\n");

    // 使用 av_frame_unref 取消目标帧的引用
    // 此函数执行完之后,此时此缓冲区的引用计数为1
    av_frame_unref(dst_frame);

    // 释放源帧, 此函数内部会再次进行引用计数减一,为0后释放缓冲区
    av_frame_free(&src_frame);

    return 0;
}

copy & clone & move_ref & replace的差异

c 复制代码
int main() {
    // 分配源帧
    AVFrame *src_frame = av_frame_alloc();

    // 设置源帧的属性
    src_frame->format = AV_PIX_FMT_YUV420P;
    src_frame->width = 640;
    src_frame->height = 480;

    // 为源帧分配缓冲区
    av_frame_get_buffer(src_frame, 0);

    // 1. av_frame_move_ref 演示
    AVFrame *dst_move_ref = av_frame_alloc();
    // 为dst_move_ref分配AVBufferRef并指向src_frame的AVBuffer,没有音视频数据拷贝
    // 此函数执行后src_frame内部的AVBufferRef *buf[]为NULL,av_frame_free(&src_frame)之后src_frame最开始指向的AVBuffer不会被释放
    av_frame_move_ref(dst_move_ref, src_frame);
    
    // 恢复源帧,用于后续测试
    av_frame_free(&src_frame);
    src_frame = av_frame_alloc();
    
    src_frame->format = AV_PIX_FMT_YUV420P;
    src_frame->width = 640;
    src_frame->height = 480;
    av_frame_get_buffer(src_frame, 0);

    // 2. av_frame_replace 演示
    AVFrame *dst_replace = av_frame_alloc();
    dst_replace->format = AV_PIX_FMT_YUV420P;
    dst_replace->width = 640;
    dst_replace->height = 480;
    av_frame_get_buffer(dst_replace, 0);
    // 如果dst_replace已有buffer引用,会先解引用,然后再指向src_frame的buffer,不会有内存泄漏
    // 没有音视频数据拷贝
    av_frame_replace(dst_replace, src_frame);
   
    // 3. av_frame_clone 演示
    // 分配一个AVFrame以及其内部AVBufferRef并引用src_frame的AVFrameBuffer
    // 没有音视频数据拷贝
    AVFrame *dst_clone = av_frame_clone(src_frame);
   

    // 4. av_frame_copy 演示
    AVFrame *dst_copy = av_frame_alloc();
   
    dst_copy->format = src_frame->format;
    dst_copy->width = src_frame->width;
    dst_copy->height = src_frame->height;
    av_frame_get_buffer(dst_copy, 0);
    //需要提前把dst_copy的属性以及内部的buffer分配好,仅仅只是拷贝buffer
    av_frame_copy(dst_copy, src_frame)

    // 释放所有帧
    av_frame_free(&src_frame);
    av_frame_free(&dst_move_ref);
    av_frame_free(&dst_replace);
    av_frame_free(&dst_clone);
    av_frame_free(&dst_copy);

    return 0;
}  
相关推荐
追随远方2 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
视频砖家6 小时前
如何设置FFmpeg实现对高分辨率视频进行转码
ffmpeg·音视频·视频编解码·视频转码
yanjiee8 小时前
视频质量分析时,遇到不同分辨率的对照视频和源视频,分辨率对齐的正确顺序。
ffmpeg·音视频
Sleepless_斑马8 小时前
【FFmpeg+SDL】使用FFmpeg捕获屏幕,SDL显示
ffmpeg
aningxiaoxixi12 小时前
FFMPEG 与 mp4
ffmpeg
慢一点会很快2 天前
【FFmpeg】介绍+安装+VisualStudio配置FFMpeg库
ide·ffmpeg·visual studio
邪恶的贝利亚3 天前
《ffplay 读线程与解码线程分析:从初始化到 seek 操作,对比视频与音频解码的差异》
ffmpeg·php·音视频
路溪非溪4 天前
关于ffmpeg的简介和使用总结
ffmpeg
gushansanren4 天前
基于WSL用MSVC编译ffmpeg7.1
windows·ffmpeg
追随远方6 天前
Android平台FFmpeg音视频开发深度指南
android·ffmpeg·音视频