FFmpeg过滤器实战:水印处理

过滤器实战:水印处理

这个 Demo 展示了如何使用 FFmpeg 的滤镜系统对视频进行处理,实现将视频上半部分倒置并与下半部分叠加的效果。下面我将详细解析这个代码。

程序整体结构

整个程序完成以下几个步骤:

  1. 初始化滤镜系统
  2. 构建滤镜图(Filter Graph)
  3. 处理输入视频帧
  4. 输出处理后的视频帧
  5. 清理资源

滤镜图设计分析

plain 复制代码
buffer_src → split → overlay (主输出)
               \
                → crop → vflip → overlay (副输入)

代码分析

文件输入初始化

c 复制代码
int ret = 0;

// 出入文件和输出文件
FILE* in_file = NULL;
FILE* out_file = NULL;

const char* in_file_name = NULL;
const char* out_file_name = NULL;

if (argc <= 3) {
    printf("Usage ./ffmpeg_watermark [in_file] [out_file]\n");
    return -1;
}

in_file_name = argv[1];
out_file_name = argv[2];

in_file = fopen(in_file_name, "rb+");
if (!in_file) {
    printf("Failed to open in_file");
    return -1;
}

out_file = fopen(out_file_name, "wb");
if (!out_file) {
    printf("Failed to open out_file");
    return -1;
}

int width = 768;
int height = 320;
  • 我们定义一些出入文件和输出文件的参数,比如文件指针和文件路径。
  • 最后的 widthheight是我们输入视频流 frame 的宽和高。

过滤器图初始化(管理所有滤镜)

c 复制代码
// 注册所有内置滤镜
avfilter_register_all();
// 初始化并分配一个空的滤镜图结构
AVFilterGraph* filter_graph = avfilter_graph_alloc();

if (!filter_graph) {
    printf("avfilter_graph_alloc failed\n");
    return -1;
}

char args[512];
sprintf(args, "video_sizeo=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
width, height, AV_PIX_FMT_YUV420P, 1, 25, 1, 1);
  • 使用 avfilter_graph_alloc函数来初始化一个空的滤镜图结构,滤镜图结构会统一管理所有的滤镜,所以后续的所有滤镜都需要注册到滤镜图上

滤镜初始化(具体的滤镜)

c 复制代码
// 获取名为buffer的滤镜,用于像滤镜图输入原始数据
// 返回值AVFilter指向FFmpeg内置的buffer滤镜
AVFilter* buffer_src = avfilter_get_by_name("buffer");
AVFilterContext* buffer_src_ctx;
ret = avfilter_graph_create_filter(&buffer_src_ctx, buffer_src, "in", args, NULL, filter_graph);
if (ret < 0) {
    printf("avfilter_graph_create_filter buffer failed\n");
    return -1;
}

// buffersink滤镜 作为滤镜的输出节点,用于从滤镜中提取处理后的帧(如缩放/裁剪后的视频帧)
AVBufferSinkParams* buffer_sink_params; // 配置输出帧的格式(如像素格式、音频采样格式等)
AVFilterContext* buffer_sink_ctx;
// 获取buffersink滤镜
AVFilter* buffer_sink_filter = avfilter_get_by_name("buffersink");
enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE}; // 指定允许的输出像素格式列表
buffer_sink_params->pixel_fmts = pix_fmts;
ret = avfilter_graph_create_filter(&buffer_sink_ctx, buffer_sink_filter, "out", NULL, buffer_sink_params, filter_graph);

if (ret < 0) {
    printf("avfilter_graph_create_filter buffer_sink_params failed\n");
    return -1;
}

AVFilter* split_filter = avfilter_get_by_name("split");
AVFilterContext* split_filter_ctx;
ret = avfilter_graph_create_filter(&split_filter_ctx, split_filter, "split", "outputs=2", NULL, filter_graph);

if (ret < 0) {
    printf("avfilter_graph_create_filter split_filter failed\n");
    return -1;
}

/*
out_w=iw:输出宽度=输入宽度
out_h=ih/2 输出高度=输入高度的一半
x=0 从x=0位置开始裁剪
y=0 从y=0位置开始裁剪
*/
AVFilter* crop_filter = avfilter_get_by_name("crop");
AVFilterContext* crop_filter_ctx;
ret = avfilter_graph_create_filter(&crop_filter_ctx, crop_filter, "crop", "out_w=iw:out_h=ih/2:x=0:y=0", NULL, filter_graph);

