FFmpeg 过滤器

257 阅读16分钟

FFmpeg 过滤器

个人理解: 在FFmpeg中,AVFilterGraph代表了一个过滤器图,它可以被视为一组过滤器的有向图,其中数据(如音频或视频帧)沿边从一个过滤器传递到另一个过滤器。可以将AVFilterGraph想象成一个工作流或流水线。数据(如音频或视频帧)从源(source)进入,经过一系列过滤器进行处理,然后到达接收器(sink)。在这个过程中,每个过滤器都可以对数据进行某种形式的处理:例如,一个视频过滤器可能会调整颜色、裁剪图像或添加水印,而一个音频过滤器可能会调整音量、添加混响或进行均衡器调整。 在AVFilterGraph中:每个节点都代表一个过滤器实例,由AVFilterContext表示。每条边都代表数据从一个过滤器流向另一个过滤器的路径,由AVFilterLink表示。

同时AVFilterGraph应该是无环的。过滤器图的目的是处理并最终输出数据,如果存在循环,数据会陷入无限循环中,导致无法完成处理。并且,不是所有过滤器的出度和入度都限制为1。部分过滤器的输入(入度)确实是1,因为它们通常处理单一来源的数据。但也存在某些过滤器,如混合(mixing)或叠加(overlay)等,需要两个或更多的输入。这些过滤器可以从多个源接收数据,并将它们结合或叠加在一起。大多数过滤器的输出(出度)也是1,因为它们只产生一种类型的输出。然而有一些过滤器可以有多个输出。例如,一个分离音视频的过滤器可能会有一个视频输出和一个音频输出。

FFmpeg过滤器框架分析

1. 过滤器框架的架构

过滤器的核心组件

  • 输入和输出链接(Pad):在FFmpeg的过滤器框架中,每个过滤器都有输入链接和输出链接。这些链接允许过滤器从前一个过滤器接收数据,并将数据传递给下一个过滤器。
  • 过滤器上下文(Context):这是存储过滤器配置和内部状态的结构。它包含关于过滤器如何处理数据的信息。

过滤器的数据流模型

  • 在FFmpeg中,数据(无论是音频还是视频)都是以帧的形式传输的。过滤器读取输入帧,进行处理,然后生成输出帧。

2. 过滤器的生命周期

创建过滤器

  • 使用avfilter_graph_create_filter函数在给定的过滤器图中为指定的过滤器名称创建一个新的过滤器上下文。

过滤器的配置与初始化

  • 一旦过滤器被创建就需要被配置。这通常涉及设置输入和输出链接,以及其他任何特定于过滤器的选项。

数据处理与释放资源

  • 当所有的配置都完成后,就可以开始发送帧到过滤器进行处理了。处理完成后使用avfilter_free释放过滤器占用的资源。

3. 过滤器的链接与交互

如何连接多个过滤器

  • 过滤器可以通过“过滤器链”连接在一起。例如一个视频流可以首先被缩放,然后再进行颜色调整。这两个过滤器会形成一个过滤器链。

数据在过滤器间的传递

  • 数据从一个过滤器的输出链接传递到下一个过滤器的输入链接。这是通过使用av_buffersink_get_frameav_buffersrc_add_frame等函数完成的。

4. 主要结构体与API

结构体

1. AVFilterContext

代表一个过滤器实例的结构体。

主要字段

  • inputs/outputs (类型:AVFilterLink **)
    • 描述:指向输入和输出链接的指针数组。描述了数据如何在过滤器间流动。
  • priv_data (类型:void *)
    • 描述:用于存储特定过滤器的私有数据。其具体内容取决于特定的过滤器实现。

2. AVFilterLink

代表过滤器之间的连接,负责传递数据。

主要字段

  • src/dst (类型:AVFilterContext *)
    • 描述:指向数据的来源和目的地过滤器的指针。src表示源过滤器,dst表示目标过滤器。
  • type (类型:AVMediaType)
    • 描述:媒体的类型,例如:视频(AVMEDIA_TYPE_VIDEO)、音频(AVMEDIA_TYPE_AUDIO)等。

3. AVFilterPad

描述过滤器的输入或输出的功能。

主要字段

  • name (类型:const char *)
    • 描述:Pad的名称。通常用于日志和错误消息中。
  • type (类型:AVMediaType)
    • 描述:媒体的类型,例如:视频、音频等。
  • filter_frame (类型:int (*)(AVFilterLink *, AVFrame *))
    • 描述:一个回调函数,用于处理通过该pad传递的帧。

4. AVFilterGraph

代表一个过滤器图,其中包含一系列的过滤器和它们之间的连接。

