学习ffmpeg给视频添加图片水印

42 阅读8分钟

背景

时间:2024.09.25

项目中需要给播放的视频添加水印,工程本身有集成 ijkplayer 所以想利用 ffmpeg 来操作添加水印。默认 ffmpeg 配置中是不包含 h264编码器的,需要手动给 ffmpeg 添加h264编码器,如何集成h264编码器,可以看我这篇文章。

在网上找了很多资料,大部分是通过命令行操作的,通过 ffmpeg 的sdk编码方式来添加水印的文章不是很多,但也找到了几篇文章,是以代码方式添加水印的,文章以 qt 或者 c++语言的居多,将代码拷贝到自己的工程里面,修修改改也能跑起来,但是效果不是很好,很多细节没有处理到位。

所以就想着自己写一个添加水印的 demo,ffmpeg 我也是才学习不久,算是刚刚摸到门把手的小白,可能我写的代码里面也有很多不完善的地方,如果有看到哪些错误的地方,希望能给我留言或者添加我的 vx: isMeJson 进行指正。

工程环境

ijkplayer 的 ffmpeg 的版本是 3.4 ,我将 3.4 版本的源码下载到本地进行编译,发现编译不过,所以就用了 4.3.2 的版本。编译 ffmpeg4.3.2 版本参考小码哥的这篇文章,我编译的时候没有任何报错。

通过编译命令,将编译好的 lib 库,放在了 /usr/local/ffmpeg4_32 目录下,编译的时候可能会有权限报错,建议安装到/opt/local/ffmpeg4_32 ,ffmpeg4_32 是我定义的文件名,为了跟其他版本的 ffmpeg 进行区分。

./configure --prefix=/usr/local/ffmpeg4_32 --enable-shared --disable-static --enable-gpl  --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265

这是我编译好的配置信息

ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
  built with Apple clang version 15.0.0 (clang-1500.3.9.4)
  configuration: --prefix=/usr/local/ffmpeg4_32 --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265 --enable-filter=delogo --disable-optimizations --enable-libspeex --enable-videotoolbox --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --cc=clang --host-cflags=
  libavutil      56. 51.100 / 56. 51.100
  libavcodec     58. 91.100 / 58. 91.100
  libavformat    58. 45.100 / 58. 45.100
  libavdevice    58. 10.100 / 58. 10.100
  libavfilter     7. 85.100 /  7. 85.100
  libswscale      5.  7.100 /  5.  7.100
  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100

编译好大概这样。

image-20240925182139064.png

我是用 Xcode 创建的 c++环境,用 Xcode 创建了一个命令行工程。

然后将 ffmpeg 编译好的 libinclude拷贝到了工程目录下。然后配置.a静态库路径 Libary Search Paths 和ffmpeg的头文件 User Header Search Paths。 同时将.a静态库拷贝到 Frameworks and Libraries下。我是将 .a 和 .dylib一股脑的拷贝到工程下,只有.a貌似会报错,但是手机上的 ffmpeg 是没有报错的。

image-20240925182742329.png

添加水印思路

  1. 输入文件解封装,创建文件上下文,找到视频流,并初始化视频解码器。
  2. 初始化输出数据编码上下文,通过找到的视频流,拷贝参数到编码器。
  3. 初始化输出文件格式上下文,我输出的是 ts 文件,所以用的 mpegts,并与输出文件路径进行 IO 绑定。
  4. 再初始化 filter过滤器,里面有两个 filter,一个是 buffer, buffersink,我的理解filter就像一个管道,将处理的数据从一个管道流向下一个管道。过滤器这块了解的不多,是将网上的代码给摘抄拼合的。
  5. 处理视频流,将视频流解封装,然后通过过滤器添加水印,最终将视频原始数据进行 h264 编码。
  6. 最后冲刷解码器和编码器(这一步很重要,如果不做会缺失数据)。
  7. 释放创建的对象。

大概就是这么几步,或许画个图会更直观一些。

实现代码

如果不想看部分代码,可以点击 代码文件下载 进行整体代码浏览。

  1. 输入文件上下文创建
