购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4
FFmpeg常见的处理流程

本节介绍FFmpeg在处理音视频文件时常见的处理流程,首先描述如何复制音视频编解码器中的参数信息,然后叙述如何创建并写入音视频文件,最后阐述如何使用滤镜加工音视频文件。

2.4.1 复制编解码器的参数

对于现成的音视频文件,其内部音视频的解码器藏在数据流的codecpar字段中,该字段为AVCodecParameters结构的指针类型,解码器编号就来自AVCodecParameters结构的codec_id字段。然而编解码器的AVCodec结构仅仅是规格定义,并不足以执行真正的编解码操作,还需调用avcodec_alloc_context3函数根据编解码器分配对应的实例AVCodecContext才行。解码器的实例分配之后,接着调用avcodec_parameters_to_context函数把数据流的编解码参数复制给解码器的实例,然后调用avcodec_open2函数才算成功打开实例。在关闭解码器的实例时,要先调用avcodec_close函数关闭实例,再调用avcodec_free_context函数释放实例资源。

总结一下,打开编解码器实例的完整流程为avcodec_alloc_context3→avcodec_parameters_to_context→avcodec_open2,关闭编解码器实例的完整流程为avcodec_close→avcodec_free_context。在编解码器实例打开之后,才能对数据包或者数据帧进行编解码操作。具体而言,就是数据包AVPacket经过解码生成数据帧AVFrame;反之,数据帧AVFrame经过编码生成数据包AVPacket。下面的FFmpeg代码片段演示了如何打开编解码器的实例(完整代码见chapter02/para.c)。

    AVCodecContext *video_decode_ctx = NULL;
    video_decode_ctx = avcodec_alloc_context3(video_codec);  // 分配解码器的实例
    if (!video_decode_ctx) {
       av_log(NULL, AV_LOG_ERROR, "video_decode_ctx is null\n");
       return -1;
    }
    // 把视频流中的编解码参数复制给解码器的实例
    avcodec_parameters_to_context(video_decode_ctx, video_stream->codecpar);
    av_log(NULL, AV_LOG_INFO, "Success copy video parameters_to_context.\n");
    ret = avcodec_open2(video_decode_ctx, video_codec, NULL);  // 打开解码器的实例
    av_log(NULL, AV_LOG_INFO, "Success open video codec.\n");
    if (ret < 0) {
       av_log(NULL, AV_LOG_ERROR, "Can't open video_decode_ctx.\n");
       return -1;
    }
    avcodec_close(video_decode_ctx);          // 关闭解码器的实例
    avcodec_free_context(&video_decode_ctx);   // 释放解码器的实例

其实只要调用了avcodec_parameters_to_context函数,就能获取音视频文件的详细编码参数,具体参数保存在AVCodecContext结构中,该结构的常见字段说明如下。

· codec_id:编解码器的编号。编号说明参见表2-5。

· codec_type:编解码器的类型。类型说明参见表2-2。

· width:视频画面的宽度。

· height:视频画面的高度。

· gop_size:每两个关键帧(I帧)间隔多少帧。

· max_b_frames:双向预测帧(B帧)的最大数量。

· pix_fmt:视频的像素格式。像素格式的定义来自AVPixelFormat枚举,详细的像素格式类型及其说明见表2-6。

表2-6 视频的像素格式及其说明

· profile:音频的规格类型,主要用于细分AAC音频的种类,详细的AAC种类定义及其说明见表2-7。

表2-7 AAC音频种类及其说明

· ch_layout:音频的声道布局,该字段为AVChannelLayout结构,声道数量为AVChannelLayout结构的nb_channels字段。声道数量的定义及其说明见表2-8。

表2-8 音频的声道数量及其说明

· sample_fmt:音频的采样格式。采样格式的定义来自AVSampleFormat枚举,详细的采样格式定义及其说明见表2-9。

表2-9 音频的采样格式及其说明

表2-9提到了交错模式和平面模式,它们之间的区别在于:平面模式把左右声道的音频数据分开存储,比如左声道的数据都保存在data[0]中(类似于LLLLL这样),右声道的数据都保存在data[1]中(类似于RRRRR这样);而交错模式把左右声道的音频数据都存储在data[0]中,数据分布形如LRLRLRLRLR这样。通常音频在播放时采用平面模式,方便扬声器设备取数,而保存文件时采用交错模式。