主要字段

  • filters (类型:AVFilterContext **)
    • 描述:指向过滤器上下文的指针数组,表示在该图中的所有过滤器。
  • sink_links (类型:AVFilterLink **)
    • 描述:指向输出链接的指针数组,这些链接连接到sink过滤器。

主要API解释

1. avfilter_graph_create_filter

创建并初始化新的过滤器上下文。

参数

  • graph (类型:AVFilterGraph *)
    • 描述:包含所有过滤器实例的过滤器图。
    • 来源:通过avfilter_graph_alloc或其他相关方法获得。
  • filter (类型:const AVFilter *)
    • 描述:需要创建的过滤器的类型。
    • 来源:通过avfilter_get_by_name或其他相关方法获得。
  • name (类型:const char *)
    • 描述:过滤器实例的名称,主要用于日志和错误消息中。
    • 来源:用户自定义。

2. av_buffersink_get_frame

从sink过滤器中获取一个帧。

参数

  • ctx (类型:AVFilterContext *)
    • 描述:sink过滤器的上下文。
    • 来源avfilter_graph_create_filter或其他相关方法获得的过滤器上下文。
  • frame (类型:AVFrame *)
    • 描述:存储从sink获取的帧的结构。
    • 来源:通常通过av_frame_alloc分配。

3. av_buffersrc_add_frame

向source过滤器添加一个帧。

参数

  • ctx (类型:AVFilterContext *)
    • 描述:source过滤器的上下文。
    • 来源avfilter_graph_create_filter或其他相关方法获得的过滤器上下文。
  • frame (类型:AVFrame *)
    • 描述:需要添加到source的帧。
    • 来源:从解码器或其他源获取的帧。

4. avfilter_init_str

使用给定的参数字符串初始化过滤器上下文。

参数

  • ctx (类型:AVFilterContext *)
    • 描述:需要初始化的过滤器上下文。
    • 来源avfilter_graph_create_filter或其他相关方法获得。
  • args (类型:const char *)
    • 描述:用于初始化过滤器的参数字符串。
    • 来源:用户自定义或其他配置来源。

视频过滤器实现

#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>