if (ret < 0) {
    printf("avfilter_graph_create_filter crop_filter failed\n");
    return -1;
}

AVFilter* vfilp_filter = avfilter_get_by_name("vflip");
AVFilterContext* vflip_filter_ctx;
ret = avfilter_graph_create_filter(&vflip_filter_ctx, vfilp_filter, "vflip", NULL, NULL, filter_graph);

if (ret < 0) {
    printf("avfilter_graph_create_filter vflip_filter failed\n");
    return -1;
}

AVFilter* overlay_filter = avfilter_get_by_name("overlay");
AVFilterContext* overlay_filter_ctx;
ret = avfilter_graph_create_filter(&overlay_filter_ctx, overlay_filter, "overlay", "y=0:H/2", NULL, filter_graph);

if (ret < 0) {
    printf("avfilter_graph_create_filter overlay_filter failed\n");
    return -1;
}
  • split滤镜的作用如下
    • 将单个输入流复制为多个完全相同的输出流
    • 每个输出流包含与输入流完全相同的内容
    • 常用于需要将同一个视频源用于多个不同处理流程的场景
  • crop是裁剪滤镜,作用如下:
    • 从输入视频中裁剪出指定的矩形区域
    • 可以精确控制裁剪的位置和尺寸
    • 支持动态表达式计算裁剪参数
  • vflip是垂直翻转滤镜,作用如下:
    • 将输入视频帧沿水平轴(X 轴)进行垂直翻转
    • 实现"上下颠倒"的视觉效果
    • 不改变视频的分辨率和像素合适
    • 处理效率高,适合实时应用

滤镜组合

c 复制代码
// 输入->split
ret = avfilter_link(buffer_src_ctx, 0, split_filter_ctx, 0);
if (ret != 0) {
    printf("avfilter_link buffer_src_ctx and split_filter_ctx failed\n");
    return -1;
}
// split主输出->overlay主输入
ret = avfilter_link(split_filter_ctx, 0, overlay_filter_ctx, 0);
if (ret != 0) {
    printf("avfilter_link split_filter_ctx and overlay_filter_ctx\n");
    return -1;
}
// split副输出->crop
ret = avfilter_link(split_filter_ctx, 1, crop_filter_ctx, 0);
if (ret != 0) {
    printf("avfilter_link split_filter_ctx and crop_filter_ctx\n");
    return -1;
}
// crop->vflip
ret = avfilter_link(crop_filter_ctx, 0, vflip_filter_ctx, 0);
if (ret != 0) {
    printf("avfilter_link crop_filter_ctx and vflip_filter_ctx\n");
    return -1;
}
// vflip->overlay副输入
ret = avfilter_link(vflip_filter_ctx, 0, overlay_filter_ctx, 1);
if (ret != 0) {
    printf("avfilter_link vflip_filter_ctx and overlay_filter_ctx\n");
    return -1;
}
// overlay->输出
ret = avfilter_link(overlay_filter_ctx, 0, buffer_sink_ctx, 0);
if (ret != 0) {
    printf("avfilter_link overlay_filter_ctx and buffer_sink_ctx\n");
    return -1;
}

输出 filter_graph 到文件

c 复制代码
char* graph_str = avfilter_graph_dump(filter_graph, NULL);
FILE* graph_file = NULL;
graph_file = fopen("graph_file.txt", "w");
fprintf(graph_file, "%s", graph_str);
av_free(graph_str);

输出结果如下:

plain 复制代码
+----------+
|    in    |default--[768x320 1:1 yuv420p]--split:default
| (buffer) |
+----------+

                                               +--------------+
overlay:default--[768x320 1:1 yuv420p]--default|     out      |
                                               | (buffersink) |
                                               +--------------+

                                          +---------+
in:default--[768x320 1:1 yuv420p]--default|  split  |output0--[768x320 1:1 yuv420p]--overlay:main
                                          | (split) |output1--[768x320 1:1 yuv420p]--crop:default
                                          +---------+

                                             +--------+
split:output1--[768x320 1:1 yuv420p]--default|  crop  |default--[768x160 1:1 yuv420p]--vflip:default
                                             | (crop) |
                                             +--------+

                                            +---------+
crop:default--[768x160 1:1 yuv420p]--default|  vflip  |default--[768x160 1:1 yuv420p]--auto_scaler_0:default
                                            | (vflip) |
                                            +---------+

                                                      +-----------+
