本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-zgdqtllz-dc.htmlhtml
FFmpeg封裝格式處理相關內容分爲以下幾篇文章:
[1]. FFmpeg封裝格式處理-簡介
[2]. FFmpeg封裝格式處理-解複用例程
[3]. FFmpeg封裝格式處理-複用例程
[4]. FFmpeg封裝格式處理-轉封裝例程git
複用(mux),是multiplex的縮寫,表示將多路流(視頻、音頻、字幕等)混入一路輸出中(普通文件、流等)。github
本例實現,提取第一路輸入文件中的視頻流和第二路輸入文件中的音頻流,將這兩路流混合,輸出到一路輸出文件中。緩存
本例不支持裸流輸入,是由於裸流不包含時間戳信息(時間戳信息通常由容器提供),爲裸流生成時間戳信息會增長示例代碼的複雜性。所以輸入文件有特定要求,第一路輸入文件應包含至少一路視頻流,第二路輸入文件應包含至少一路音頻流,且輸入文件必須包含封裝格式,以便能取得時間戳信息,從而可根據時間戳信息對音視頻幀排序;另外,爲了觀測輸出文件的音畫效果,第一路輸入中的視頻和第二路輸入中的音頻最好有必定的關係關係,本例中便是先從一個電影片斷中分離出視頻和音頻,用做測試輸入。ide
源碼實現步驟如註釋所述。函數
#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; }
注意兩點:測試
音頻流視頻流混合進輸出媒體時,須要確保音頻幀和視頻幀按照dts遞增的順序交錯排列,這就是交織(interleaved)問題。若是咱們使用av_interleaved_write_frame(),這個函數會緩存必定數量的幀,將將緩存的幀按照dts遞增的順序寫入輸出媒體,用戶(調用者)沒必要關注交織問題(固然,由於緩存幀數量有限,用戶不可能徹底不關注交織問題,小範圍的dts順序錯誤問題這個函數能夠修正)。若是咱們使用av_write_frame(),這個函數會直接將幀寫入輸出媒體,用戶(必須)自行處理交織問題,確保寫幀的順序嚴格按照dts遞增的順序。編碼
代碼中,經過av_compare_ts()
比較視頻幀dts和音頻幀dts哪值小,將值小的幀調用av_write_frame()
先輸出。code
運行測試命令(詳細測試方法在4.3節描述):orm
./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,數值逐行遞增。
在代碼中,讀取音頻幀或視頻幀後,調用了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值是不一樣的,說明它們的時間單位不一樣。
源文件爲muxing.c,在SHELL中執行以下編譯命令:
gcc -o muxing muxing.c -lavformat -lavcodec -lavutil -g
生成可執行文件muxing
測試文件下載: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播放不正常,這和預期是相符的。