int main(int argc, char* argv)
{
    int ret = 0;
    // 打开输入的YUV文件
    FILE* inFile = NULL;
    const char* inFileName = "768x320.yuv";
    fopen_s(&inFile, inFileName, "rb+");
    if (!inFile) {
        printf("Fail to open file\n");
        return -1;
    }

    // 输入视频的宽度和高度
    int in_width = 768;
    int in_height = 320;
    // 打开输出的YUV文件
    FILE* outFile = NULL;
    const char* outFileName = "out_crop_vfilter.yuv";
    fopen_s(&outFile, outFileName, "wb");
    if (!outFile) {
        printf("Fail to create file for output\n");
        return -1;
    }

    // 注册所有已编译的过滤器
    avfilter_register_all();

    // 创建一个过滤器图,并验证是否成功创建
    AVFilterGraph* filter_graph = avfilter_graph_alloc();
    if (!filter_graph) {
        printf("Fail to create filter graph!\n");
        return -1;
    }

    // 创建并初始化源过滤器,此过滤器从程序中接收YUV数据
    char args[512];
    sprintf(args,
        "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
        in_width, in_height, AV_PIX_FMT_YUV420P,
        1, 25, 1, 1);
    AVFilter* bufferSrc = avfilter_get_by_name("buffer");   // AVFilterGraph的输入源
    AVFilterContext* bufferSrc_ctx;
    ret = avfilter_graph_create_filter(&bufferSrc_ctx, bufferSrc, "in", args, NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create filter bufferSrc\n");
        return -1;
    }

    // 创建并初始化buffersink过滤器,此过滤器用于从过滤器图中获取处理后的视频帧
    AVBufferSinkParams *bufferSink_params;
    AVFilterContext* bufferSink_ctx;
    AVFilter* bufferSink = avfilter_get_by_name("buffersink");
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
    bufferSink_params = av_buffersink_params_alloc();
    bufferSink_params->pixel_fmts = pix_fmts;
    ret = avfilter_graph_create_filter(&bufferSink_ctx, bufferSink, "out", NULL,
                                       bufferSink_params, filter_graph);
    if (ret < 0) {
        printf("Fail to create filter sink filter\n");
        return -1;
    }

    // 创建并初始化split过滤器,它将一个视频流分为两个输出
    AVFilter *splitFilter = avfilter_get_by_name("split");
    AVFilterContext *splitFilter_ctx;
    ret = avfilter_graph_create_filter(&splitFilter_ctx, splitFilter, "split", "outputs=2",
                                       NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create split filter\n");
        return -1;
    }

    // 创建并初始化crop过滤器,用于裁剪视频帧
    AVFilter *cropFilter = avfilter_get_by_name("crop");
    AVFilterContext *cropFilter_ctx;
    ret = avfilter_graph_create_filter(&cropFilter_ctx, cropFilter, "crop",
                                       "out_w=iw:out_h=ih/2:x=0:y=0", NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create crop filter\n");
        return -1;
    }

    // 创建并初始化vflip过滤器,用于垂直翻转视频帧。
    AVFilter *vflipFilter = avfilter_get_by_name("vflip");
    AVFilterContext *vflipFilter_ctx;
    ret = avfilter_graph_create_filter(&vflipFilter_ctx, vflipFilter, "vflip", NULL, NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create vflip filter\n");
        return -1;
    }

    // 创建并初始化overlay过滤器,用于叠加两个视频帧。
    AVFilter *overlayFilter = avfilter_get_by_name("overlay");
    AVFilterContext *overlayFilter_ctx;
    ret = avfilter_graph_create_filter(&overlayFilter_ctx, overlayFilter, "overlay",
                                       "y=0:H/2", NULL, filter_graph);
    if (ret < 0) {
        printf("Fail to create overlay filter\n");
        return -1;
    }

    // 源过滤器连接到分裂过滤器
    ret = avfilter_link(bufferSrc_ctx, 0, splitFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link src filter and split filter\n");
        return -1;
    }

    // 分裂过滤器的第一个输出连接到叠加过滤器的主输入
    ret = avfilter_link(splitFilter_ctx, 0, overlayFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link split filter and overlay filter main pad\n");
        return -1;
    }

    // 分裂过滤器的第二个输出连接到裁剪过滤器
    ret = avfilter_link(splitFilter_ctx, 1, cropFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link split filter's second pad and crop filter\n");
        return -1;
    }

    // 裁剪过滤器连接到垂直翻转过滤器
    ret = avfilter_link(cropFilter_ctx, 0, vflipFilter_ctx, 0);
    if (ret != 0) {
        printf("Fail to link crop filter and vflip filter\n");
        return -1;
    }

    // 垂直翻转过滤器连接到叠加过滤器的第二个输入
    ret = avfilter_link(vflipFilter_ctx, 0, overlayFilter_ctx, 1);
    if (ret != 0) {
        printf("Fail to link vflip filter and overlay filter's second pad\n");
        return -1;
    }

    // 叠加过滤器连接到接收过滤器
    ret = avfilter_link(overlayFilter_ctx, 0, bufferSink_ctx, 0);
    if (ret != 0) {
        printf("Fail to link overlay filter and sink filter\n");
        return -1;
    }
    
    // 检查
    ret = avfilter_graph_config(filter_graph, NULL);
    if (ret < 0) {
        printf("Fail in filter graph\n");
        return -1;
    }
  
    // 将过滤器图的结构保存到一个文本文件中
    char *graph_str = avfilter_graph_dump(filter_graph, NULL);
    FILE* graphFile = NULL;
    fopen_s(&graphFile, "graphFile.txt", "w");  // 打印filtergraph的具体情况
    fprintf(graphFile, "%s", graph_str);
    av_free(graph_str);
  
    // 为输入和输出视频帧分配和初始化空间
    AVFrame *frame_in = av_frame_alloc();
    unsigned char *frame_buffer_in = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, in_width, in_height, 1));
    av_image_fill_arrays(frame_in->data, frame_in->linesize, frame_buffer_in,
        AV_PIX_FMT_YUV420P, in_width, in_height, 1);
    AVFrame *frame_out = av_frame_alloc();
    unsigned char *frame_buffer_out = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, in_width, in_height, 1));
    av_image_fill_arrays(frame_out->data, frame_out->linesize, frame_buffer_out,
        AV_PIX_FMT_YUV420P, in_width, in_height, 1);

    // 初始化输入帧的相关属性
    frame_in->width = in_width;
    frame_in->height = in_height;
    frame_in->format = AV_PIX_FMT_YUV420P;
    uint32_t frame_count = 0;
    while (1) {
        // 从输入文件中读取YUV数据并填充到输入帧
        if (fread(frame_buffer_in, 1, in_width*in_height * 3 / 2, inFile) != in_width*in_height * 3 / 2) {
            break;
        }
        
        //input Y,U,V
        frame_in->data[0] = frame_buffer_in;
        frame_in->data[1] = frame_buffer_in + in_width*in_height;
        frame_in->data[2] = frame_buffer_in + in_width*in_height * 5 / 4;

        // 将输入帧添加到过滤器图
        if (av_buffersrc_add_frame(bufferSrc_ctx, frame_in) < 0) {
            printf("Error while add frame.\n");
            break;
        }
        // 从过滤器图中获取处理后的输出帧
        ret = av_buffersink_get_frame(bufferSink_ctx, frame_out);
        if (ret < 0)
            break;
            
        // 如果输出格式是YUV420P,则将其写入输出文件
        if (frame_out->format == AV_PIX_FMT_YUV420P) {
            for (int i = 0; i < frame_out->height; i++) {
                fwrite(frame_out->data[0] + frame_out->linesize[0] * i, 1, frame_out->width, outFile);
            }
            for (int i = 0; i < frame_out->height / 2; i++) {
                fwrite(frame_out->data[1] + frame_out->linesize[1] * i, 1, frame_out->width / 2, outFile);
            }
            for (int i = 0; i < frame_out->height / 2; i++) {
                fwrite(frame_out->data[2] + frame_out->linesize[2] * i, 1, frame_out->width / 2, outFile);
            }
        }
        ++frame_count;
        if(frame_count % 25 == 0)
            printf("Process %d frame!\n",frame_count);
        av_frame_unref(frame_out);
    }
 
    fclose(inFile);
    fclose(outFile);
    av_frame_free(&frame_in);
    av_frame_free(&frame_out);
    avfilter_graph_free(&filter_graph); // 内部去释放AVFilterContext产生的内存
    return 0;

}

