使用ffmpeg实现OSD叠加
1.使用命令实现OSD叠加
ffmpeg -i input.h264 -vf "drawbox=x=50:y=50:w=200:h=100:color=red@0.5:t=max, drawbox=x=300:y=200:w=100:h=100:color=blue@1.0:t=5, drawtext=text='2024-10-01':x=W-tw-10:y=10:fontcolor=white:fontsize=50" -c:v libx264 -f h264 output.h264
drawbox
滤镜用于绘制色块和方框。drawtext
:滤镜用于在视频上添加文本。text='2024-10-01'
指定要显示的固定日期。x=W-tw-10
将文本放置在视频宽度减去文本宽度再减去10像素的位置,即右上角。y=10
将文本放置在距离顶部10像素的位置。fontcolor
参数指定文本颜色为白色。fontsize
参数指定文本字体大小为50。
执行后会在码流中叠加对应色块、方框,以及使用字库叠加文字至码流中。
2.使用程序实现OSD叠加
主函数流程如下所示:
- 初始化并解析输入参数。
- 初始化 FFmpeg 库。
- 打开输入文件并初始化解码器。
- 初始化输出文件上下文和视频编码器。
- 初始化滤镜图。
- 读取、解码、处理和编码视频帧。
- 刷新编码器并写入所有剩余帧。
- 写入文件尾部并清理所有资源。
2.1 滤镜图函数流程
滤镜图在视频处理过程中用于对视频帧进行各种滤镜操作,比如调整帧率、添加文本或图形等。我们将结合 init_filter_graph
函数和 main
函数中的相关部分来详细说明。
- 定义滤镜描述
在 init_filter_graph
函数中,我们首先定义了一个滤镜描述字符串 filter_descr
,它包含了一系列滤镜操作:
const char *filter_descr = "fps=fps=25,"
"drawbox=x=50:y=50:w=200:h=100:color=red@0.5:t=max,"
"drawbox=x=300:y=200:w=100:h=100:color=blue@1.0:t=5,"
"drawtext=text='2024-10-01':x=W-tw-10:y=10:fontcolor=white:fontsize=50";
这个字符串定义了以下滤镜操作:
fps=fps=25
:将帧率设置为 25 fps。drawbox
:在指定位置绘制红色和蓝色矩形。drawtext
:在视频帧的右上角绘制白色文本“2024-10-01”。
- 获取输入/输出滤镜
接下来,我们获取缓冲区源和接收器滤镜:
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
buffer
滤镜用于接收解码后的帧。buffersink
滤镜用于输出处理后的帧。
- 手动设置视频参数
我们手动构建一个包含视频参数的字符串 args
:
char args[512];
snprintf(args, sizeof(args),
"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,
dec_ctx->time_base.num, dec_ctx->time_base.den,
dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den);
这个字符串包含了视频的宽度、高度、像素格式、时间基和像素纵横比。
- 创建缓冲区源和接收器
使用 avfilter_graph_create_filter
创建缓冲区源和接收器:
if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, nullptr, filter_graph) < 0) {
std::cerr << "Cannot create buffer source\n";
return -1;
}
if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr, filter_graph) < 0) {
std::cerr << "Cannot create buffer sink\n";
return -1;
}
buffersrc_ctx
是缓冲区源滤镜的上下文。buffersink_ctx
是缓冲区接收器滤镜的上下文。
- 设置滤镜图的端点
我们设置滤镜图的输入和输出端点:
outputs->name = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = nullptr;
inputs->name = av_strdup("out");
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = nullptr;
outputs
表示滤镜图的输入端点。inputs
表示滤镜图的输出端点。
- 解析并配置滤镜图
使用 avfilter_graph_parse_ptr
解析滤镜图,并使用 avfilter_graph_config
配置滤镜图:
if (avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr) < 0) {
std::cerr << "Error parsing filter graph\n";
return -1;
}
if (avfilter_graph_config(filter_graph, nullptr) < 0) {
std::cerr << "Error configuring filter graph\n";
return -1;
}
avfilter_graph_parse_ptr
解析滤镜描述字符串并构建滤镜图。avfilter_graph_config
配置滤镜图,使其准备好处理帧。
在 main
函数中,滤镜图的使用主要体现在处理视频帧的过程中:
- 读取和解码视频帧
while (av_read_frame(fmt_ctx, &packet) >= 0) {
if (packet.stream_index == 0) {
if (avcodec_send_packet(dec_ctx, &packet) < 0) {
std::cerr << "Error sending packet for decoding\n";
break;
}
while (avcodec_receive_frame(dec_ctx, frame) == 0) {
if (frame->pts == AV_NOPTS_VALUE) {
frame->pts = frame_count++;
}
// 将解码后的帧推送到滤镜图
if (av_buffersrc_add_frame(buffersrc_ctx, frame) < 0) {
std::cerr << "Error feeding frame to filter graph\n";
break;
}
- 读取输入文件中的帧并解码。
- 将解码后的帧推送到滤镜图的缓冲区源滤镜。
- 从滤镜图中获取处理后的帧
while (av_buffersink_get_frame(buffersink_ctx, filt_frame) >= 0) {
// 编码过滤后的帧
av_init_packet(&enc_pkt);
enc_pkt.data = nullptr;
enc_pkt.size = 0;
filt_frame->pts = frame_count++;
if (avcodec_send_frame(enc_ctx, filt_frame) >= 0) {
while (avcodec_receive_packet(enc_ctx, &enc_pkt) >= 0) {
enc_pkt.stream_index = 0;
av_packet_rescale_ts(&enc_pkt, enc_ctx->time_base, out_fmt_ctx->streams[0]->time_base);
av_interleaved_write_frame(out_fmt_ctx, &enc_pkt);
av_packet_unref(&enc_pkt);
}
}
av_frame_unref(filt_frame);
}
av_frame_unref(frame);
}
}
av_packet_unref(&packet);
}
- 从滤镜图的缓冲区接收器滤镜中获取处理后的帧。
- 对处理后的帧进行编码并写入输出文件。
滤镜图使用步骤:初始化滤镜图、设置滤镜描述、创建缓冲区源和接收器、解析和配置滤镜图。
3.程序源码
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
}
#include <iostream>
#include <string>
// 打开输入文件并使用用户指定的编解码器信息
int open_input_file(AVFormatContext **fmt_ctx, const char *filename, AVCodecContext **dec_ctx, int width, int height, AVPixelFormat pix_fmt, AVRational time_base) {
// 查找 H.264 解码器
AVCodec *decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!decoder) {
std::cerr << "Codec not found\n";
return -1;
}
// 分配解码器上下文
AVCodecContext *dec_ctx_local = avcodec_alloc_context3(decoder);
if (!dec_ctx_local) {
std::cerr << "Could not allocate video codec context\n";
return -1;
}
// 手动指定编解码器参数
dec_ctx_local->width = width;
dec_ctx_local->height = height;
dec_ctx_local->pix_fmt = pix_fmt;
dec_ctx_local->time_base = time_base;
// 打开编解码器
if (avcodec_open2(dec_ctx_local, decoder, nullptr) < 0) {
std::cerr << "Could not open codec\n";
return -1;
}
*dec_ctx = dec_ctx_local;
// 打开输入文件格式上下文
if (avformat_open_input(fmt_ctx, filename, nullptr, nullptr) < 0) {
std::cerr << "Error opening input file\n";
return -1;
}
return 0;
}
// 初始化滤镜图以进行处理
int init_filter_graph(AVFilterGraph **graph, AVFilterContext **src_ctx, AVFilterContext **sink_ctx, AVCodecContext *dec_ctx) {
// 分配滤镜图
AVFilterGraph *filter_graph = avfilter_graph_alloc();
AVFilterContext *buffersrc_ctx = nullptr;
AVFilterContext *buffersink_ctx = nullptr;
// 定义滤镜描述
const char *filter_descr = "fps=fps=25,"
"drawbox=x=50:y=50:w=200:h=100:color=red@0.5:t=max,"
"drawbox=x=300:y=200:w=100:h=100:color=blue@1.0:t=5,"
"drawtext=text='2024-10-01':x=W-tw-10:y=10:fontcolor=white:fontsize=50";
// 输入/输出滤镜
const AVFilter *buffersrc = avfilter_get_by_name("buffer");
const AVFilter *buffersink = avfilter_get_by_name("buffersink");
AVFilterInOut *inputs = avfilter_inout_alloc();
AVFilterInOut *outputs = avfilter_inout_alloc();
// 手动指定用户输入的视频参数
char args[512];
snprintf(args, sizeof(args),
"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,
dec_ctx->time_base.num, dec_ctx->time_base.den,
dec_ctx->sample_aspect_ratio.num, dec_ctx->sample_aspect_ratio.den);
std::cout << "Filter graph parameters: " << args << std::endl;
// 创建缓冲区源和接收器
if (avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in", args, nullptr, filter_graph) < 0) {
std::cerr << "Cannot create buffer source\n";
return -1;
}
if (avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out", nullptr, nullptr, filter_graph) < 0) {
std::cerr << "Cannot create buffer sink\n";
return -1;
}
// 设置滤镜图的端点
outputs->name = av_strdup("in");
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = nullptr;
inputs->name = av_strdup("out");
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = nullptr;
// 解析并配置滤镜图
if (avfilter_graph_parse_ptr(filter_graph, filter_descr, &inputs, &outputs, nullptr) < 0) {
std::cerr << "Error parsing filter graph\n";
return -1;
}
if (avfilter_graph_config(filter_graph, nullptr) < 0) {
std::cerr << "Error configuring filter graph\n";
return -1;
}
*graph = filter_graph;
*src_ctx = buffersrc_ctx;
*sink_ctx = buffersink_ctx;
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
return 0;
}
// 初始化视频编码器(libx264)
int init_encoder(AVCodecContext **enc_ctx, AVFormatContext *fmt_ctx, AVCodecContext *dec_ctx, const char *filename) {
// 查找 H.264 编码器
AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!encoder) {
std::cerr << "Codec not found\n";
return -1;
}
// 分配编码器上下文
AVCodecContext *enc_ctx_local = avcodec_alloc_context3(encoder);
if (!enc_ctx_local) {
std::cerr << "Could not allocate video codec context\n";
return -1;
}
// 设置编码器参数
enc_ctx_local->bit_rate = 2000000;
enc_ctx_local->width = dec_ctx->width;
enc_ctx_local->height = dec_ctx->height;
//enc_ctx_local->time_base = dec_ctx->time_base; // Ensure time_base matches input
//enc_ctx_local->framerate = dec_ctx->framerate; // Set framerate from input
enc_ctx_local->framerate = (AVRational){25, 1};
enc_ctx_local->time_base = (AVRational){1, 25}; // Time base should match the framerate
enc_ctx_local->gop_size = 10;
enc_ctx_local->max_b_frames = 1;
enc_ctx_local->pix_fmt = AV_PIX_FMT_YUV420P;
// 打开编码器
if (avcodec_open2(enc_ctx_local, encoder, nullptr) < 0) {
std::cerr << "Could not open codec\n";
return -1;
}
*enc_ctx = enc_ctx_local;
// 打开输出文件并设置输出流的时间基
AVStream *out_stream = avformat_new_stream(fmt_ctx, nullptr);
if (!out_stream) {
std::cerr << "Error allocating output stream\n";
return -1;
}
out_stream->time_base = enc_ctx_local->time_base; // 设置输出流的时间基
if (avcodec_parameters_from_context(out_stream->codecpar, enc_ctx_local) < 0) {
std::cerr << "Failed to copy encoder parameters to output stream\n";
return -1;
}
if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) {
if (avio_open(&fmt_ctx->pb, filename, AVIO_FLAG_WRITE) < 0) {
std::cerr << "Could not open output file\n";
return -1;
}
}
if (avformat_write_header(fmt_ctx, nullptr) < 0) {
std::cerr << "Error writing header\n";
return -1;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc < 7) {
std::cerr << "Usage: " << argv[0] << " input_file output_file width height pix_fmt time_base\n";
std::cerr << "Example: " << argv[0] << " input.h264 output.h264 1920 1080 0 25\n";
return -1;
}
// 解析命令行参数
const char *input_file = argv[1];
const char *output_file = argv[2];
int width = std::stoi(argv[3]);
int height = std::stoi(argv[4]);
AVPixelFormat pix_fmt = static_cast<AVPixelFormat>(std::stoi(argv[5])); // Example: 0 for AV_PIX_FMT_YUV420P
AVRational time_base = {1, std::stoi(argv[6])}; // Example: 1/25 for 25fps
// 初始化 FFmpeg 并注册所有编解码器和格式
av_register_all();
avfilter_register_all();
// 1. 打开输入文件并手动指定编解码器信息
AVFormatContext *fmt_ctx = nullptr;
AVCodecContext *dec_ctx = nullptr;
if (open_input_file(&fmt_ctx, input_file, &dec_ctx, width, height, pix_fmt, time_base) < 0) {
std::cerr << "Error opening input file\n";
return -1;
}
// 初始化输出上下文以编码文件
AVFormatContext *out_fmt_ctx = nullptr;
avformat_alloc_output_context2(&out_fmt_ctx, nullptr, nullptr, output_file);
if (!out_fmt_ctx) {
std::cerr << "Could not create output context\n";
return -1;
}
// 3. 初始化视频编码器
AVCodecContext *enc_ctx = nullptr;
if (init_encoder(&enc_ctx, out_fmt_ctx, dec_ctx, output_file) < 0) {
std::cerr << "Error initializing encoder\n";
return -1;
}
// 4. 初始化滤镜图
AVFilterGraph *filter_graph = nullptr;
AVFilterContext *buffersrc_ctx = nullptr;
AVFilterContext *buffersink_ctx = nullptr;
if (init_filter_graph(&filter_graph, &buffersrc_ctx, &buffersink_ctx, dec_ctx) < 0) {
std::cerr << "Error initializing filter graph\n";
return -1;
}
// 5. 开始处理视频帧
AVPacket packet;
AVFrame *frame = av_frame_alloc();
AVFrame *filt_frame = av_frame_alloc();
AVPacket enc_pkt;
int64_t frame_count = 0;
while (av_read_frame(fmt_ctx, &packet) >= 0) {
if (packet.stream_index == 0) { // 假设流 0 是视频流
if (avcodec_send_packet(dec_ctx, &packet) < 0) {
std::cerr << "Error sending packet for decoding\n";
break;
}
while (avcodec_receive_frame(dec_ctx, frame) == 0) {
if (frame->pts == AV_NOPTS_VALUE) {
frame->pts = frame_count++;
}
// 将解码后的帧推送到滤镜图
if (av_buffersrc_add_frame(buffersrc_ctx, frame) < 0) {
std::cerr << "Error feeding frame to filter graph\n";
break;
}
// 从滤镜图中拉取过滤后的帧
while (av_buffersink_get_frame(buffersink_ctx, filt_frame) >= 0) {
// 编码过滤后的帧
av_init_packet(&enc_pkt);
enc_pkt.data = nullptr;
enc_pkt.size = 0;
filt_frame->pts = frame_count++;
if (avcodec_send_frame(enc_ctx, filt_frame) >= 0) {
while (avcodec_receive_packet(enc_ctx, &enc_pkt) >= 0) {
enc_pkt.stream_index = 0;
av_packet_rescale_ts(&enc_pkt, enc_ctx->time_base, out_fmt_ctx->streams[0]->time_base);
av_interleaved_write_frame(out_fmt_ctx, &enc_pkt); // 写入编码后的帧
av_packet_unref(&enc_pkt);
}
}
av_frame_unref(filt_frame);
}
av_frame_unref(frame);
}
}
av_packet_unref(&packet);
}
// 刷新编码器
avcodec_send_frame(enc_ctx, nullptr);
while (avcodec_receive_packet(enc_ctx, &enc_pkt) >= 0) {
av_packet_rescale_ts(&enc_pkt, enc_ctx->time_base, out_fmt_ctx->streams[0]->time_base);
av_interleaved_write_frame(out_fmt_ctx, &enc_pkt);
av_packet_unref(&enc_pkt);
}
// Write file trailer
av_write_trailer(out_fmt_ctx);
// 写入文件尾部
av_frame_free(&frame);
av_frame_free(&filt_frame);
avfilter_graph_free(&filter_graph);
avcodec_free_context(&enc_ctx);
avcodec_free_context(&dec_ctx);
avformat_close_input(&fmt_ctx);
if (!(out_fmt_ctx->oformat->flags & AVFMT_NOFILE))
avio_closep(&out_fmt_ctx->pb);
avformat_free_context(out_fmt_ctx);
return 0;
}