FFmpeg开发——基础篇(一)

2024-04-24  本文已影响0人  拉丁吴

前言

书接上回,我们介绍了ffmpeg的一些基础知识,使用方法,接下来介绍如何使用ffmpeg进行开发,所谓使用ffmpeg进行开发,就是依赖它的基础库,调用它的API来实现我们的功能。

当然要看懂文章需要一些C++的基础知识,能看懂基本语法,了解指针(一级指针/二级指针)的基本知识。

image.png

我们在上篇文章中也介绍了ffmpeg对于音视频操作的主要流程:

image.png

其实差不多每个阶段都通过对应的关键函数以及关键结构体来对应,因此,接下来先重点介绍一下这些重要的结构体,以及它的用法。

核心结构体

AVFormatContext

媒体信息存储结构,同时管理了IO音视频流对文件进行读写,相当于保存了音视频信息的上下文。

它在ffmpeg中的作用是非常重要的,在封装/解封装/编解码过程中都需要用到它。在程序中关于某个音视频的所有信息归根结底都来自于AVFormatContext。

结构体信息


typedef struct AVFormatContext {
    // 针对输入逻辑的结构体
    const struct AVInputFormat *iformat;
    // 针对输出逻辑的结构体
    const struct AVOutputFormat *oformat;

    //字节流IO操作 结构体
    AVIOContext *pb;
    ...
    ...
    unsigned int nb_streams; // 视音频流的个数

    AVStream **streams; // 视音频流


    char *url;  //输入或输出地址 替换原有的filename

    int64_t duration; // 时长,微秒(1s/1000_000)

    int64_t bit_rate; // 比特率(单位bps,转换为kbps需要除以1000)

    ...
    ...
} AVFormatContext;

以上只是一个简略的结构体成员信息展示,但是已经能体现它管理IO,数据流,保存媒体信息的的功能了,对于初学者而言,只需要关注nb_streams和streams这两个成员,表示流的数量以及流数组。后面我们需要通过流来获取对应的信息。

使用的基本方法

输入过程

// 可以,但一般没必要
AVFormatContext *formatContext = avformat_alloc_context();

但是一般在开发中并不需要开发者手动创建结构体,而是在读取文件的接口中通过库自动创建即可

AVFormatContext *formatContext = NULL;


// 注意 即使传入的formatContext为NULL,avformat_open_input内部也会为formatContext申请内存空间的
if (avformat_open_input(&formatContext, "xxx.mp4", NULL, NULL) != 0) {
    fprintf(stderr, "Failed to open input file\n");
    return 1;
}

注意我们需要在avformat_open_input函数中传入正确的媒体文件路径,这样才能正确读取到文件头的信息。

if (avformat_find_stream_info(formatContext, NULL) < 0) {
    fprintf(stderr, "Failed to find stream information\n");
    return 1;
}

avformat_open_input即使成功调用,也不一定能获取到文件头信息,因为可能有的媒体格式没有文件头?哈哈,所以一般继续调用avformat_find_stream_info可以获取正确的信息

// 此处主要涉及解码过程,可以略过,知道解码过程也需要传递formatContext信息即可
AVPacket packet;
while (av_read_frame(formatContext, &packet) == 0) {
    // 处理 packet 中的数据
   
     // 在使用完 packet 后释放引用
    av_packet_unref(&packet);
}
avformat_close_input(&formatContext);

avformat_close_input会关闭输入流,同时释放AVFormatContext结构体

输出过程

上面介绍的主要输入过程中使用AVFormatContext的基本方式,那么输出过程是否一致呢?函数调用上略有区别。

// 通常在你需要进行音视频编码并生成一个新的音视频文件时使用
AVFormatContext *output_format_context = NULL;

// 
avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);

//先写入头文件
ret = avformat_write_header(output_format_context, &opts);
  
//再写入帧数据
ret = av_interleaved_write_frame(output_format_context, &packet);

// 写入收尾(同时刷新缓冲区)
av_write_trailer(output_format_context);

  avformat_free_context(output_format_context); 
  

AVStream

AVStream是AVFormatContext结构体中的一个成员(数组结构),它表示媒体文件中某一种数据的流以及对应的媒体信息,比如该流表示视频流,则同时也会含有视频相关的宽高,帧率等信息,以及time_base等基础信息。