音频过滤器实现

下面是一个完整示例,使用FFmpeg库的音频过滤功能调整音频的音量。 这个代码的主要目的是调整输入音频文件的音量并将其保存到输出文件中

#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersrc.h>
#include <libavfilter/buffersink.h>

int main(int argc, char **argv) {
    if (argc < 3) {
        printf("使用方法: %s <输入文件> <输出文件>\n", argv[0]);
        return -1;
    }

    const char *input_filename = argv[1];
    const char *output_filename = argv[2];

    // 注册所有组件
    av_register_all();
    avfilter_register_all();

    AVFormatContext *fmt_ctx = NULL;
    // 打开输入文件
    if (avformat_open_input(&fmt_ctx, input_filename, NULL, NULL) < 0) {
        printf("无法打开输入文件\n");
        return -1;
    }

    // 获取流信息
    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
        printf("无法找到流信息\n");
        return -1;
    }

    // 寻找音频流
    int audio_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audio_stream_index < 0) {
        printf("无法找到音频流\n");
        return -1;
    }

    AVStream *audio_stream = fmt_ctx->streams[audio_stream_index];
    AVCodecContext *codec_ctx = audio_stream->codec;

    // 设置过滤器图
    AVFilterGraph *filter_graph = avfilter_graph_alloc();
    AVFilterContext *abuffer_ctx, *volume_ctx, *abuffersink_ctx;

    // 创建abuffer过滤器上下文
    char args[512];
    snprintf(args, sizeof(args), "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64,
        codec_ctx->time_base.num, codec_ctx->time_base.den, codec_ctx->sample_rate,
        av_get_sample_fmt_name(codec_ctx->sample_fmt), codec_ctx->channel_layout);
    avfilter_graph_create_filter(&abuffer_ctx, avfilter_get_by_name("abuffer"), "src", args, NULL, filter_graph);

    // 创建音量过滤器上下文
    avfilter_graph_create_filter(&volume_ctx, avfilter_get_by_name("volume"), "volume", "volume=2.0", NULL, filter_graph);

    // 创建abuffersink过滤器上下文
    avfilter_graph_create_filter(&abuffersink_ctx, avfilter_get_by_name("abuffersink"), "sink", NULL, NULL, filter_graph);

    // 将过滤器链接在一起
    avfilter_link(abuffer_ctx, 0, volume_ctx, 0);
    avfilter_link(volume_ctx, 0, abuffersink_ctx, 0);

    // 打开输出文件并写入头部
    AVFormatContext *out_fmt_ctx = NULL;
    avformat_alloc_output_context2(&out_fmt_ctx, NULL, NULL, output_filename);
    AVStream *out_stream = avformat_new_stream(out_fmt_ctx, NULL);
    avcodec_parameters_copy(out_stream->codecpar, audio_stream->codecpar);

    avformat_write_header(out_fmt_ctx, NULL);

    AVPacket pkt;
    // 读取并处理每个数据包
    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        if (pkt.stream_index == audio_stream_index) {
            AVFrame *frame = av_frame_alloc();
            int got_frame;
            avcodec_decode_audio4(codec_ctx, frame, &got_frame, &pkt);
            if (got_frame) {
                av_buffersrc_add_frame(abuffer_ctx, frame);
                AVFrame *filt_frame = av_frame_alloc();
                av_buffersink_get_frame(abuffersink_ctx, filt_frame);
                // 这里可以将filt_frame编码并复用到输出文件
                av_frame_free(&filt_frame);
            }
            av_frame_free(&frame);
        }
        av_packet_unref(&pkt);
    }

    // 写入输出文件的尾部并关闭
    avformat_write_trailer(out_fmt_ctx);
    avformat_close_input(&fmt_ctx);
    avformat_free_context(out_fmt_ctx);
    avfilter_graph_free(&filter_graph);

    return 0;
}