· sample_rate:音频的采样频率,单位为赫兹(次每秒)。

· frame_size:音频的帧大小,也叫采样个数,即每个音频帧采集的样本数量。

· bit_rate:码率,也叫比特率,单位为比特每秒。

· framerate:视频的帧率,该字段为AVRational结构。

· time_base:音视频的时间基,该字段为AVRational结构。

接下来把AVStream到AVCodec再到AVCodecContext的完整流程串起来,分别在视频流和音频流中寻找它们的解码器实例,并执行实例的打开和关闭操作。据此编写的FFmpeg示例代码如下(完整的代码见chapter02/para.c)。

接着执行下面的编译命令:

    gcc para.c -o para -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望查看指定文件的编解码参数。

    ./para ../fuzhou.mp4

程序运行完毕,发现控制台输出如下日志信息:

    Success open input_file ../fuzhou.mp4.
    video_codec name=h264
    Success copy video parameters_to_context.
    video_decode_ctx width=1440
    video_decode_ctx height=810
    Success open video codec.
    audio_codec name=aac
    Success copy audio parameters_to_context.
    audio_decode_ctx profile=1
    audio_decode_ctx nb_channels=2
    Success open audio codec.

由日志信息可见,视频流和音频流的解码器实例都被找到并且成功打开,还发现目标文件的视频宽高为1440×810,并且音频规格为AAC-LC(profile=1,根据表2-7找到规格说明),声道类型为双声道(立体声)。

2.4.2 创建并写入音视频文件

前面介绍的音视频处理都属于对文件的读操作,如果是写操作,那又是另一套流程。写入音视频文件的总体步骤说明如下:

01 调用avformat_alloc_output_context2函数分配音视频文件的封装实例。

02 调用avio_open函数打开音视频文件的输出流。

03 调用avformat_write_header函数写入音视频的文件头。

04 多次调用av_write_frame函数写入音视频的数据帧。

05 调用av_write_trailer函数写入音视频的文件尾。

06 调用avio_close函数关闭音视频文件的输出流。

07 调用avformat_free_context函数释放音视频文件的封装实例。

由此可见,FFmpeg对于音视频的处理操作很多是对称的,有分配就有释放,有打开就有关闭,有写文件头就有写文件尾。包含上述写文件步骤的FFmpeg示例代码片段如下。

    AVFormatContext *out_fmt_ctx;  // 输出文件的封装器实例
    // 分配音视频文件的封装实例
    int ret = avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, filename);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't alloc output_file %s.\n", filename);
        return -1;
    }
    // 打开输出流
    ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't open output_file %s.\n", filename);
        return -1;
    }
    av_log(NULL, AV_LOG_INFO, "Success open output_file %s.\n", filename);
    ret = avformat_write_header(out_fmt_ctx, NULL);  // 写文件头
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "write file_header occur error %d.\n", ret);
        return -1;
    }
    av_log(NULL, AV_LOG_INFO, "Success write file_header.\n");
    av_write_trailer(out_fmt_ctx);                      // 写文件尾
    avio_close(&out_fmt_ctx->pb);                       // 关闭输出流
    avformat_free_context(out_fmt_ctx);                 // 释放封装器的实例

不过,如果直接编译运行上述步骤的代码,会发现程序运行失败。究其原因,缘于音视频文件要求至少封装一路数据流,要么封装单路视频,要么封装单路音频,要么封装包括音频和视频在内的两路数据流。总之,不允许连一路数据流都没有。以下是音视频文件封装数据流的总体步骤说明。

01 调用avcodec_find_encoder函数查找指定编号的编码器。

02 调用avcodec_alloc_context3函数根据编码器分配对应的编码器实例。对于视频来说,还要设置编码器实例的width和height字段,指定视频画面的宽高。

03 调用avformat_new_stream函数,给输出文件创建采用指定编码器的数据流。

04 调用avcodec_parameters_from_context函数把编码器实例的参数复制给数据流。