static int open_input_file(const char *filename) {
    int ret = -1;
    AVCodec *dec = nullptr;
    
    // 1.文件解封装,打开文件上下文
    ret = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "avformat_open_input error");
        return ret;
    }
    
    //2.找到视频流
    ret = avformat_find_stream_info(fmt_ctx, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "avformat_find_stream_info error");
        return ret;
    }
    
    // 3.找到视频流,并获取解码器
    ret = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &dec, 0);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "av_find_best_stream error----> 没有视频流");
        return ret;
    }
    // 获取到视频的下标
    v_stream_index = ret;
    
    // 4.找到流之后,创建解码上下文,并打开解码器
    // 解码上下文
    dec_ctx = avcodec_alloc_context3(dec);
    if (!dec_ctx) {
        // 内存不足,创建失败.
        return AVERROR(ENOMEM);
    }
    
    // 获取上下文,拷贝 parameter 信息
    ret = avcodec_parameters_to_context(dec_ctx, fmt_ctx->streams[v_stream_index]->codecpar);
    if (ret) {
        av_log(nullptr, AV_LOG_ERROR, "avcodec_parameters_to_context error");
        return ret;
    }
    
    // 打开解码器
    ret = avcodec_open2(dec_ctx, dec, nullptr);
    if (ret) {
        av_log(nullptr, AV_LOG_ERROR, "avcodec_open2 error");
        return ret;
    }
    return 0;
}
  1. 输出数据上下文创建
static int init_outfmt_context(const char *filename) {
    // 输出格式上下文,绑定视频流和音频流等
    // 输出为 ts 文件数据
    int ret = avformat_alloc_output_context2(&out_fmt_ctx, nullptr, "mpegts", filename);
    if (ret < 0) {
        FFM_AV_LOG("avformat_alloc_output_context2 --------->out_fmt_ctx error");
        return ret;
    }
    // 初始化 stream_map.
    // 有几条流就初始化几条
    stream_map = (int *)av_calloc(fmt_ctx->nb_streams, sizeof(int));
    int stream_idx = 0;
    for (int i = 0 ; i < fmt_ctx->nb_streams; i++) {
        // 取出每条流,判断流的类型,进行过滤
        AVStream *inStream = fmt_ctx->streams[i];
        AVCodecParameters *inCodecPar = inStream->codecpar;
        if (inCodecPar->codec_type != AVMEDIA_TYPE_VIDEO
            && inCodecPar->codec_type != AVMEDIA_TYPE_AUDIO
            && inCodecPar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
            // 不是视频\音频\字幕流都过滤掉
            stream_map[i]  = -1;
            continue;
        }
        stream_map[i] = stream_idx++;
        // 创建新的流
        AVStream *outStream = avformat_new_stream(out_fmt_ctx, nullptr);
        if (!outStream) {
            FFM_AV_LOG("avformat_new_stream --------->error");
            return  -1;
        }
        // 拷贝流参数
        avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
        outStream->codecpar->codec_tag = 0;
        
    }
    
    //打开输出IO 绑定上下文
    ret = avio_open(&out_fmt_ctx->pb, filename, AVIO_FLAG_WRITE);
    if (ret < 0) {
        FFM_AV_LOG("avio_open ---------> error");
        return ret;
    }
    //写多媒体文件头到目标的文件
    ret = avformat_write_header(out_fmt_ctx, NULL);
    if (ret < 0) {
        FFM_AV_LOG("avformat_write_header ----> error");
        return ret;
    }
    
    return 0;
}
  1. 初始化过滤器