split:output0----------[768x320 1:1 yuv420p]------main|  overlay  |default--[768x320 1:1 yuv420p]--out:default
auto_scaler_0:default--[768x160 1:1 yuva420p]--overlay| (overlay) |
                                                      +-----------+

                                             +---------------+
vflip:default--[768x160 1:1 yuv420p]--default| auto_scaler_0 |default--[768x160 1:1 yuva420p]--overlay:overlay
                                             |    (scale)    |
                                             +---------------+

帧处理

c 复制代码
VFrame* in_frame = av_frame_alloc();
unsigned char* in_frame_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1));
av_image_fill_arrays(in_frame->data, in_frame->linesize, in_frame_buffer, AV_PIX_FMT_YUV410P, width, height, 1);

AVFrame* out_frame = av_frame_alloc();
unsigned char* out_frame_buffer = (unsigned char*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1));
av_image_fill_arrays(out_frame->data, out_frame->linesize, out_frame_buffer, AV_PIX_FMT_YUV420P, width, height, 1);

in_frame->width = width;
in_frame->height = height;
in_frame->format = AV_PIX_FMT_YUV420P;
uint32_t frame_count = 0;

while (1) {
    // 读取yuv帧数据
    if (fread(in_frame_buffer, 1, width*height*3/2, in_file) != width*height*3/2) {
        break;
    }

    // 设置帧数据指针
    in_frame->data[0] = in_frame_buffer;
    in_frame->data[1] = in_frame_buffer + width*height;
    in_frame->data[2] = in_frame_buffer + width*height*5/4;

    // 将帧送入滤镜系统
    if (av_buffersrc_add_frame(buffer_src_ctx, in_frame) < 0) {
        printf("av_buffersrc_add_frame failed\n");
        break;
    }

    // 从滤镜系统获取处理后的帧
    ret = av_buffersink_get_frame(buffer_sink_ctx, out_frame);
    if (ret < 0) {
        break;
    }

    // 写入处理后的帧
    if (out_frame->format == AV_PIX_FMT_YUV420P) {
        // 写入Y分量
        for (int i = 0; i < out_frame->height; i++) {
            fwrite(out_frame->data[0] + out_frame->linesize[0] * i, 1, out_frame->width, out_file);
        }
        // 写入U分量
        for (int i = 0; i < out_frame->height / 2; i++) {
            fwrite(out_frame->data[1] + out_frame->linesize[1] * i, 1, out_frame->width / 2, out_file);
        }
        // 写入V分量
        for (int i = 0; i < out_frame->height / 2; i++) {
            fwrite(out_frame->data[2] + out_frame->linesize[2] * i, 1, out_frame->width / 2, out_file);
        }
    }

    ++frame_count;
    if (frame_count % 25 == 0)
        printf("process %d frame\n", frame_count);
    // 释放引用,不释放frame本身
    av_frame_unref(out_frame);
}

参考资料:https://github.com/0voice

相关推荐
胖_大海_2 小时前
【FFmpeg+Surface 底层渲染,实现超低延迟100ms】
ffmpeg
冷冷的菜哥2 小时前
springboot调用ffmpeg实现对视频的截图,截取与水印
java·spring boot·ffmpeg·音视频·水印·截图·截取
进击的CJR15 小时前
redis哨兵实现主从自动切换
mysql·ffmpeg·dba
huahualaly18 小时前
重建oracle测试库步骤
数据库·oracle·ffmpeg
aqi001 天前
FFmpeg开发笔记(九十九)基于Kotlin的国产开源播放器DKVideoPlayer
android·ffmpeg·kotlin·音视频·直播·流媒体
lizongyao1 天前
FFMPEG命令行典型案例
ffmpeg
冷冷的菜哥1 天前
ASP.NET Core调用ffmpeg对视频进行截图,截取,增加水印
开发语言·后端·ffmpeg·asp.net·音视频·asp.net core
冷冷的菜哥1 天前
go(golang)调用ffmpeg对视频进行截图、截取、增加水印
后端·golang·ffmpeg·go·音视频·水印截取截图
小尧嵌入式2 天前
【基础学习七十】ffmpeg命令
c++·stm32·嵌入式硬件·ffmpeg
烧饼Fighting2 天前
统信UOS操作系统离线安装ffmpeg
开发语言·javascript·ffmpeg