上述的封装步骤虽然没有写入真实的视频帧,但是不影响测试程序的正常运行,反正已经创建了一路视频流,无非是视频内容为空而已。综合音视频文件的写入步骤,以及数据流的封装步骤,编写完整的FFmpeg代码如下(完整代码见chapter02/write.c)。

    #include <stdio.h>
    // 之所以增加__cplusplus的宏定义,是为了同时兼容GCC编译器和G++编译器
    #ifdef __cplusplus
    extern "C"
    {
    #endif
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/avutil.h>
    #ifdef __cplusplus
    };
    #endif
 
    int main(int argc, char **argv) {
        const char *filename = "output.mp4";
        if (argc > 1) {
            filename = argv[1];
        }
        AVFormatContext *out_fmt_ctx;  // 输出文件的封装器实例
        // 分配音视频文件的封装实例
        int ret = avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, filename);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Can't alloc output_file %s.\n", filename);
            return -1;
        }
        // 打开输出流
        ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_READ_WRITE);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "Can't open output_file %s.\n", filename);
            return -1;
        }
        av_log(NULL, AV_LOG_INFO, "Success open output_file %s.\n", filename);
        // 查找编码器
        AVCodec *video_codec = (AVCodec*) avcodec_find_encoder(AV_CODEC_ID_H264);
        if (!video_codec) {
            av_log(NULL, AV_LOG_ERROR, "AV_CODEC_ID_H264 not found\n");
            return -1;
        }
        AVCodecContext *video_encode_ctx = NULL;
        video_encode_ctx = avcodec_alloc_context3(video_codec);  // 分配编解码器的实例
        if (!video_encode_ctx) {
            av_log(NULL, AV_LOG_ERROR, "video_encode_ctx is null\n");
            return -1;
        }
        video_encode_ctx->width = 320;                  // 视频画面的宽度
        video_encode_ctx->height = 240;                 // 视频画面的高度
        // 创建指定编码器的数据流
        AVStream * video_stream = avformat_new_stream(out_fmt_ctx, video_codec);
        // 把编码器实例中的参数复制给数据流
        avcodec_parameters_from_context(video_stream->codecpar, video_encode_ctx);
        video_stream->codecpar->codec_tag = 0;                     // 非特殊情况都填0
        ret = avformat_write_header(out_fmt_ctx, NULL);  // 写文件头
       if (ret < 0) {
           av_log(NULL, AV_LOG_ERROR, "write file_header occur error %d.\n", ret);
           return -1;
       }
       av_log(NULL, AV_LOG_INFO, "Success write file_header.\n");
       av_write_trailer(out_fmt_ctx);                  // 写文件尾
       avio_close(&out_fmt_ctx->pb);                   // 关闭输出流
       avformat_free_context(out_fmt_ctx);             // 释放封装器的实例
       return 0;
    }

接着执行下面的编译命令:

     gcc write.c -o write -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望生成一个全新的视频文件。

    ./write output.mp4

程序运行完毕,发现控制台输出如下日志信息,可知已成功写入了音视频文件。

    Success open output_file output.mp4.
    Success write file_header.

2.4.3 使用滤镜加工音视频

音视频文件的修改操作与文本文件不同,修改音视频数据需要借助滤镜,由FFmpeg提供的各类滤镜实现相应的更改处理。使用滤镜加工音视频的总体步骤说明如下。

01 根据源文件的数据流、解码器实例、过滤字符串来初始化滤镜,得到输入滤镜的实例和输出滤镜的实例。

02 调用av_buffersrc_add_frame_flags函数把一个数据帧添加到输入滤镜的实例。

03 调用av_buffersink_get_frame函数从输出滤镜的实例获取加工后的数据帧。

04 把加工后的数据帧压缩编码后保存到目标文件中。

05 重复前面的第2~4步,直到源文件的所有数据帧都处理完毕。

关于av_buffersrc_add_frame_flags和av_buffersink_get_frame两个函数的用法留待以后详述,本小节只介绍第1步的滤镜初始化操作,初始化滤镜的具体步骤说明如下。

