【FFMpeg视频开发与应用基础】七、 调用FFMpeg SDK实现视频水印

简介: 《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDKGithub工程代码地址:FFmpeg_Tutorial视频的水印通常指附加在原始视频上的可见或者不可见的,与原始视频无直接关联的标识。

《FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK》视频教程已经在“CSDN学院”上线,视频中包含了从0开始逐行代码实现FFMpeg视频开发的过程,欢迎观看!链接地址:FFMpeg视频开发与应用基础——使用FFMpeg工具与SDK

Github工程代码地址:FFmpeg_Tutorial


视频的水印通常指附加在原始视频上的可见或者不可见的,与原始视频无直接关联的标识。通常在有线电视画面上电视台的台标以及视频网站上的logo就是典型的视频水印的应用场景。通常实现视频水印可以通过FFMpeg提供的libavfilter库实现。libavfilter库实际上实现的是视频的滤镜功能,除了水印之外,还可以实现视频帧的灰度化、平滑、翻转、直方图均衡、裁剪等操作。

我们这里实现的视频水印等操作,完全在视频像素域实现,即从一个yuv文件中读取数据到AVFrame结构,对AVFrame结构进行处理后再输出到另一个yuv文件。中间不涉及封装或编码解码等操作。

1. 解析命令行,获取输入输出文件信息

我们通过与之前类似的方式,在命令行中获取输入、输出文件名,图像宽高。首先定义如下的结构体用于保存配置信息:

typedef struct _IOFiles
{
    const char *inputFileName;      //输入文件名
    const char *outputFileName;     //输出文件名

    FILE *iFile;                    //输入文件指针
    FILE *oFile;                    //输出文件指针

    uint8_t filterIdx;              //Filter索引

    unsigned int frameWidth;        //图像宽度
    unsigned int frameHeight;       //图像高度
}IOFiles;

在这个结构体中,filterIdx用于表示当前工程选择哪一种filter,即希望实现哪一种功能。

在进入main函数之后,调用hello函数来解析命令行参数:

static int hello(int argc, char **argv, IOFiles &files)
{
    if (argc < 4) 
    {
        printf("usage: %s output_file input_file filter_index\n"
            "Filter index:.\n"
            "1. Color component\n"
            "2. Blur\n"
            "3. Horizonal flip\n"
            "4. HUE\n"
            "5. Crop\n"
            "6. Box\n"
            "7. Text\n"
            "\n", argv[0]);

        return -1;
    }

    files.inputFileName = argv[1];
    files.outputFileName = argv[2];
    files.frameWidth = atoi(argv[3]);
    files.frameHeight = atoi(argv[4]);
    files.filterIdx = atoi(argv[5]);

    fopen_s(&files.iFile, files.inputFileName, "rb+");
    if (!files.iFile)
    {
        printf("Error: open input file failed.\n");
        return -1;
    }

    fopen_s(&files.oFile, files.outputFileName, "wb+");
    if (!files.oFile)
    {
        printf("Error: open output file failed.\n");
        return -1;
    }

    return 0;
}

该函数实现了输入输出文件的文件名获取并打开,并读取filter索引。

2. Video Filter初始化

在进行初始化之前,必须调用filter的init函数,之后才能针对Video Filter进行各种操作。其声明如下:

void avfilter_register_all(void);

为了实现视频水印的功能,所需要的相关结构主要有:

AVFilterContext *buffersink_ctx;  
AVFilterContext *buffersrc_ctx;  
AVFilterGraph *filter_graph;

其中AVFilterContext用于表示一个filter的实例上下文,AVFilterGraph表示一个video filtering的工作流。Video Filter的初始化实现如以下函数:

//初始化video filter相关的结构
int Init_video_filter(const char *filter_descr, int width, int height)
{
    char args[512];  
    AVFilter *buffersrc  = avfilter_get_by_name("buffer");  
    AVFilter *buffersink = avfilter_get_by_name("buffersink");  
    AVFilterInOut *outputs = avfilter_inout_alloc();  
    AVFilterInOut *inputs  = avfilter_inout_alloc();  
    enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };  
    AVBufferSinkParams *buffersink_params;  

    filter_graph = avfilter_graph_alloc();  

    /* buffer video source: the decoded frames from the decoder will be inserted here. */  
    snprintf(args, sizeof(args), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", width,height,AV_PIX_FMT_YUV420P, 1, 25,1,1);
    int ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, NULL, filter_graph);  
    if (ret < 0) 
    {
        printf("Error: cannot create buffer source.\n");  
        return ret;  
    }  

    /* buffer video sink: to terminate the filter chain. */  
    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);  
    av_free(buffersink_params);  
    if (ret < 0) 
    {
        printf("Error: cannot create buffer sink\n");  
        return ret;
    }  

    /* Endpoints for the filter graph. */  
    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;  

    if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, NULL)) < 0)
    {
        printf("Error: avfilter_graph_parse_ptr failed.\n");
        return ret;  
    }

    if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)  
    {
        printf("Error: avfilter_graph_config");
        return ret;  
    }

    return 0;
}

3. 初始化输入输出AVFrame并分配内存

我们首先声明AVFrame类型的对象和指向像素缓存的指针:

AVFrame *frame_in = NULL;  
AVFrame *frame_out = NULL;  
unsigned char *frame_buffer_in = NULL;  
unsigned char *frame_buffer_out = NULL; 

然后分配AVFrame对象,并分配其中的缓存区:

void Init_video_frame_in_out(AVFrame **frameIn, AVFrame **frameOut, unsigned char **frame_buffer_in, unsigned char **frame_buffer_out, int frameWidth, int frameHeight)
{
    *frameIn = av_frame_alloc();  
    *frame_buffer_in = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, frameWidth,frameHeight,1));  
    av_image_fill_arrays((*frameIn)->data, (*frameIn)->linesize,*frame_buffer_in, AV_PIX_FMT_YUV420P,frameWidth,frameHeight,1);  

    *frameOut = av_frame_alloc();  
    *frame_buffer_out = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, frameWidth,frameHeight,1));  
    av_image_fill_arrays((*frameOut)->data, (*frameOut)->linesize,*frame_buffer_out, AV_PIX_FMT_YUV420P,frameWidth,frameHeight,1);  

    (*frameIn)->width = frameWidth;  
    (*frameIn)->height = frameHeight;  
    (*frameIn)->format = AV_PIX_FMT_YUV420P;
}

4. Video Filtering循环体

这一部分主要包括三大部分:

  1. 读取原始的YUV数据到输入的frame;
  2. 使用预先定义好的filter_graph处理输入frame,生成输出frame;
  3. 将输出frame中的像素值写入输出yuv文件;

第一部分,读取原始yuv的实现由自定义函数Read_yuv_data_to_buf实现:

//从输入yuv文件中读取数据到buffer和frame结构
bool Read_yuv_data_to_buf(unsigned char *frame_buffer_in, const IOFiles &files, AVFrame **frameIn)
{
    AVFrame *pFrameIn = *frameIn;
    int width = files.frameWidth, height = files.frameHeight;
    int frameSize = width * height * 3 / 2;

    if (fread_s(frame_buffer_in, frameSize, 1, frameSize, files.iFile) != frameSize)
    {
        return false;
    }

    pFrameIn->data[0] = frame_buffer_in;
    pFrameIn->data[1] = pFrameIn->data[0] + width * height;
    pFrameIn->data[2] = pFrameIn->data[1] + width * height / 4;

    return true;
}

第二部分实际上分为两部分,即将输入frame送入filter graph,以及从filter graph中取出输出frame。实现方法分别为:

//将待处理的输入frame添加进filter graph
bool Add_frame_to_filter(AVFrame *frameIn)
{
    if (av_buffersrc_add_frame(buffersrc_ctx, frameIn) < 0) 
    {  
        return false;  
    }  

    return true;
}

//从filter graph中获取输出frame
int Get_frame_from_filter(AVFrame **frameOut)
{
    if (av_buffersink_get_frame(buffersink_ctx, *frameOut) < 0)
    {
        return false;
    }

    return true;
}

第三部分,写出输出frame到输出yuv文件:

//从输出frame中写出像素数据到输出文件
void Write_yuv_to_outfile(const AVFrame *frame_out, IOFiles &files)
{
    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,files.oFile);  
        }  
        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,files.oFile);  
        }  
        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,files.oFile);  
        }  
    }  
}

该部分的综合实现如下:

while (Read_yuv_data_to_buf(frame_buffer_in, files, &frame_in)) 
{
    //将输入frame添加到filter graph
    if (!Add_frame_to_filter(frame_in))
    {
        printf("Error while adding frame.\n");
        goto end;
    }

    //从filter graph中获取输出frame
    if (!Get_frame_from_filter(&frame_out))
    {
        printf("Error while getting frame.\n");
        goto end;
    }

    //将输出frame写出到输出文件
    Write_yuv_to_outfile(frame_out, files);

    printf("Process 1 frame!\n");  
    av_frame_unref(frame_out);  
}

5、 收尾工作

整体实现完成后,需要进行善后的收尾工作有释放输入和输出frame、关闭输入输出文件,以及释放filter graph:

//关闭文件及相关结构
fclose(files.iFile);
fclose(files.oFile);

av_frame_free(&frame_in);
av_frame_free(&frame_out);

avfilter_graph_free(&filter_graph);

处理前的原始图像如下:

FFMpeg Filter Origin

添加水印之后的图像如下:

FFMpeg Filter Filtered

目录
相关文章
|
2月前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
137 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
2月前
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
74 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
1月前
|
编解码 监控 网络协议
如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频
本文详细介绍了如何使用FFmpeg实现RTSP推送H.264和H.265(HEVC)编码视频。内容涵盖环境搭建、编码配置、服务器端与客户端实现等方面,适合视频监控系统和直播平台等应用场景。通过具体命令和示例代码,帮助读者快速上手并实现目标。
126 6
|
2月前
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
159 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
2月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
81 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
2月前
|
Java 数据安全/隐私保护
Java ffmpeg 实现视频加文字/图片水印功能
【10月更文挑战第22天】在 Java 中使用 FFmpeg 实现视频加文字或图片水印功能,需先安装 FFmpeg 并添加依赖(如 JavaCV)。通过构建 FFmpeg 命令行参数,使用 `drawtext` 滤镜添加文字水印,或使用 `overlay` 滤镜添加图片水印。示例代码展示了如何使用 JavaCV 实现文字水印。
124 1
|
2月前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
106 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
计算机视觉 Python
FFMPEG学习笔记(一): 提取视频的纯音频及无声视频
本文介绍了如何使用FFmpeg工具从视频中提取纯音频和无声视频。提供了具体的命令行操作,例如使用`ffmpeg -i input.mp4 -vn -c:a libmp3lame output.mp3`来提取音频,以及`ffmpeg -i input.mp4 -c:v copy -an output.mp4`来提取无声视频。此外,还包含了一个Python脚本,用于批量处理视频文件,自动提取音频和生成无声视频。
74 1
|
2月前
|
Android开发 开发者
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
谷歌推出的Transformer,作为Jetpack Media3架构的一部分,助力开发者实现音视频格式转换与编辑。Media3简化了媒体处理流程,提升了定制性和可靠性。Transformer可用于剪辑、添加滤镜等操作,其示例代码可在指定GitHub仓库中找到。要使用Transformer,需在`build.gradle`中添加相关依赖,并按文档编写处理逻辑,最终完成音视频转换任务。具体步骤包括配置剪辑参数、设置空间效果以及监听转换事件等。
58 0
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
|
2月前
|
Linux 视频直播
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
本文介绍了如何使用EasyPusher-Android实现RTSP直播流程。首先对比了RTSP、RTMP、SRT和RIST四种流媒体协议,并以RTSP为例,详细说明了使用EasyPusher-Android向流媒体服务器进行RTSP直播推流的方法。文中还提供了OBS Studio配置RTSP插件及ZLMediaKit云服务器部署的相关信息,通过修改EasyPusher-Android源码使其支持通用RTSP地址,最终验证了直播功能的成功实现。
66 0
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播

热门文章

最新文章