typedef struct AVStream {


    int index;    /**< stream index in AVFormatContext */
    // stream ID
    int id;

    // 与流关联的编解码器的参数结构
    AVCodecParameters *codecpar;

    //time_base   AVRational结构体有两个成员,组成一个分数(有理数)
    AVRational time_base;

    ...
    ...

    int64_t duration;

    int64_t nb_frames;                 ///< number of frames in this stream if known or 0
    ...
    ...
    /**
     * sample aspect ratio (0 if unknown)
     * - encoding: Set by user.
     * - decoding: Set by libavformat.
     */
    AVRational sample_aspect_ratio;
    ...
    ...
} AVStream;

对于初学者而言,可以先重点关注time_base和codecpar这两个成员,time_base不用讲,是ffmpeg中的时间基本单位,codecpar则表示了当前流的解码信息。

我们可以看一下AVCodecParameters这个结构体的成员情况

typedef struct AVCodecParameters {
    /**
     * General type of the encoded data.
     */
    enum AVMediaType codec_type;
    /**
     * Specific type of the encoded data (the codec used).
     */
    enum AVCodecID   codec_id;
    ...

    /**
     * - video: the pixel format, the value corresponds to enum AVPixelFormat.
     * - audio: the sample format, the value corresponds to enum AVSampleFormat.
     */
    int format;
    ...
    ...
    /**
     * 视频帧相关的一些参数
     * Video only. The dimensions of the video frame in pixels.
     */
    int width;
    int height;

    AVRational sample_aspect_ratio;

    enum AVColorRange                  color_range;
    enum AVColorPrimaries              color_primaries;
    enum AVColorTransferCharacteristic color_trc;
    enum AVColorSpace                  color_space;
    enum AVChromaLocation              chroma_location;

    /**
     * Audio only. The number of audio samples per second.
     */
    int      sample_rate;

   // Audio only. Audio frame size
    int      frame_size;

    // 声道配置情况(音频)
    AVChannelLayout ch_layout;


    AVRational framerate;

} AVCodecParameters;

可以看到AVCodecParameters是把音频和视频这两种信息混合在一起了,在流属于不同类型时使用不同的字段,或者同一个字段表达不同的含义。比如format,如果是视频,则表示AVPixelFormat枚举类型,如果是音频,则表示AVSampleFormat枚举类型。

// formatContext->nb_streams表示流的个数
int stream_size = formatContext->nb_streams;

for(int i=0;i<stream_size;i++){
    //获取一个AVStream
    AVStream *in_stream = formatContext->streams[i];
    // 从AVStream中获取AVCodecParameters
    AVCodecParameters *av_in_codec_param = in_stream->codecpar;
    ... 
}
    

AVCodec

//编解码器
typedef struct AVCodec {
    // 编解码器的名称
    const char *name;
    const char *long_name;
    enum AVMediaType type; // 媒体类型(视频,音频,字幕等)
    enum AVCodecID id; // 编解码器的ID

    // 编解码器所支持的一些参数
    const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
    const enum AVPixelFormat *pix_fmts;     ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
    const int *supported_samplerates;       ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
    const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1

    /**
     * Array of supported channel layouts, terminated with a zeroed layout.
     */
    const AVChannelLayout *ch_layouts;
} AVCodec;

AVCodec可以表示一个编解码器,里面包含了编解码的一些基本信息。

一般而言,媒体文件中的音频流,视频流中都保存有解码器ID等信息,通过这个ID可以获取对应AVCodec,从而获取该解码器的比较全面的信息。

// formatContext即 AVFormatContext的结构体对象,此时应该已经创建并读取了信息
// codecpar是AVStream中的结构体成员,表示该流数据对应的解码器信息
// 从流中找到对应编解码器信息和id
enum AVCodecID id = formatContext->streams[videoStreamIndex]->codecpar->codec_id
// 通过ID找到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

获取到AVCodec之后,需要通过它构建一个可用编解码器上下文(提供编解码过程中待解码数据的背景和配置)

AVCodecContext


typedef struct AVCodecContext {

    enum AVMediaType codec_type;// 数据类型(音频、视频、字幕、等)
    const struct AVCodec  *codec; // 对应的编解码器
    enum AVCodecID     codec_id; //编解码器ID

     // time_base,编码时必须设置
    AVRational time_base; 
    
    /*视频使用*/
    int width, height;
    // 像素格式,告诉解码器你想要把数据解码成哪个像素格式,不设置的话ffmpeg会有默认值
    enum AVPixelFormat pix_fmt;

    /* audio only */
    int sample_rate; ///< samples per second
    ...
    ...
    enum AVSampleFormat sample_fmt;  ///< 采样格式

    // AVFrame中每个声道的采样数,音频时使用
    int frame_size;
    //也是time_base,解码时设置
    AVRational pkt_timebase;
}