01 声明滤镜的各种实例资源,除输入滤镜的实例、输出滤镜的实例、滤镜图外,还要调用avfilter_get_by_name函数分别获取输入滤镜和输出滤镜,调用avfilter_inout_alloc函数各自分配滤镜的输入输出参数,调用avfilter_graph_alloc函数分配一个滤镜图。

02 拼接输入源的媒体参数信息字符串,以视频为例,参数字符串需要包括视频宽高、像素格式、时间基等。

03 调用avfilter_graph_create_filter函数,根据输入滤镜和第2步的参数字符串,创建输入滤镜的实例,并将其添加到现有的滤镜图中。

04 调用avfilter_graph_create_filter函数,根据输出滤镜创建输出滤镜的实例,并将其添加到现有的滤镜图中。

05 调用av_opt_set_int_list函数设置额外的选项参数,比如加工视频要给输出滤镜的实例设置像素格式。

06 设置滤镜的输入输出参数,给AVFilterInOut结构的filter_ctx字段填写输入滤镜的实例或者输出滤镜的实例。

07 调用avfilter_graph_parse_ptr函数,把采用过滤字符串描述的图形添加到滤镜图中,这个过滤字符串指定了滤镜的种类名称及其参数取值。

08 调用avfilter_graph_config函数检查过滤字符串的有效性,并配置滤镜图中的所有前后连接和图像格式。

09 调用avfilter_inout_free函数分别释放滤镜的输入参数和输出参数。

综合上述的滤镜初始化步骤说明,编写FFmpeg对滤镜的初始化函数代码如下(完整代码见chapter02/filter.c):

     AVFilterContext *buffersrc_ctx = NULL;                    // 输入滤镜的实例
     AVFilterContext *buffersink_ctx = NULL;                   // 输出滤镜的实例
     AVFilterGraph *filter_graph = NULL;                       // 滤镜图
 
     // 初始化滤镜(也称过滤器、滤波器)。第一个参数是视频流,第二个参数是解码器实例,第三个参数是过滤字符串
     int init_filter(AVStream *video_stream, AVCodecContext *video_decode_ctx, const char
*filters_desc) {
         int ret = 0;
         const AVFilter *buffersrc = avfilter_get_by_name("buffer");                      // 获取输入滤镜
         const AVFilter *buffersink = avfilter_get_by_name("buffersink");   // 获取输出滤镜
         AVFilterInOut *inputs = avfilter_inout_alloc();                       // 分配滤镜的输入输出参数
         AVFilterInOut *outputs = avfilter_inout_alloc();                      // 分配滤镜的输入输出参数
         AVRational time_base = video_stream->time_base;
         enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
         filter_graph = avfilter_graph_alloc();                                // 分配一个滤镜图
         if (!outputs || !inputs || !filter_graph) {
             ret = AVERROR(ENOMEM);
             return ret;
         }
         char args[512];             //临时字符串,存放输入源的媒体参数信息,比如视频宽高、像素格式等
         snprintf(args, sizeof(args),
             "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
             video_decode_ctx->width, video_decode_ctx->height, video_decode_ctx->pix_fmt,
             time_base.num, time_base.den,
             video_decode_ctx->sample_aspect_ratio.num, video_decode_ctx->
sample_aspect_ratio.den);
         av_log(NULL, AV_LOG_INFO, "args : %s\n", args);
         // 创建输入滤镜的实例,并将其添加到现有的滤镜图中
         ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
             args, NULL, filter_graph);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Cannot create buffer source\n");
             return ret;
         }
         // 创建输出滤镜的实例,并将其添加到现有的滤镜图中
         ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
             NULL, NULL, filter_graph);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
             return ret;
         }
         // 将二进制选项设置为整数列表,此处给输出滤镜的实例设置像素格式
         ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
             AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
             return ret;
         }
         // 设置滤镜的输入输出参数
         outputs->name = av_strdup("in");
         outputs->filter_ctx = buffersrc_ctx;
         outputs->pad_idx = 0;
         outputs->next = NULL;
         // 设置滤镜的输入输出参数
         inputs->name = av_strdup("out");
         inputs->filter_ctx = buffersink_ctx;
         inputs->pad_idx = 0;
         inputs->next = NULL;
         // 把采用过滤字符串描述的图形添加到滤镜图中
         ret = avfilter_graph_parse_ptr(filter_graph, filters_desc, &inputs, &outputs, NULL);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Cannot parse graph string\n");
             return ret;
         }
         // 检查过滤字符串的有效性,并配置滤镜图中的所有前后连接和图像格式
         ret = avfilter_graph_config(filter_graph, NULL);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Cannot config filter graph\n");
             return ret;
         }
         avfilter_inout_free(&inputs);   // 释放滤镜的输入参数
         avfilter_inout_free(&outputs);  // 释放滤镜的输出参数
         av_log(NULL, AV_LOG_INFO, "Success initialize filter.\n");
         return ret;
      }
    

