用ffmpeg 进行视频的拼接

发布于:2025-07-24 ⋅ 阅读:(12) ⋅ 点赞:(0)

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


网站公告

今日签到

点亮在社区的每一天
去签到