美文网首页
FFmpeg开发——基础篇(一)

FFmpeg开发——基础篇(一)

作者: 拉丁吴 | 来源:发表于2024-04-24 01:40 被阅读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可以获取正确的信息

    • 读取数据包:确认有音视频数据之后,可以通过av_read_frame把数据流读取到AVPacket中
    // 此处主要涉及解码过程,可以略过,知道解码过程也需要传递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
    // 通常在你需要进行音视频编码并生成一个新的音视频文件时使用
    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);
    
    
    • 释放avformatcontext结构体
      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枚举类型。

    • 如何获取AVStream
    // 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,从而获取该解码器的比较全面的信息。

    • 获取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手动申请内存,则需要手动释放内存,如果自动申请内存则不需要。
    • 从编解码器中接收数据放到packet中,使用完之后,需要释放引用av_packet_unref 即可
    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的空间就需要我们自己开辟了,也需要我们进行释放。

    • 填充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的使用方式。

    相关文章

      网友评论

          本文标题:FFmpeg开发——基础篇(一)

          本文链接:https://www.haomeiwen.com/subject/cggwxjtx.html