author: hjjdebug
date: 2025年 07月 22日 星期二 17:06:02 CST
descrip: 用ffmpeg 進行視頻的拼接
文章目錄
- 1. 指定協議為concat 方式.
- 1.1 協議為concat 模式,會調用 concat_open 函數
- 1.2 當讀數據時,會調用concat_read
- 2. 指定file_format 為 concat 方式
- 2.1 調用concat_read_header 時,讀入文件信息
- 2.2 調用concat_read_packet 來讀取數據包
- 2.3 怎樣打開下一個文件
- 3. 使用 filter concat
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;elsetime = 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已經知道了文件名,用第一個文件打開AVFormatCtxreturn 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