AVCodecContext就是我们前面说的编解码上下文,主要包含待解码数据的一些特性,便于在解码过程中解码器正确解析数据。比如等待解码的是视频数据,那么解码器需要知道time_base(关于time_base的概念不懂可以看前一篇文章)统一时间单位;每帧图片的宽高;视频帧的像素格式(关于像素格式见(移动开发中关于视频的一些基本概念),了解像素排列方式....

有了以上信息,解码器才可以正确的对数据进行解码。

AVCodecContext中音频的的frame_size,在解码器中可能不存在,因此在解码过程避免使用这个字段,可以找decoded_frame中的nb_samples来替代

// id 是从前文中通过AVStream中获取的
// 获取到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

  // 创建AVCodecContext的结构,此时还没有对应的参数(都是默认参数)
AVCodecContext  *pCodecCtx = avcodec_alloc_context3(av_codec);

// 从数据流AVStream中得到的AVCodecParameter中的相关信息复制到AVCodecContext
// 此时AVCodecContext就有了正确的信息了
if(avcodec_parameters_to_context(pCodecCtx,av_codec_parameters) < 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }
 //初始化并启动解码器
 if(avcodec_open2(pCodecCtx, pCodec, NULL)<0){
       return -1; // Could not open codec
 }

利用AVCodec构建AVCodecContext,然后把AVStream中已知的一些信息复制到AVCodecContext中,接着初始化并开启编解码器。

avcodec_free_context(&pCodecCtx)

AVCodecContext在编解码过程中都会被用到。

AVPacket

读取文件获取AVFormatContext结构体,并且获取了解码器上下文

AVPacket是存储压缩编码数据相关信息的结构体


typedef struct AVPacket {
    int64_t pts; // 显示时间戳
    int64_t dts; // 解码时间戳
    uint8_t *data;   // 压缩编码的数据
    int   size; // data的大小
    int   stream_index; // 当前packet所属的流(视频流或者音频流等)
    ...
    ...
    ...

    AVRational time_base;
}

AVPacket的成员主要包括time_base,pts,dts等一些在解码时可能被用到的参数以及编码数据data。

创建过程

AVPacket pkt; // 自动申请出内存
// 此时只是申请了AVPacket结构体的内存空间,其所指向的数据内存区域还没有创建
AVPacket *pkt2 = av_packet_alloc(); // 如果手动申请内存,泽需要和后续av_packet_free释放
...
// do something with avpacket
...
// 在使用完 pkt 后释放内存
av_packet_free(&pkt2);

使用方式

// 从AVFormatContext中读取数据到avpacket中
// 创建avpacket指向的数据内存区域的函数是av_new_packet
if (av_read_frame(formatContext, &pkt) == 0) { 
    ...
    //解码器解码处理 pkt中的数据
    ...
    //在使用完 pkt 后释放引用(引用数到0),从而释放其指向的数据内存区域
    av_packet_unref(&pkt);
}

AVFrame

AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),除此之外就是数据对应的一些属性:时长,格式等

视频和音频共用一个结构体,因此有的属性是双方公用,有的可能主要用于一方
typedef struct AVFrame {
    #define AV_NUM_DATA_POINTERS 8
    // *data[]是一个成员为指针的数组
    // 原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
    uint8_t *data[AV_NUM_DATA_POINTERS];
    
    // data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽
    int linesize[AV_NUM_DATA_POINTERS];

    uint8_t **extended_data;

    int width, height;

    /**
     * number of audio samples (per channel) described by this frame
     */
     // 音频类型中,AVFrame包含的多少个采样
    int nb_samples;


// 音视频的格式,
    int format;

     //帧类型,I帧,P帧,B帧等
    enum AVPictureType pict_type;
    ...

    /**
     * Presentation timestamp in time_base units (time when frame should be shown to user).
     */
    int64_t pts;

    /**
     * DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
     * This is also the Presentation time of this AVFrame calculated from
     * only AVPacket.dts values without pts values.
     */
    int64_t pkt_dts;

    AVRational time_base;

    int sample_rate;

    AVChannelLayout ch_layout;

    int64_t duration;
} AVFrame;