static int init_filters(const char *filter_desc) {
    
    int ret = -1;
    
    AVFilterInOut *inputs = avfilter_inout_alloc();
    AVFilterInOut *outputs = avfilter_inout_alloc();
    
    if (!inputs || !outputs) {
        FFM_AV_LOG("avfilter_inout_alloc error");
        return AVERROR(ENOMEM);
    }
    
    graph = avfilter_graph_alloc();
    
    if (!graph) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_alloc error");
        return AVERROR(ENOMEM);
    }
    
    // 输入 buffer
    const AVFilter *bufsrc = avfilter_get_by_name("buffer");
    if (!bufsrc) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_get_by_name error --->buffer");
        return -1;
    }
    // 输出 buffersink
    const AVFilter *bufsink = avfilter_get_by_name("buffersink");
    
    if (!bufsink) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_get_by_name error --->bufsink");
        return -1;
    }
    
    enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE};
    
    char args[512] = {};
    
    AVRational time_base = fmt_ctx->streams[v_stream_index]->time_base;
    
    snprintf(args, 512, "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",dec_ctx->width,
             dec_ctx->height, dec_ctx->pix_fmt, time_base.num, time_base.den, dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den);
    // 创建输入 filter
    ret = avfilter_graph_create_filter(&buf_ctx, bufsrc, "in", args, nullptr, graph);
    if (ret) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_create_filter error----->buf_ctx");
        goto __ERROR;
    }
    
    // 创建输出 filter
    ret = avfilter_graph_create_filter(&bufsink_ctx, bufsink, "out", nullptr, nullptr, graph);
    if (ret) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_create_filter error------>bufsink_ctx");
        goto __ERROR;
    }
    
    av_opt_set_int_list(bufsink_ctx, "pix_fmts", pix_fmts, AV_PIX_FMT_NONE, AV_OPT_SEARCH_CHILDREN);
    
    
    inputs->name = av_strdup("out");
    inputs->filter_ctx = bufsink_ctx;
    inputs->pad_idx = 0;
    inputs->next = nullptr;
    
    outputs->name = av_strdup("in");
    outputs->filter_ctx = buf_ctx;
    outputs->pad_idx = 0;
    outputs->next = nullptr;
    
    // 创建 filter 并且添加到graph 描述符
    ret = avfilter_graph_parse_ptr(graph, filter_desc, &inputs, &outputs, nullptr);
    if (ret < 0) {
        av_log(nullptr, AV_LOG_ERROR, "avfilter_graph_parse_ptr error");
        goto __ERROR;
    }
    ret = avfilter_graph_config(graph, nullptr);
    if (ret < 0) {
        FFM_AV_LOG("avfilter_graph_config ERROR")
        goto __ERROR;
    }
    return 0;
__ERROR:
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);
    
    return ret;
}
  1. 对视频流做解码、添加水印、编码操作
// 获取解码后的原始数据 PCM/YUV
    // 将数据添加到 buffer filter 中
    // 从 buffer sink 中获取处理好的数据
    
    AVPacket packet;
    AVFrame *frame = nullptr;
    AVFrame *filt_frame = nullptr;
    frame = av_frame_alloc();
    filt_frame = av_frame_alloc();
    if (!frame || !filt_frame) {
        FFM_AV_LOG("av_frame_alloc----------->ERROR");
        return -1;
    }
    int video_index = 0;
    
    // 读取数据
    while (true) {
        ret = av_read_frame(fmt_ctx, &packet);
        if (ret == AVERROR_EOF) {
            // 读到文件尾部
            break;
        }else if (ret == 0) {
            if (stream_map[packet.stream_index] < 0) {
                // 不是需要的数据,丢弃
                av_packet_unref(&packet);
                continue;
            }
            printf("mhy输入-------------------------------------------%lld\n", packet.dts);
​
            // 修改 packet 的 index 为输出上下文中的顺序
            int orgStreamIndex = packet.stream_index;
            // 改为输出文件上下文中的流下标
            packet.stream_index = stream_map[packet.stream_index];
            AVStream *inStream, *outStream;
            inStream = fmt_ctx->streams[orgStreamIndex];
            //此时 out 的stream_index已经改为了输出上下文中的顺序
            outStream = out_fmt_ctx->streams[packet.stream_index];
            // 重新设置流的 pts dts duration
            av_packet_rescale_ts(&packet, inStream->time_base, outStream->time_base);
            packet.pos = -1;
            
            if (orgStreamIndex == v_stream_index) {
                // 视频流
                // 将 pkt 数据送到解码器解码
//                printf("mhy输入----------------------%d \n", video_index++);
                ret = avcodec_send_packet(dec_ctx, &packet);
                if (ret < 0) {
                    FFM_AV_LOG("avcodec_send_packet ----pkt error");
                    goto __ERROR;
                }
                ret = decode_frame_and_filter(frame, filt_frame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    // 读完,或没有数据了,再读取
                    continue;
                }
                if (ret < 0) {
                    
                    FFM_AV_LOG("decode_frame_and_filter ---------- error");
                    goto __ERROR;
                }
            }else {
​
                // 其他流,不做处理,直接写入本地
                printf("mhy输出转换1-------------------------------------------%lld\n", packet.dts);
                av_interleaved_write_frame(out_fmt_ctx, &packet);
            }
            av_packet_unref(&packet);
        }else {
            continue;
        }
        
    }
    
    // 刷新解码缓冲区
    avcodec_send_packet(dec_ctx, nullptr);
    decode_frame_and_filter(frame, filt_frame);
    // 刷新编码缓冲区
    do_frame(nullptr);
    //添加数据结束字符
    ret = av_write_trailer(out_fmt_ctx);