本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10506653.html
FFmpeg封裝格式處理相關內容分為如下幾篇文章:
[1]. FFmpeg封裝格式處理-簡介
[2]. FFmpeg封裝格式處理-解復用例程
[3]. FFmpeg封裝格式處理-復用例程
[4]. FFmpeg封裝格式處理-轉封裝例程
4. 復用例程
復用(mux),是multiplex的縮寫,表示將多路流(視頻、音頻、字幕等)混入一路輸出中(普通文件、流等)。
本例實現,提取第一路輸入文件中的視頻流和第二路輸入文件中的音頻流,將這兩路流混合,輸出到一路輸出文件中。
本例不支持裸流輸入,是因為裸流不包含時間戳信息(時間戳信息一般由容器提供),為裸流生成時間戳信息會增加示例代碼的復雜性。因此輸入文件有特定要求,第一路輸入文件應包含至少一路視頻流,第二路輸入文件應包含至少一路音頻流,且輸入文件必須包含封裝格式,以便能取得時間戳信息,從而可根據時間戳信息對音視頻幀排序;另外,為了觀測輸出文件的音畫效果,第一路輸入中的視頻和第二路輸入中的音頻最好有一定的關系關系,本例中即是先從一個電影片段中分離出視頻和音頻,用作測試輸入。
4.1 源碼
源碼實現步驟如注釋所述。
#include <stdbool.h>
#include <libavformat/avformat.h>
/*
ffmpeg -i tnmil.flv -c:v copy -an tnmil_v.flv
ffmpeg -i tnmil.flv -c:a copy -vn tnmil_a.flv
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
*/
int main (int argc, char **argv)
{
if (argc != 4)
{
fprintf(stderr, "usage: %s test.h264 test.aac test.ts\n", argv[0]);
exit(1);
}
const char *input_v_fname = argv[1];
const char *input_a_fname = argv[2];
const char *output_fname = argv[3];
int ret = 0;
// 1 打開兩路輸入
// 1.1 打開第一路輸入,並找到一路視頻流
AVFormatContext *v_ifmt_ctx = NULL;
ret = avformat_open_input(&v_ifmt_ctx, input_v_fname, NULL, NULL);
ret = avformat_find_stream_info(v_ifmt_ctx, NULL);
int video_idx = av_find_best_stream(v_ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
AVStream *in_v_stream = v_ifmt_ctx->streams[video_idx];
// 1.2 打開第二路輸入,並找到一路音頻流
AVFormatContext *a_ifmt_ctx = NULL;
ret = avformat_open_input(&a_ifmt_ctx, input_a_fname, NULL, NULL);
ret = avformat_find_stream_info(a_ifmt_ctx, NULL);
int audio_idx = av_find_best_stream(a_ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
AVStream *in_a_stream = a_ifmt_ctx->streams[audio_idx];
av_dump_format(v_ifmt_ctx, 0, input_v_fname, 0);
av_dump_format(a_ifmt_ctx, 1, input_a_fname, 0);
if (video_idx < 0 || audio_idx < 0)
{
printf("find stream failed: %d %d\n", video_idx, audio_idx);
return -1;
}
// 2 打開輸出,並向輸出中添加兩路流,一路用於存儲視頻,一路用於存儲音頻
AVFormatContext *ofmt_ctx = NULL;
ret = avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, output_fname);
AVStream *out_v_stream = avformat_new_stream(ofmt_ctx, NULL);
ret = avcodec_parameters_copy(out_v_stream->codecpar, in_v_stream->codecpar);
AVStream *out_a_stream = avformat_new_stream(ofmt_ctx, NULL);
ret = avcodec_parameters_copy(out_a_stream->codecpar, in_a_stream->codecpar);
if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) // TODO: 研究AVFMT_NOFILE標志
{
ret = avio_open(&ofmt_ctx->pb, output_fname, AVIO_FLAG_WRITE);
}
av_dump_format(ofmt_ctx, 0, output_fname, 1);
// 3 寫輸入文件頭
ret = avformat_write_header(ofmt_ctx, NULL);
AVPacket vpkt;
av_init_packet(&vpkt);
vpkt.data = NULL;
vpkt.size = 0;
AVPacket apkt;
av_init_packet(&apkt);
apkt.data = NULL;
apkt.size = 0;
AVPacket *p_pkt = NULL;
int64_t vdts = 0;
int64_t adts = 0;
bool video_finished = false;
bool audio_finished = false;
bool v_or_a = false;
// 4 從兩路輸入依次取得packet,交織存入輸出中
printf("V/A\tPTS\tDTS\tSIZE\n");
while (1)
{
if (vpkt.data == NULL && (!video_finished))
{
while (1) // 取出一個video packet,退出循環
{
ret = av_read_frame(v_ifmt_ctx, &vpkt);
if ((ret == AVERROR_EOF) || avio_feof(v_ifmt_ctx->pb))
{
printf("video finished\n");
video_finished = true;
vdts = AV_NOPTS_VALUE;
break;
}
else if (ret < 0)
{
printf("video read error\n");
goto end;
}
if (vpkt.stream_index == video_idx)
{
// 更新packet中的pts和dts。關於AVStream.time_base的說明:
// 輸入:輸入流中含有time_base,在avformat_find_stream_info()中可取到每個流中的time_base
// 輸出:avformat_write_header()會根據輸出的封裝格式確定每個流的time_base並寫入文件中
// AVPacket.pts和AVPacket.dts的單位是AVStream.time_base,不同的封裝格式其AVStream.time_base不同
// 所以輸出文件中,每個packet需要根據輸出封裝格式重新計算pts和dts
av_packet_rescale_ts(&vpkt, in_v_stream->time_base, out_v_stream->time_base);
vpkt.pos = -1; // 讓muxer根據重新將packet在輸出容器中排序
vpkt.stream_index = 0;
vdts = vpkt.dts;
break;
}
av_packet_unref(&vpkt);
}
}
if (apkt.data == NULL && (!audio_finished))
{
while (1) // 取出一個audio packet,退出循環
{
ret = av_read_frame(a_ifmt_ctx, &apkt);
if ((ret == AVERROR_EOF) || avio_feof(a_ifmt_ctx->pb))
{
printf("audio finished\n");
audio_finished = true;
adts = AV_NOPTS_VALUE;
break;
}
else if (ret < 0)
{
printf("audio read error\n");
goto end;
}
if (apkt.stream_index == audio_idx)
{
ret = av_compare_ts(vdts, out_v_stream->time_base, adts, out_a_stream->time_base);
apkt.pos = -1;
apkt.stream_index = 1;
adts = apkt.dts;
break;
}
av_packet_unref(&apkt);
}
}
if (video_finished && audio_finished)
{
printf("all read finished. flushing queue.\n");
//av_interleaved_write_frame(ofmt_ctx, NULL); // 沖洗交織隊列
break;
}
else // 音頻或視頻未讀完
{
if (video_finished) // 視頻讀完,音頻未讀完
{
v_or_a = false;
}
else if (audio_finished) // 音頻讀完,視頻未讀完
{
v_or_a = true;
}
else // 音頻視頻都未讀完
{
// video pakect is before audio packet?
ret = av_compare_ts(vdts, in_v_stream->time_base, adts, in_a_stream->time_base);
v_or_a = (ret <= 0);
}
p_pkt = v_or_a ? &vpkt : &apkt;
printf("%s\t%3"PRId64"\t%3"PRId64"\t%-5d\n", v_or_a ? "vp" : "ap",
p_pkt->pts, p_pkt->dts, p_pkt->size);
//ret = av_interleaved_write_frame(ofmt_ctx, p_pkt);
ret = av_write_frame(ofmt_ctx, p_pkt);
if (p_pkt->data != NULL)
{
av_packet_unref(p_pkt);
}
}
}
// 5 寫輸出文件尾
av_write_trailer(ofmt_ctx);
printf("Muxing succeeded.\n");
end:
avformat_close_input(&v_ifmt_ctx);
avformat_close_input(&a_ifmt_ctx);
avformat_free_context(ofmt_ctx);
return 0;
}
注意兩點:
4.1.1 音視頻幀交織問題
音頻流視頻流混合進輸出媒體時,需要確保音頻幀和視頻幀按照dts遞增的順序交錯排列,這就是交織(interleaved)問題。如果我們使用av_interleaved_write_frame(),這個函數會緩存一定數量的幀,將將緩存的幀按照dts遞增的順序寫入輸出媒體,用戶(調用者)不必關注交織問題(當然,因為緩存幀數量有限,用戶不可能完全不關注交織問題,小范圍的dts順序錯誤問題這個函數可以修正)。如果我們使用av_write_frame(),這個函數會直接將幀寫入輸出媒體,用戶(必須)自行處理交織問題,確保寫幀的順序嚴格按照dts遞增的順序。
代碼中,通過av_compare_ts()
比較視頻幀dts和音頻幀dts哪值小,將值小的幀調用av_write_frame()
先輸出。
運行測試命令(詳細測試方法在4.3節描述):
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
抓取一段打印看一下:
V/A PTS DTS SIZE
vp 80 0 12840
ap 0 0 368
ap 23 23 364
vp 240 40 4346
ap 46 46 365
ap 70 70 365
vp 160 80 1257
ap 93 93 368
ap 116 116 367
vp 120 120 626
ap 139 139 367
vp 200 160 738
ap 163 163 367
ap 186 186 367
vp 400 200 4938
可以看到,第三列DTS,數值逐行遞增。
4.1.2 時間域轉換問題
在代碼中,讀取音頻幀或視頻幀后,調用了av_packet_rescale_ts()
將幀中的時間相關值(pts、dts、duration)進行了時基轉換,從輸入流的時基轉換為輸出流的時間基(time_base)。pts/dts的單位是time_base,pts/dts的值乘以time_base表示時刻值。不同的封裝格式,其時間基(time_base)不同,所以需要進行轉換。當然,如果輸出封裝格式和輸入封裝格式相同,那不調用av_packet_rescale_ts()
也可以。
封裝格式中的時間基就是流中的時間基AVStream.time_base,關於AVStream.time_base的說明:
輸入:輸入流中含有time_base,在avformat_find_stream_info()中可取到每個流中的time_base
輸出:avformat_write_header()會根據輸出的封裝格式確定每個流的time_base並寫入文件中
我們對比看一下,ts封裝格式和flv封裝格式的不同,運行測試命令(詳細測試方法在4.3節描述):
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts
看一下前15幀的打印信息:
V/A PTS DTS SIZE
vp 7200 0 12840
ap 0 0 368
ap 2070 2070 364
vp 21600 3600 4346
ap 4140 4140 365
ap 6300 6300 365
vp 14400 7200 1257
ap 8370 8370 368
ap 10440 10440 367
vp 10800 10800 626
ap 12510 12510 367
vp 18000 14400 738
ap 14670 14670 367
ap 16740 16740 367
vp 36000 18000 4938
和上一節flv封裝格式打印信息對比一下,不同封裝格式中同樣的一幀數據,其解碼時刻和播放時刻肯定是一樣的,但其PTS/DTS值是不同的,說明它們的時間單位不同。
4.2 編譯
源文件為muxing.c,在SHELL中執行如下編譯命令:
gcc -o muxing muxing.c -lavformat -lavcodec -lavutil -g
生成可執行文件muxing
4.3 驗證
測試文件下載:tnmil.flv
先看一下測試用資源文件的格式:
think@opensuse> ffprobe tnmil.flv
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, flv, from 'tnmil.flv':
Metadata:
encoder : Lavf58.20.100
Duration: 00:00:54.52, start: 0.000000, bitrate: 611 kb/s
Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp
可以看到視頻文件'tnmil.flv'封裝格式為flv,包含一路h264編碼的視頻流和一路aac編碼的音頻流。
運行如下兩條命令,處理一下,生成只含一路視頻流的文件,和只含一路音頻流的文件,文件封裝格式均為FLV。這兩個文件用於下一步的測試。
ffmpeg -i tnmil.flv -c:v copy -an tnmil_v.flv
ffmpeg -i tnmil.flv -c:a copy -vn tnmil_a.flv
不輸出裸流,而輸出帶封裝格式的流,就是為了利用封裝格式中攜帶的時間戳信息,簡化本例程。
運行如下命令進行測試:
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.flv
使用ffprobe檢測輸出文件正常。使用ffplay播放輸出文件正常,播放效果和原始的測試文件一致。
輸出另外一路封裝格式的文件再測試一下,運行如下命令:
./muxing tnmil_v.flv tnmil_a.flv tnmil_av.ts
使用ffprobe檢測輸出文件正常。使用ffplay播放輸出文件正常,播放效果和原始的測試文件一致。
如果我們改一下代碼,將av_packet_rescale_ts()
注釋掉,再測上述兩條指令,發現tnmil_av.flv播放正常,tnmil_av.ts播放不正常,這和預期是相符的。