以上代码定义了一个名为init_filter的滤镜初始化函数,接着回到读取源文件的FFmpeg代码中,增加调用init_filter函数即可正常初始化滤镜。滤镜使用结束后,要记得调用avfilter_free函数分别释放输入滤镜的实例和输出滤镜的实例,还要调用avfilter_graph_free函数释放滤镜图资源。下面是针对视频流初始化视频滤镜的FFmpeg代码片段:

    // 找到视频流的索引
    int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (video_index >= 0) {
        AVStream *video_stream = fmt_ctx->streams[video_index];
        enum AVCodecID video_codec_id = video_stream->codecpar->codec_id;
        // 查找视频解码器
       AVCodec *video_codec = (AVCodec*) avcodec_find_decoder(video_codec_id);
       if (!video_codec) {
           av_log(NULL, AV_LOG_INFO, "video_codec not found\n");
           return -1;
       }
       av_log(NULL, AV_LOG_INFO, "video_codec name=%s\n", video_codec->name);
       AVCodecContext *video_decode_ctx = NULL;                              // 视频解码器的实例
       video_decode_ctx = avcodec_alloc_context3(video_codec);   // 分配解码器的实例
       if (!video_decode_ctx) {
           av_log(NULL, AV_LOG_INFO, "video_decode_ctx is null\n");
           return -1;
       }
       // 把视频流中的编解码器参数复制给解码器的实例
       avcodec_parameters_to_context(video_decode_ctx, video_stream->codecpar);
       av_log(NULL, AV_LOG_INFO, "Success copy video parameters_to_context.\n");
       ret = avcodec_open2(video_decode_ctx, video_codec, NULL);                  // 打开解码器的实例
       av_log(NULL, AV_LOG_INFO, "Success open video codec.\n");
       if (ret < 0) {
           av_log(NULL, AV_LOG_ERROR, "Can't open video_decode_ctx.\n");
           return -1;
       }
       // 初始化滤镜
       init_filter(video_stream, video_decode_ctx, "fps=25");
       avcodec_close(video_decode_ctx);                      // 关闭解码器的实例
       avcodec_free_context(&video_decode_ctx);              // 释放解码器的实例
       avfilter_free(buffersrc_ctx);                         // 释放输入滤镜的实例
       avfilter_free(buffersink_ctx);                        // 释放输出滤镜的实例
       avfilter_graph_free(&filter_graph);                   // 释放滤镜图资源
    }

接着执行下面的编译命令:

     gcc filter.c -o filter -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望初始化视频滤镜。

    ./filter ../fuzhou.mp4

程序运行完毕,发现控制台输出如下日志信息,可知已成功初始化视频滤镜。

    Success open input_file ../fuzhou.mp4.
    video_codec name=h264
    Success copy video parameters_to_context.
    Success open video codec.
    args : video_size=1440x810:pix_fmt=0:time_base=1/12800:pixel_aspect=1/1
    Success initialize filter.

当然,这里的示例程序仅仅演示了如何初始化滤镜,并没有真正加工视频文件,也没将加工结果另存为新的文件,有关滤镜的加工处理操作会在第6章详细介绍。 /gkXUinx08bOyyX9qF11wGr+dUXXgot9pkaLaKZCYk+PjT0h98pynUWbIxwkhoam

点击中间区域
呼出菜单
上一章
目录
下一章
×