FFmpeg進(jìn)階教程之給視頻添加文字水印
前言
和圖片水印一樣,很多時(shí)候?yàn)榱寺暶饕曨l的原創(chuàng)性,我們會(huì)給視頻添加文字水印進(jìn)行版權(quán)保護(hù)。添加文字水印和添加圖片水印的流程相似,但又略有不同,這里介紹一下如何通過(guò)FFmpeg給視頻添加文字水印。添加文字水印的流程圖如下圖所示:

文字水印配置項(xiàng)
在講文字水印之前先介紹一下文字水印支持的那些配置,方便大家的使用。
| 項(xiàng)目 | 介紹 |
|---|---|
| 使用格式 | drawtext=fontfile=font_f:text=text1…(通過(guò)冒號(hào)分割配置項(xiàng),通過(guò)=給配置項(xiàng)賦值) |
| fontfile | 用于繪制文本的字體文件的正確路徑,強(qiáng)制參數(shù) |
| text | 要繪制的文本字符串,必須是UTF-8編碼的字符序列 |
| x,y | 繪制的位置的起始坐標(biāo)值 |
| fontcolor | 字體顏色名稱或0xRRGGBB[AA]格式的顏色,默認(rèn)為黑色 |
| fontsize | 要繪制的文本字體大小,默認(rèn)值為16 |
| tabsize | 用于呈現(xiàn)選項(xiàng)卡的空間大小,默認(rèn)值為4 |
| line_h,lh | 每個(gè)文本行的高度 |
| main_h,h,H | 輸入的高度 |
| main_w,w,W | 輸入的寬度 |
常用的配置項(xiàng)主要有這些,如果需要其他的配置可以參考官方文檔介紹。
文字水印關(guān)鍵點(diǎn)
中文的支持
和QT一樣,F(xiàn)Fmpeg繪制文字水印也存在中文亂碼的問(wèn)題。在windows下解決中文亂碼主要需要以下幾點(diǎn):
1.將源碼文件修改為utf-8編碼
2.將編譯編碼類型修改為utf-8編碼對(duì)應(yīng)的配置如下:
#pragma execution_character_set("utf-8")同時(shí)我們還應(yīng)該確保使用的字體支持中文。
字體路徑問(wèn)題
指定字體文件路徑是強(qiáng)制參數(shù),可以使用絕對(duì)路徑和相對(duì)路徑
//使用工程相對(duì)路徑下的字體 fontfile=.//simsun.ttc //使用D盤(pán)絕對(duì)路徑下的字體,要對(duì)斜杠進(jìn)行轉(zhuǎn)義 fontfile=D\\\\:simun.ttc
定義濾鏡實(shí)現(xiàn)
文字水印對(duì)應(yīng)的繪制流程圖如下圖所示:

文字水印濾鏡的實(shí)現(xiàn)如下:
int InitFilter(AVCodecContext * codecContext)
{
char args[512];
int ret = 0;
//緩存輸入和緩存輸出
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
//創(chuàng)建輸入輸出參數(shù)
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
//濾鏡的描述
//使用simhei字體,繪制的字體大小為100,文本內(nèi)容為"鬼滅之刃",繪制位置為(100,100)
//繪制的字體顏色為白色
string filters_descr = "drawtext=fontfile=.//simsun.ttc:fontsize=100:text=鬼滅之刃:x=100:y=100:fontcolor=0xFFFFFF";
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P };
//創(chuàng)建濾鏡容器
filter_graph = avfilter_graph_alloc();
if (!outputs || !inputs || !filter_graph)
{
ret = AVERROR(ENOMEM);
goto end;
}
//初始化數(shù)據(jù)幀的格式
sprintf_s(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
codecContext->width, codecContext->height, codecContext->pix_fmt,
codecContext->time_base.num, codecContext->time_base.den,
codecContext->sample_aspect_ratio.num, codecContext->sample_aspect_ratio.den);
//輸入數(shù)據(jù)緩存
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
goto end;
}
//輸出數(shù)據(jù)緩存
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");
goto end;
}
//設(shè)置元素樣式
ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
AV_PIX_FMT_YUV420P, AV_OPT_SEARCH_CHILDREN);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
goto end;
}
//設(shè)置濾鏡的端點(diǎn)
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, filters_descr.c_str(),
&inputs, &outputs, NULL)) < 0)
goto end;
//濾鏡生效
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
goto end;
end:
//釋放對(duì)應(yīng)的輸入輸出
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
項(xiàng)目工程源碼
給視頻文件添加文字水印的工程源碼如下,歡迎參考,如有問(wèn)題歡迎反饋。
#pragma execution_character_set("utf-8")
#include <string>
#include <iostream>
#include <thread>
#include <memory>
#include <iostream>
#include <fstream>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavfilter/avfilter.h>
#include <libavformat/avformat.h>
#include <libavformat/avio.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
#include <libavutil/frame.h>
#include <libavutil/imgutils.h>
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libavdevice/avdevice.h>
}
using namespace std;
//輸入媒體文件的上下文
AVFormatContext * input_format_ctx = nullptr;
//輸出媒體文件的上下文
AVFormatContext* output_format_ctx;
//輸出視頻編碼器
AVCodecContext* ouput_video_encode_ctx = NULL;
//音視頻解碼器
AVCodecContext *video_decode_ctx = NULL;
AVCodecContext *audio_decode_ctx = NULL;
//視頻索引和音頻索引
int video_stream_index = -1;
int audio_stream_index = -1;
//輸出編碼器
static AVCodec * output_video_codec;
//濾鏡容器和緩存
AVFilterGraph * filter_graph = nullptr;
AVFilterContext *buffersink_ctx = nullptr;;
AVFilterContext *buffersrc_ctx = nullptr;;
AVPacket packet;
//起始時(shí)間
static int64_t startTime;
int OpenOutput(char *fileName)
{
//創(chuàng)建輸出流,輸出flv格式視頻
int ret = 0;
ret = avformat_alloc_output_context2(&output_format_ctx, NULL, "flv", fileName);
if (ret < 0)
{
return -1;
}
//打開(kāi)輸出流
ret = avio_open(&output_format_ctx->pb, fileName, AVIO_FLAG_READ_WRITE);
if (ret < 0)
{
return -2;
}
//創(chuàng)建輸出流
for (int index = 0; index < input_format_ctx->nb_streams; index++)
{
if (index == video_stream_index)
{
AVStream * stream = avformat_new_stream(output_format_ctx, output_video_codec);
avcodec_parameters_from_context(stream->codecpar, ouput_video_encode_ctx);
stream->codecpar->codec_tag = 0;
}
else if (index == audio_stream_index)
{
AVStream * stream = avformat_new_stream(output_format_ctx, NULL);
stream->codecpar = input_format_ctx->streams[audio_stream_index]->codecpar;
stream->codecpar->codec_tag = 0;
}
}
//寫(xiě)文件頭
ret = avformat_write_header(output_format_ctx, nullptr);
if (ret < 0)
{
return -3;
}
if (ret >= 0)
cout << "open output stream successfully" << endl;
return ret;
}
//初始化輸出視頻的編碼器
int InitEncoderCodec(int iWidth, int iHeight)
{
output_video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (NULL == output_video_codec)
{
return -1;
}
//指定編碼器的參數(shù)
ouput_video_encode_ctx = avcodec_alloc_context3(output_video_codec);
ouput_video_encode_ctx->time_base = input_format_ctx->streams[video_stream_index]->time_base;
ouput_video_encode_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
ouput_video_encode_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
ouput_video_encode_ctx->width = iWidth;
ouput_video_encode_ctx->height = iHeight;
ouput_video_encode_ctx->bit_rate = input_format_ctx->streams[video_stream_index]->codecpar->bit_rate;
ouput_video_encode_ctx->pix_fmt = (AVPixelFormat)*output_video_codec->pix_fmts;
ouput_video_encode_ctx->profile = FF_PROFILE_H264_MAIN;
ouput_video_encode_ctx->level = 41;
ouput_video_encode_ctx->thread_count = 8;
return 0;
}
int InitFilter(AVCodecContext * codecContext)
{
char args[512];
int ret = 0;
//緩存輸入和緩存輸出
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
//創(chuàng)建輸入輸出參數(shù)
AVFilterInOut *outputs = avfilter_inout_alloc();
AVFilterInOut *inputs = avfilter_inout_alloc();
//濾鏡的描述
//使用simhei字體,繪制的字體大小為100,文本內(nèi)容為"鬼滅之刃",繪制位置為(100,100)
//繪制的字體顏色為白色
string filters_descr = "drawtext=fontfile=.//simsun.ttc:fontsize=100:text=鬼滅之刃:x=100:y=100:fontcolor=0xFFFFFF";
enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P };
//創(chuàng)建濾鏡容器
filter_graph = avfilter_graph_alloc();
if (!outputs || !inputs || !filter_graph)
{
ret = AVERROR(ENOMEM);
goto end;
}
//初始化數(shù)據(jù)幀的格式
sprintf_s(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
codecContext->width, codecContext->height, codecContext->pix_fmt,
codecContext->time_base.num, codecContext->time_base.den,
codecContext->sample_aspect_ratio.num, codecContext->sample_aspect_ratio.den);
//輸入數(shù)據(jù)緩存
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);
if (ret < 0) {
goto end;
}
//輸出數(shù)據(jù)緩存
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");
goto end;
}
//設(shè)置元素樣式
ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
AV_PIX_FMT_YUV420P, AV_OPT_SEARCH_CHILDREN);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
goto end;
}
//設(shè)置濾鏡的端點(diǎn)
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, filters_descr.c_str(),
&inputs, &outputs, NULL)) < 0)
goto end;
//濾鏡生效
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
goto end;
end:
//釋放對(duì)應(yīng)的輸入輸出
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return ret;
}
//將加水印之后的圖像幀輸出到文件中
static int output_frame(AVFrame *frame, AVRational time_base)
{
int code;
AVPacket packet = { 0 };
av_init_packet(&packet);
int ret = avcodec_send_frame(ouput_video_encode_ctx, frame);
if (ret < 0)
{
printf("Error sending a frame for encoding\n");
return -1;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(ouput_video_encode_ctx, &packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
return (ret == AVERROR(EAGAIN)) ? 0 : 1;
}
else if (ret < 0) {
printf("Error during encoding\n");
exit(1);
}
AVRational avTimeBaseQ = { 1, AV_TIME_BASE };
int64_t ptsTime = av_rescale_q(frame->pts, input_format_ctx->streams[video_stream_index]->time_base, avTimeBaseQ);
int64_t nowTime = av_gettime() - startTime;
if ((ptsTime > nowTime))
{
int64_t sleepTime = ptsTime - nowTime;
av_usleep((sleepTime));
}
else
{
printf("not sleeping\n");
}
packet.pts = av_rescale_q_rnd(packet.pts, time_base, output_format_ctx->streams[video_stream_index]->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, time_base, output_format_ctx->streams[video_stream_index]->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
packet.stream_index = video_stream_index;
code = av_interleaved_write_frame(output_format_ctx, &packet);
av_packet_unref(&packet);
if (code < 0)
{
av_log(NULL, AV_LOG_ERROR, "[ERROR] Writing Live Stream Interleaved Frame");
}
if (ret < 0) {
exit(1);
}
av_packet_unref(&packet);
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("usage:%1 input filepath %2 outputfilepath");
return -1;
}
//輸入文件地址、輸出文件地址
string fileInput = std::string(argv[1]);
string fileOutput = std::string(argv[2]);
//初始化各種配置
avformat_network_init();
av_log_set_level(AV_LOG_ERROR);
//打開(kāi)輸入文件
int ret = avformat_open_input(&input_format_ctx, fileInput.c_str(), NULL, NULL);
if (ret < 0)
{
return ret;
}
ret = avformat_find_stream_info(input_format_ctx, NULL);
if (ret < 0)
{
return ret;
}
//查找音視頻流的索引
for (int index = 0; index < input_format_ctx->nb_streams; ++index)
{
if (index == AVMEDIA_TYPE_AUDIO)
{
audio_stream_index = index;
}
else if (index == AVMEDIA_TYPE_VIDEO)
{
video_stream_index = index;
}
}
//打開(kāi)視頻解碼器
const AVCodec* codec = avcodec_find_decoder(input_format_ctx->streams[video_stream_index]->codecpar->codec_id);
if (!codec)
{
return -1;
}
video_decode_ctx = avcodec_alloc_context3(codec);
if (!video_decode_ctx)
{
fprintf(stderr, "Could not allocate video codec context\n");
return -2;
}
avcodec_parameters_to_context(video_decode_ctx, input_format_ctx->streams[video_stream_index]->codecpar);
if (codec->capabilities & AV_CODEC_CAP_TRUNCATED)
video_decode_ctx->flags |= AV_CODEC_FLAG_TRUNCATED;
ret = avcodec_open2(video_decode_ctx, codec, NULL);
if (ret < 0)
{
av_free(video_decode_ctx);
return -3;
}
//初始化視頻編碼器
ret = InitEncoderCodec(video_decode_ctx->width, video_decode_ctx->height);
if (ret < 0)
{
return 0;
}
//初始化濾鏡
ret = InitFilter(ouput_video_encode_ctx);
//打開(kāi)編碼器
ret = avcodec_open2(ouput_video_encode_ctx, output_video_codec, NULL);
if (ret < 0)
{
return ret;
}
//初始化輸出
if (OpenOutput((char *)fileOutput.c_str()) < 0)
{
cout << "Open file Output failed!" << endl;
this_thread::sleep_for(chrono::seconds(10));
return 0;
}
AVFrame* pSrcFrame = av_frame_alloc();
AVFrame* filterFrame = av_frame_alloc();
av_init_packet(&packet);
startTime = av_gettime();
while (true)
{
int ret = av_read_frame(input_format_ctx, &packet);
if (ret < 0)
{
break;
}
//視頻幀通過(guò)濾鏡處理之后編碼輸出
if (packet.stream_index == video_stream_index)
{
int ret = avcodec_send_packet(video_decode_ctx, &packet);
if (ret < 0)
{
break;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(video_decode_ctx, pSrcFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
goto End;
}
pSrcFrame->pts = pSrcFrame->best_effort_timestamp;
//添加到濾鏡中
if (av_buffersrc_add_frame_flags(buffersrc_ctx, pSrcFrame, AV_BUFFERSRC_FLAG_KEEP_REF) < 0)
{
break;
}
while (1)
{
//獲取濾鏡輸出
int ret = av_buffersink_get_frame(buffersink_ctx, filterFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
goto End;
}
//編碼之后輸出
output_frame(filterFrame, buffersink_ctx->inputs[0]->time_base);
av_frame_unref(filterFrame);
}
av_frame_unref(pSrcFrame);
}
}
else if (packet.stream_index == audio_stream_index)
{
packet.pts = av_rescale_q_rnd(packet.pts, input_format_ctx->streams[audio_stream_index]->time_base, output_format_ctx->streams[audio_stream_index]->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
packet.dts = av_rescale_q_rnd(packet.dts, input_format_ctx->streams[audio_stream_index]->time_base, output_format_ctx->streams[audio_stream_index]->time_base, (AVRounding)(AV_ROUND_INF | AV_ROUND_PASS_MINMAX));
packet.stream_index = audio_stream_index;
av_interleaved_write_frame(output_format_ctx, &packet);
}
av_packet_unref(&packet);
}
av_write_trailer(output_format_ctx);
End:
//結(jié)束的時(shí)候清理資源
avfilter_graph_free(&filter_graph);
if (input_format_ctx != NULL)
{
avformat_close_input(&input_format_ctx);
}
avcodec_free_context(&video_decode_ctx);
avcodec_free_context(&ouput_video_encode_ctx);
return 0;
}
使用效果
沒(méi)有添加水印之前的視頻截圖如下:

添加水印之后的效果圖如下圖所示:

總結(jié)
到此這篇關(guān)于FFmpeg進(jìn)階教程之給視頻添加文字水印的文章就介紹到這了,更多相關(guān)FFmpeg視頻加文字水印內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)通訊錄系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)通訊錄系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
Qt私有信號(hào)實(shí)現(xiàn)(private signal)
在使用Qt信號(hào)槽機(jī)制的時(shí)候,有時(shí)候我們需要一個(gè)信號(hào)只能由類內(nèi)發(fā)出,而不允許使用該類對(duì)象的用戶發(fā)出,此時(shí)就需要私有信號(hào)的支持,本文主要介紹了Qt私有信號(hào)實(shí)現(xiàn)(private signal),感興趣的可以了解一下2023-10-10
探索Visual C++下創(chuàng)建WPF項(xiàng)目的方法示例
這篇文章主要介紹了探索Visual C++下創(chuàng)建WPF項(xiàng)目的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
c語(yǔ)言算術(shù)運(yùn)算符越界問(wèn)題解決方案
大量的安全漏洞是由于計(jì)算機(jī)算術(shù)運(yùn)算的微妙細(xì)節(jié)引起的, 具體的C語(yǔ)言, 諸如符號(hào)數(shù)和無(wú)符號(hào)數(shù)之間轉(zhuǎn)換, 算術(shù)運(yùn)算的越界都會(huì)導(dǎo)致不可預(yù)知的錯(cuò)誤和安全漏洞, 具體的案例數(shù)不勝數(shù).2012-11-11
C語(yǔ)言系統(tǒng)日期和時(shí)間實(shí)例詳解
我們?cè)趯?xiě)C語(yǔ)言程序的時(shí)候,有的時(shí)候會(huì)用到讀取本機(jī)的時(shí)間和日期,下面這篇文章主要給大家介紹了關(guān)于C語(yǔ)言系統(tǒng)日期和時(shí)間的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06
OpenCV實(shí)現(xiàn)圖像輪廓檢測(cè)以及外接矩形
這篇文章主要為大家詳細(xì)介紹了OpenCV實(shí)現(xiàn)圖像輪廓檢測(cè)以及外接矩形,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01
通過(guò)C++程序示例理解設(shè)計(jì)模式中的外觀模式
這篇文章主要介紹了通過(guò)設(shè)計(jì)模式中的外觀模式及相關(guān)的C++程序示例,外觀模式在高層提供了一個(gè)統(tǒng)一的接口實(shí)現(xiàn)一定程度上的解耦,需要的朋友可以參考下2016-03-03