data与linesize

关于data和linesize这两个字段,分别表示原始数据存储数组和每一行的大小。但是数据是如何排列的我们并不清楚。

之前讲视频的基础知识时,我们讲到YUV的数据排列有多种方式,因此想要知道data中的YUV数据排列,我们还需要知道AVFrame的format,这个format来自于AVCodecContext->pix_fmt,这个解码器的参数设置成什么,最终解码出来的杨素格式就是什么。假如不指定的话,默认会解码为YUV420p。

我们假设视频数据解码出来的AVFrame,format是YUV420P,那么data和linesize的数据在ffmpeg中的内存示意图可能是这样的:

image.png

类似的音频数据解码出来的AVFrame,format是AV_SAMPLE_FMT_FLTP,双声道,那么对应的数据在ffmpeg中的内存示意图可能是这样的:

image.png

以上都是planar的存储模式,如果是packed(关于planar/packed的解释见文章)存储模式呢?

YUV422 packed存储格式的视频,ffmpeg中的内存示意图大概是这样的:

image.png

当然,其实对于初学者而言,一般不需要直接操作data和linesize,但是能够把ffmpeg中的数据结构和所学的音视频知识做一个对应理解会更深刻。

对于linsize,音频类型。一般只有linesize[0]会被设置;视频则需要看存储方式的不同,常用的planar模式下,linsize数组一般会用到前三个。
对于data指针数组而言,音频数据占用数组几个的指针要看声道数和存储格式(palnar/packed);视频则只看存储格式,planar一般占3个,packed占

使用方式

  // 申请内存空间
  AvFrame *pFrame = av_frame_alloc();

解码就是AVPakcet=>AVFrame的过程。

/********一次循环************/
// 从输入文件的流中读取数据到packet中
av_read_frame(pFormatCtx, &packet)
// 把AVPacket中的数据发送到解码器
avcodec_send_packet(pCodecCtx,&packet);
// 从解码器中读取数据到AVFrame中
avcodec_receive_frame(pCodecCtx,pFrame);

编码则是AVFrame=>AVPacket的过程(解码的逆过程)。

// av_encode_ctx 编码器的上下文
// pFrame 已获得的原始数据帧
int ret = avcodec_send_frame(av_encode_ctx,pFrame); // 把原始数据发送到编码器

// 从编码器中读取编码后的数据到av_out_packet
ret = avcodec_receive_packet(av_encode_ctx,av_out_packet);

// 使用完之后
av_frame_free(&pFrame); 

手动填充AVFrame->data

ffmpeg中,我们是通过av_frame_alloc函数来获得AVFrame,但是这个函数只是开辟了AVFrame结构体空间,而avframe->data是一个成员为指针的数组,这些成员指针和它们指向的内存空间并未被开辟出来。我们从源码实现中也能看到:

AVFrame *av_frame_alloc(void)
{
    // 为AVFrame的结构体开辟空间
    AVFrame *frame = av_malloc(sizeof(*frame));

    if (!frame)
        return NULL;
    // 未某些成员赋默认值值(不包括data)
    get_frame_defaults(frame);

    return frame;
}

是因为在编解码过程中编解码器会帮助我们开辟这块空间,所以我们不必管。

但是假如我们在编解码之外使用AVFrame,比如把YUV类型的AVFrame转换为RGB类型的AVFrame,那么AVFrame->data的空间就需要我们自己开辟了,也需要我们进行释放。

以视频帧为例

// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();
  
// 通过宽高以及像素格式来计算获得新的帧所需要的缓冲区大小
numBytes= av_image_get_buffer_size(AV_PIX_FMT_RGB24, width,height,1);

// 假设 buffer = 1024byte   表示buffer是指向一个1024个uint_8数据的内存区域的指针
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

// 让pFrameRGB->data数组中几个指针分别指向buffer这块空间(不同位置),
// 然后可以向这块空间填充数据
av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize, buffer,AV_PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height,1);

总结

本文主要详细介绍了ffmpeg中比较重要的几个结构体,他们都伴随着音视频处理的某个阶段而存在的,因此了解他们有助于我们理解音视频的处理流程。

image.png

我们把前面的音视频解码播放流程图添加关键API和搭配关键结构体就会发现ffmpeg的处理流程还是比较简洁的。接下来我们尝试用一个较完整的demo来熟悉ffmpeg的使用方式。

上一篇下一篇

猜你喜欢

热点阅读