附录

1 buffer 过滤器:

  • buffer过滤器是一个特殊的过滤器,它用于将外部的媒体数据(如视频或音频帧)注入到过滤器图中。换句话说,当想要将一个视频帧或音频帧发送到AVFilterGraph进行处理时,首先需要通过buffer过滤器将其放入过滤器链。
  • 对于音频,相应的过滤器是abuffer
  • 它通常作为过滤器图的源节点(即图的开始),确保原始数据可以被后续的过滤器处理。

2 命令行使用过滤器:

filter的语法和结构有点复杂,因为它支持链式处理和复杂的过滤器图形。

以下是一些基本过滤器语法和概念:

  1. 过滤器链(Filterchain)

    • 这是由一系列过滤器组成的链条,其中每个过滤器接收一个输入,进行处理,然后产生一个输出。
    • 示例:[input]scale=640:480,crop=640:240[vout]。这里,视频首先被缩放,然后被裁剪。
  2. 过滤器图(Filtergraph)

    • 它是一组过滤器链,其中链之间可以共享输入或输出。
    • 示例:[in1]scale=640:480[out1];[in2]crop=640:240[out2]。这表示两个独立的过滤器链,一个进行缩放,另一个进行裁剪。
  3. 命名输入/输出标签

    • 可以为过滤器图中的输入和输出定义标签,使其更具有描述性。
    • 如:[input]...[vout],这里inputvout是命名标签。
  4. 连接多个过滤器

    • 过滤器可以使用,;连接。,表示在同一过滤器链中连接过滤器,而;表示开始新的过滤器链。
  5. 参数设置

    • 过滤器的参数通常使用=进行设置,多个参数之间使用:分隔。
    • 示例:scale=w=640:h=480atempo=1.5
  6. 复杂的过滤器图

    • 可以使用多个输入和输出构建复杂的过滤器图。
    • 示例:[in1][in2]overlay[out]。这将两个输入流叠加在一起。
  7. 为过滤器指定参数

    • 过滤器可以接受参数以控制其行为。
    • 示例:scale=1920:1080。这将视频缩放到1920x1080的大小。

使用ffmpeg命令行工具应用过滤器时,-vf-af选项通常用于指定视频和音频过滤器,分别表示视频过滤器和音频过滤器。

一个复合使用的例子,主要目的是将原始视频分割并在上半部分加上其下半部分的翻转

ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

1. 基础命令结构:

ffmpeg -i INPUT -vf "FILTER_STRING" OUTPUT
  • ffmpeg: FFmpeg工具的命令行调用。
  • -i INPUT: 输入文件,代表想处理的媒体文件。
  • -vf "FILTER_STRING": video filter的简写。这里定义了一个过滤器图(Filtergraph)字符串来处理视频流。
  • OUTPUT: 输出文件,代表处理后的媒体文件。

2. 解释FILTER_STRING:

split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2

(1) split [main][tmp]:

  • split: 分割过滤器将视频流分为两个完全相同的输出。
  • [main][tmp]: 这定义了split的输出标签。第一个输出标记为main,第二个标记为tmp

(2) [tmp] crop=iw:ih/2:0:0, vflip [flip]:

  • [tmp]: 这是从上一个过滤器中得到的标签,即我们想处理的视频流部分。
  • crop=iw:ih/2:0:0: 裁剪过滤器。参数iw:ih/2:0:0的意思是:宽度为原始宽度(iw),高度为原始高度的一半(ih/2),起始点的x和y坐标都是0。
  • ,: 用于链接两个过滤器,代表Filterchain。
  • vflip: 垂直翻转过滤器,它会将视频流上下翻转。
  • [flip]: 这是vflip过滤器的输出标签。

(3) [main][flip] overlay=0:H/2:

  • [main][flip]: 这些是输入标签,即将要处理的两个视频流。
  • overlay=0:H/2: 叠加过滤器。参数0:H/2表示第二个视频flip将叠加到第一个视频main上,起始坐标为(0, H/2),其中H是原始视频的高度。