author: hjjdebug
date: 2025年 07月 22日 星期二 17:06:02 CST
descrip: 用ffmpeg 进行视频的拼接
文章目录
1. 指定协议为concat 方式.
举例:
ffmpeg -i “concat:short1.ts|1.ts” -c copy output.ts
工作原理:
在libavformat/concat.c 文件有该协议的实现
1.1 协议为concat 模式,会调用 concat_open 函数
会引起读写数据时,由concat协议控制从文件中读数据,当第一个文件读到尾时,
接着从第二个文件读
分析字符串:“concat:short1.ts|1.ts”, 找到文件名 “short1.ts”,“1.ts”,
用一个循环把文件都打开.
err = ffurl_open_whitelist(&uc, node_uri, flags,
&h->interrupt_callback, NULL, h->protocol_whitelist, h->protocol_blacklist, h);
并保留uc 到nodes
nodes[i].uc = uc;
nodes[i].size = size;
total_size += size;
1.2 当读数据时,会调用concat_read
static int concat_read(URLContext *h, unsigned char *buf, int size)
{
int result, total = 0;
struct concat_data *data = h->priv_data; //拿到数据上下文
struct concat_nodes *nodes = data->nodes;
size_t i = data->current;
while (size > 0) {
result = ffurl_read(nodes[i].uc, buf, size); //从URLContext 中读取数据
if (result == AVERROR_EOF) { //如果到了文件尾
if (i + 1 == data->length || //存在下一个文件
ffurl_seek(nodes[++i].uc, 0, SEEK_SET) < 0) //从下一个文件读取数据
break;
result = 0;
}
if (result < 0)
return total ? total : result;
total += result;
buf += result;
size -= result;
}
data->current = i;
return total ? total : result;
}
2. 指定file_format 为 concat 方式
举例:
创建 filelist.txt 文件,内容如下:
file ‘short1.ts’
file ‘1.ts’
ffmpeg -f concat -i filelist.txt -c copy output.mp4 -y
工作原理:
在libavformat/concatdec.c 文件有该demuxer的实现
定义了concat_read_header, concat_read_packet, concat_seek 等函数
当指定format 为concat 会找到 concat_demuxer
2.1 调用concat_read_header 时,读入文件信息
具体代码:
static int concat_read_header(AVFormatContext *avf)
{
ConcatContext *cat = avf->priv_data; //拿到上下文
int64_t time = 0;
unsigned i;
int ret = concat_parse_script(avf); //分析输入文件
if (ret < 0) return ret;
for (i = 0; i < cat->nb_files; i++) //枚举处理每一个输入文件, 但中途退出了.
{
if (cat->files[i].start_time == AV_NOPTS_VALUE)
cat->files[i].start_time = time;
else
time = cat->files[i].start_time;
if (cat->files[i].user_duration == AV_NOPTS_VALUE)
{
if (cat->files[i].inpoint == AV_NOPTS_VALUE || cat->files[i].outpoint == AV_NOPTS_VALUE ||
cat->files[i].outpoint - (uint64_t)cat->files[i].inpoint != av_sat_sub64(cat->files[i].outpoint, cat->files[i].inpoint)
)
break; //但这里中途退出了,相当于i==0, 没有给 duration 赋值
cat->files[i].user_duration = cat->files[i].outpoint - cat->files[i].inpoint;
}
cat->files[i].duration = cat->files[i].user_duration;
time += cat->files[i].user_duration;
}
if (i == cat->nb_files) {
avf->duration = time;
cat->seekable = 1;
}
cat->stream_match_mode = avf->nb_streams ? MATCH_EXACT_ID :
MATCH_ONE_TO_ONE;
if ((ret = open_file(avf, 0)) < 0) //前面代码都没有用, 此处avf已经知道了文件名,用第一个文件打开AVFormatCtx
return ret;
return 0;
}
2.2 调用concat_read_packet 来读取数据包
可以在这里控制从第一个文件中组包, 当第一个文件到达文件尾时,从第2个文件读包.
具体实现:
static int concat_read_packet(AVFormatContext *avf, AVPacket *pkt)
{
ConcatContext *cat = avf->priv_data;
int ret;
int64_t delta;
ConcatStream *cs;
AVStream *st;
FFStream *sti;
if (cat->eof)
return AVERROR_EOF;
if (!cat->avf)
return AVERROR(EIO);
while (1) {
ret = av_read_frame(cat->avf, pkt); // 靠 读取frame 来处理数据
if (ret == AVERROR_EOF) { //文件到达尾部后
if ((ret = open_next_file(avf)) < 0) //切换到下一个文件,继续读包
return ret;
continue;
}
if (ret < 0) return ret;
//流不匹配返回错误
if ((ret = match_streams(avf)) < 0) {
return ret;
}
//包位置判断
if (packet_after_outpoint(cat, pkt)) {
av_packet_unref(pkt);
if ((ret = open_next_file(avf)) < 0)
return ret;
continue;
}
//获取ConcatStream 指针供后续使用
cs = &cat->cur_file->streams[pkt->stream_index];
if (cs->out_stream_index < 0) {
av_packet_unref(pkt);
continue;
}
break;
}
if ((ret = filter_packet(avf, cs, pkt)) < 0) return ret; //检查是否需要bsf处理,一般格式会直接返回
st = cat->avf->streams[pkt->stream_index];
sti = ffstream(st);
//时间戳转换
av_log(avf, AV_LOG_DEBUG, "file:%d stream:%d pts:%s pts_time:%s dts:%s dts_time:%s",
(unsigned)(cat->cur_file - cat->files), pkt->stream_index,
av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, &st->time_base),
av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, &st->time_base));
delta = av_rescale_q(cat->cur_file->start_time - cat->cur_file->file_inpoint,
AV_TIME_BASE_Q,
cat->avf->streams[pkt->stream_index]->time_base);
if (pkt->pts != AV_NOPTS_VALUE) pkt->pts += delta;
if (pkt->dts != AV_NOPTS_VALUE) pkt->dts += delta;
av_log(avf, AV_LOG_DEBUG, " -> pts:%s pts_time:%s dts:%s dts_time:%s\n",
av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, &st->time_base),
av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, &st->time_base));
//metadata 处理
if (cat->cur_file->metadata) {
size_t metadata_len;
char* packed_metadata = av_packet_pack_dictionary(cat->cur_file->metadata, &metadata_len);
if (!packed_metadata)
return AVERROR(ENOMEM);
ret = av_packet_add_side_data(pkt, AV_PKT_DATA_STRINGS_METADATA,
packed_metadata, metadata_len);
if (ret < 0) {
av_freep(&packed_metadata);
return ret;
}
}
//时间戳处理
if (cat->cur_file->duration == AV_NOPTS_VALUE && sti->cur_dts != AV_NOPTS_VALUE) {
int64_t next_dts = av_rescale_q(sti->cur_dts, st->time_base, AV_TIME_BASE_Q);
if (cat->cur_file->next_dts == AV_NOPTS_VALUE || next_dts > cat->cur_file->next_dts) {
cat->cur_file->next_dts = next_dts;
}
}
//pkt流索引号
pkt->stream_index = cs->out_stream_index;
return 0;
}
2.3 怎样打开下一个文件
static int open_next_file(AVFormatContext *avf)
{
ConcatContext *cat = avf->priv_data; //拿到上下文
unsigned fileno = cat->cur_file - cat->files; //取到文件号 0,1,2...
cat->cur_file->duration = get_best_effort_duration(cat->cur_file, cat->avf); //获取当前文件时长
if (++fileno >= cat->nb_files) { //文件号加1 并判断是否到尾.
cat->eof = 1;
return AVERROR_EOF;
}
return open_file(avf, fileno); //用fileno 打开AVFormatContext
}
concat 作为协议,与concat 作为demux 控制的时机是不同的.
前者是读数据的时候.
后者是组包的时候.
组包是先读取数据,再从数据中挑出有用的负载组成包.
3. 使用 filter concat
举例:
ffmpeg -i input1.mp4 -i input2.mp4 -filter_complex “[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1” output.mp4 -y
v=1 和 a=1:表示只保留 1 个视频流和 1 个音频流。
编码参数中不能够使用-c copy,
Filtering and streamcopy cannot be used together.
过滤器有解码和编码参与,使得执行速度大大降低,只有2-3倍速而已. 详细过程也没分析透,此处忽略.
其它filter 使用举例.
举例:由scale 和 crop:统一分辨率。然后用overlay进行叠加的过滤器链. 这里用了3个过滤器,scale,crop,overlay
ffmpeg -i input1.mp4 -i input2.mp4 -filter_complex “[0:v]scale=1280:720[bg];[1:v]crop=1280:720[fg];[bg][fg]overlay=0:0” output.mp4