一、FFMPEG的封裝格式轉換器(無編解碼)
1.封裝格式轉換
所謂的封裝格式轉換,就是在AVI,FLV,MKV,MP4這些格式之間轉換(對應.avi,.flv,.mkv,.mp4文件)。
需要注意的是,本程序並不進行視音頻的編碼和解碼工作。而是直接將視音頻壓縮碼流從一種封裝格式文件中獲取出來然后打包成另外一種封裝格式的文件。
本程序的工作原理如下圖1所示:
由圖可見,本程序並不進行視頻和音頻的編解碼工作,因此本程序和普通的轉碼軟件相比,有以下兩個特點:
處理速度極快。視音頻編解碼算法十分復雜,占據了轉碼的絕大部分時間。因為不需要進行視音頻的編碼和解碼,所以節約了大量的時間。
視音頻質量無損。因為不需要進行視音頻的編碼和解碼,所以不會有視音頻的壓縮損傷。
2.基於FFmpeg的Remuxer的流程圖
下面附上基於FFmpeg的Remuxer的流程圖。圖2中使用淺紅色標出了關鍵的數據結構,淺藍色標出了輸出視頻數據的函數。
可見成個程序包含了對兩個文件的處理:讀取輸入文件(位於左邊)和寫入輸出文件(位於右邊)。中間使用了一個avcodec_copy_context()拷貝輸入的AVCodecContext到輸出的AVCodecContext。
簡單介紹一下流程中關鍵函數的意義:
輸入文件操作:
avformat_open_input():打開輸入文件,初始化輸入視頻碼流的AVFormatContext。
av_read_frame():從輸入文件中讀取一個AVPacket。
輸出文件操作:
avformat_alloc_output_context2():初始化輸出視頻碼流的AVFormatContext。
avformat_new_stream():創建輸出碼流的AVStream。
avcodec_copy_context():拷貝輸入視頻碼流的AVCodecContex的數值t到輸出視頻的AVCodecContext。
avio_open():打開輸出文件。
avformat_write_header():寫文件頭(對於某些沒有文件頭的封裝格式,不需要此函數。比如說MPEG2TS)。
av_interleaved_write_frame():將AVPacket(存儲視頻壓縮碼流數據)寫入文件。
av_write_trailer():寫文件尾(對於某些沒有文件頭的封裝格式,不需要此函數。比如說MPEG2TS)。
二、FFmpegRemuxer代碼
基於FFmpeg的封裝格式轉換器,取了個名字稱為FFmpegRemuxer
主要是FFmpegRemuxer.cpp文件,代碼如下(基本上每一行都有注釋):

1 /***************************************************************************** 2 * Copyright (C) 2017-2020 Hanson Yu All rights reserved. 3 ------------------------------------------------------------------------------ 4 * File Module : FFmpegRemuxer.cpp 5 * Description : FFmpegRemuxer Demo 6 7 輸出結果: 8 Input #0, flv, from 'cuc_ieschool1.flv': 9 Metadata: 10 metadatacreator : iku 11 hasKeyframes : true 12 hasVideo : true 13 hasAudio : true 14 hasMetadata : true 15 canSeekToEnd : false 16 datasize : 932906 17 videosize : 787866 18 audiosize : 140052 19 lasttimestamp : 34 20 lastkeyframetimestamp: 30 21 lastkeyframelocation: 886498 22 encoder : Lavf55.19.104 23 Duration: 00:00:34.20, start: 0.042000, bitrate: 394 kb/s 24 Stream #0:0: Video: h264 (High), yuv420p, 512x288 [SAR 1:1 DAR 16:9], 15.17 fps, 15 tbr, 1k tbn, 30 tbc 25 Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s 26 Output #0, mp4, to 'cuc_ieschool1.mp4': 27 Stream #0:0: Video: h264, yuv420p, 512x288 [SAR 1:1 DAR 16:9], q=2-31, 90k tbn, 30 tbc 28 Stream #0:1: Audio: mp3, 44100 Hz, stereo, s16p, 128 kb/s 29 Write 0 frames to output file 30 Write 1 frames to output file 31 Write 2 frames to output file 32 Write 3 frames to output file 33 . 34 . 35 . 36 37 * Created : 2017.09.21. 38 * Author : Yu Weifeng 39 * Function List : 40 * Last Modified : 41 * History : 42 * Modify Date Version Author Modification 43 * ----------------------------------------------- 44 * 2017/09/21 V1.0.0 Yu Weifeng Created 45 ******************************************************************************/ 46 #include <stdio.h> 47 48 49 /* 50 __STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h 51 macros specified in the C99 standard that aren't in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN, 52 and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide 53 if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS 54 and __STDC_CONSTANT_MACROS be defined before stdint.h is included. 55 56 This isn't part of the C++ standard, but it has been adopted by more than one implementation. 57 */ 58 #define __STDC_CONSTANT_MACROS 59 60 61 #ifdef _WIN32//Windows 62 extern "C" 63 { 64 #include "libavformat/avformat.h" 65 }; 66 #else//Linux... 67 #ifdef __cplusplus 68 extern "C" 69 { 70 #endif 71 #include <libavformat/avformat.h> 72 #ifdef __cplusplus 73 }; 74 #endif 75 #endif 76 77 /***************************************************************************** 78 -Fuction : main 79 -Description : main 80 -Input : 81 -Output : 82 -Return : 83 * Modify Date Version Author Modification 84 * ----------------------------------------------- 85 * 2017/09/21 V1.0.0 Yu Weifeng Created 86 ******************************************************************************/ 87 int main(int argc, char* argv[]) 88 { 89 AVOutputFormat * ptOutputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header(). 90 AVFormatContext * ptInFormatContext = NULL;//輸入文件的封裝格式上下文,內部包含所有的視頻信息 91 AVFormatContext * ptOutFormatContext = NULL;//輸出文件的封裝格式上下文,內部包含所有的視頻信息 92 AVPacket tOutPacket ={0};//存儲一幀壓縮編碼數據給輸出文件 93 const char * strInFileName=NULL, * strOutFileName = NULL;//輸入文件名和輸出文件名 94 int iRet, i; 95 int iFrameCount = 0;//輸出的幀個數 96 AVStream * ptInStream=NULL,* ptOutStream=NULL;//輸入音視頻流和輸出音視頻流 97 98 if(argc!=3)//argc包括argv[0]也就是程序名稱 99 { 100 printf("Usage:%s InputFileURL OutputFileURL\r\n",argv[0]); 101 printf("For example:\r\n"); 102 printf("%s InputFile.flv OutputFile.mp4\r\n",argv[0]); 103 return -1; 104 } 105 strInFileName = argv[1];//Input file URL 106 strOutFileName = argv[2];//Output file URL 107 108 av_register_all();//注冊FFmpeg所有組件 109 110 /*------------Input------------*/ 111 if ((iRet = avformat_open_input(&ptInFormatContext, strInFileName, 0, 0)) < 0) 112 {//打開輸入視頻文件 113 printf("Could not open input file\r\n"); 114 } 115 else 116 { 117 if ((iRet = avformat_find_stream_info(ptInFormatContext, 0)) < 0) 118 {//獲取視頻文件信息 119 printf("Failed to find input stream information\r\n"); 120 } 121 else 122 { 123 av_dump_format(ptInFormatContext, 0, strInFileName, 0);//手工調試的函數,內部是log,輸出相關的格式信息到log里面 124 125 /*------------Output------------*/ 126 127 /*初始化一個用於輸出的AVFormatContext結構體 128 *ctx:函數調用成功之后創建的AVFormatContext結構體。 129 *oformat:指定AVFormatContext中的AVOutputFormat,用於確定輸出格式。如果指定為NULL, 130 可以設定后兩個參數(format_name或者filename)由FFmpeg猜測輸出格式。 131 PS:使用該參數需要自己手動獲取AVOutputFormat,相對於使用后兩個參數來說要麻煩一些。 132 *format_name:指定輸出格式的名稱。根據格式名稱,FFmpeg會推測輸出格式。輸出格式可以是“flv”,“mkv”等等。 133 *filename:指定輸出文件的名稱。根據文件名稱,FFmpeg會推測輸出格式。文件名稱可以是“xx.flv”,“yy.mkv”等等。 134 函數執行成功的話,其返回值大於等於0 135 */ 136 avformat_alloc_output_context2(&ptOutFormatContext, NULL, NULL, strOutFileName); 137 if (!ptOutFormatContext) 138 { 139 printf("Could not create output context\r\n"); 140 iRet = AVERROR_UNKNOWN; 141 } 142 else 143 { 144 ptOutputFormat = ptOutFormatContext->oformat; 145 for (i = 0; i < ptInFormatContext->nb_streams; i++) 146 { 147 //Create output AVStream according to input AVStream 148 ptInStream = ptInFormatContext->streams[i]; 149 ptOutStream = avformat_new_stream(ptOutFormatContext, ptInStream->codec->codec);//給ptOutFormatContext中的流數組streams中的 150 if (!ptOutStream) //一條流(數組中的元素)分配空間,也正是由於這里分配了空間,后續操作直接拷貝編碼數據(pkt)就可以了。 151 { 152 printf("Failed allocating output stream\r\\n"); 153 iRet = AVERROR_UNKNOWN; 154 break; 155 } 156 else 157 { 158 if (avcodec_copy_context(ptOutStream->codec, ptInStream->codec) < 0) //Copy the settings of AVCodecContext 159 { 160 printf("Failed to copy context from input to output stream codec context\r\n"); 161 iRet = AVERROR_UNKNOWN; 162 break; 163 } 164 else 165 { 166 ptOutStream->codec->codec_tag = 0; 167 if (ptOutFormatContext->oformat->flags & AVFMT_GLOBALHEADER) 168 ptOutStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; 169 170 } 171 } 172 } 173 if(AVERROR_UNKNOWN == iRet) 174 { 175 } 176 else 177 { 178 av_dump_format(ptOutFormatContext, 0, strOutFileName, 1);//Output information------------------ 179 //Open output file 180 if (!(ptOutputFormat->flags & AVFMT_NOFILE)) 181 { /*打開FFmpeg的輸入輸出文件,使后續讀寫操作可以執行 182 *s:函數調用成功之后創建的AVIOContext結構體。 183 *url:輸入輸出協議的地址(文件也是一種“廣義”的協議,對於文件來說就是文件的路徑)。 184 *flags:打開地址的方式。可以選擇只讀,只寫,或者讀寫。取值如下。 185 AVIO_FLAG_READ:只讀。AVIO_FLAG_WRITE:只寫。AVIO_FLAG_READ_WRITE:讀寫。*/ 186 iRet = avio_open(&ptOutFormatContext->pb, strOutFileName, AVIO_FLAG_WRITE); 187 if (iRet < 0) 188 { 189 printf("Could not open output file %s\r\n", strOutFileName); 190 } 191 else 192 { 193 //Write file header 194 if (avformat_write_header(ptOutFormatContext, NULL) < 0) //avformat_write_header()中最關鍵的地方就是調用了AVOutputFormat的write_header() 195 {//不同的AVOutputFormat有不同的write_header()的實現方法 196 printf("Error occurred when opening output file\r\n"); 197 } 198 else 199 { 200 while (1) 201 { 202 //Get an AVPacket 203 iRet = av_read_frame(ptInFormatContext, &tOutPacket);//從輸入文件讀取一幀壓縮數據 204 if (iRet < 0) 205 break; 206 207 ptInStream = ptInFormatContext->streams[tOutPacket.stream_index]; 208 ptOutStream = ptOutFormatContext->streams[tOutPacket.stream_index]; 209 //Convert PTS/DTS 210 tOutPacket.pts = av_rescale_q_rnd(tOutPacket.pts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); 211 tOutPacket.dts = av_rescale_q_rnd(tOutPacket.dts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); 212 tOutPacket.duration = av_rescale_q(tOutPacket.duration, ptInStream->time_base, ptOutStream->time_base); 213 tOutPacket.pos = -1; 214 //Write 215 /*av_interleaved_write_frame包括interleave_packet()以及write_packet(),將還未輸出的AVPacket輸出出來 216 *write_packet()函數最關鍵的地方就是調用了AVOutputFormat中寫入數據的方法。write_packet()實際上是一個函數指針, 217 指向特定的AVOutputFormat中的實現函數*/ 218 if (av_interleaved_write_frame(ptOutFormatContext, &tOutPacket) < 0) 219 { 220 printf("Error muxing packet\r\n"); 221 break; 222 } 223 printf("Write %8d frames to output file\r\n", iFrameCount); 224 av_free_packet(&tOutPacket);//釋放空間 225 iFrameCount++; 226 } 227 //Write file trailer//av_write_trailer()中最關鍵的地方就是調用了AVOutputFormat的write_trailer() 228 av_write_trailer(ptOutFormatContext);//不同的AVOutputFormat有不同的write_trailer()的實現方法 229 } 230 if (ptOutFormatContext && !(ptOutputFormat->flags & AVFMT_NOFILE)) 231 avio_close(ptOutFormatContext->pb);//該函數用於關閉一個AVFormatContext->pb,一般情況下是和avio_open()成對使用的。 232 } 233 } 234 } 235 avformat_free_context(ptOutFormatContext);//釋放空間 236 } 237 } 238 avformat_close_input(&ptInFormatContext);//該函數用於關閉一個AVFormatContext,一般情況下是和avformat_open_input()成對使用的。 239 } 240 return 0; 241 }
具體代碼見github:
https://github.com/fengweiyu/FFmpegFormat/FFmpegRemuxer
三、FFmpeg的封裝格式處理:視音頻復用器(muxer)
1.封裝格式處理
視音頻復用器(Muxer)即是將視頻壓縮數據(例如H.264)和音頻壓縮數據(例如AAC)合並到一個封裝格式數據(例如MKV)中去。
如圖3所示。在這個過程中並不涉及到編碼和解碼。
2.基於FFmpeg的muxer的流程圖
程序的流程如下圖4所示。從流程圖中可以看出,一共初始化了3個AVFormatContext,其中2個用於輸入,1個用於輸出。3個AVFormatContext初始化之后,通過avcodec_copy_context()函數可以將輸入視頻/音頻的參數拷貝至輸出視頻/音頻的AVCodecContext結構體。
然后分別調用視頻輸入流和音頻輸入流的av_read_frame(),從視頻輸入流中取出視頻的AVPacket,音頻輸入流中取出音頻的AVPacket,分別將取出的AVPacket寫入到輸出文件中即可。
其間用到了一個不太常見的函數av_compare_ts(),是比較時間戳用的。通過該函數可以決定該寫入視頻還是音頻。
本文介紹的視音頻復用器,輸入的視頻不一定是H.264裸流文件,音頻也不一定是純音頻文件。可以選擇兩個封裝過的視音頻文件作為輸入。程序會從視頻輸入文件中“挑”出視頻流,音頻輸入文件中“挑”出音頻流,再將“挑選”出來的視音頻流復用起來。
PS1:對於某些封裝格式(例如MP4/FLV/MKV等)中的H.264,需要用到名稱為“h264_mp4toannexb”的bitstream filter。
PS2:對於某些封裝格式(例如MP4/FLV/MKV等)中的AAC,需要用到名稱為“aac_adtstoasc”的bitstream filter。
簡單介紹一下流程中各個重要函數的意義:
avformat_open_input():打開輸入文件。
avcodec_copy_context():賦值AVCodecContext的參數。
avformat_alloc_output_context2():初始化輸出文件。
avio_open():打開輸出文件。
avformat_write_header():寫入文件頭。
av_compare_ts():比較時間戳,決定寫入視頻還是寫入音頻。這個函數相對要少見一些。
av_read_frame():從輸入文件讀取一個AVPacket。
av_interleaved_write_frame():寫入一個AVPacket到輸出文件。
av_write_trailer():寫入文件尾。
3.優化為可以從內存中讀取音視頻數據
打開文件的函數是avformat_open_input(),直接將文件路徑或者流媒體URL的字符串傳遞給該函數就可以了。
但其是否支持從內存中讀取數據呢?
分析ffmpeg的源代碼,發現其竟然是可以從內存中讀取數據的,代碼很簡單,如下所示:
ptInFormatContext = avformat_alloc_context();
pbIoBuf = (unsigned char *)av_malloc(IO_BUFFER_SIZE);
ptAVIO = avio_alloc_context(pbIoBuf, IO_BUFFER_SIZE, 0, NULL, FillIoBuffer, NULL, NULL);
ptInFormatContext->pb = ptAVIO;
ptInputFormat = av_find_input_format("h264");//得到ptInputFormat以便后面打開使用
if ((iRet = avformat_open_input(&ptInFormatContext, "", ptInputFormat, NULL)) < 0)
{
printf("Could not open input file\r\n");
}
else
{
}
關鍵要在avformat_open_input()之前初始化一個AVIOContext,而且將原本的AVFormatContext的指針pb(AVIOContext類型)指向這個自行初始化AVIOContext。
當自行指定了AVIOContext之后,avformat_open_input()里面的URL參數就不起作用了。示例代碼開辟了一塊空間iobuffer作為AVIOContext的緩存。
FillIoBuffer則是將數據讀取至iobuffer的回調函數。FillIoBuffer()形式(參數,返回值)是固定的,是一個回調函數,如下所示(只是個例子,具體怎么讀取數據可以自行設計)。
示例中回調函數將文件中的內容通過fread()讀入內存。
int FillIoBuffer(void *opaque, unsigned char *o_pbBuf, int i_iMaxSize)
{
int iRet=-1;
if (!feof(g_fileH264))
{
iRet = fread(o_pbBuf, 1, i_iMaxSize, g_fileH264);
}
else
{
}
return iRet;
}
整體結構大致如下:
FILE *fp_open;
int fill_iobuffer(void *opaque, uint8_t *buf, int buf_size){
...
}
int main(){
...
fp_open=fopen("test.h264","rb+");
AVFormatContext *ic = NULL;
ic = avformat_alloc_context();
unsigned char * iobuffer=(unsigned char *)av_malloc(32768);
AVIOContext *avio =avio_alloc_context(iobuffer, 32768,0,NULL,fill_iobuffer,NULL,NULL);
ic->pb=avio;
err = avformat_open_input(&ic, "nothing", NULL, NULL);
...//解碼
}
4.將音視頻數據輸出到內存
同時再說明一下,和從內存中讀取數據類似,ffmpeg也可以將處理后的數據輸出到內存。
回調函數如下示例,可以將輸出到內存的數據寫入到文件中。
//寫文件的回調函數
int write_buffer(void *opaque, uint8_t *buf, int buf_size){
if(!feof(fp_write)){
int true_size=fwrite(buf,1,buf_size,fp_write);
return true_size;
}else{
return -1;
}
}
主函數如下所示:
FILE *fp_write;
int write_buffer(void *opaque, uint8_t *buf, int buf_size){
...
}
main(){
...
fp_write=fopen("src01.h264","wb+"); //輸出文件
...
AVFormatContext* ofmt_ctx=NULL;
avformat_alloc_output_context2(&ofmt_ctx, NULL, "h264", NULL);
unsigned char* outbuffer=(unsigned char*)av_malloc(32768);
AVIOContext *avio_out =avio_alloc_context(outbuffer, 32768,0,NULL,NULL,write_buffer,NULL);
ofmt_ctx->pb=avio_out;
ofmt_ctx->flags=AVFMT_FLAG_CUSTOM_IO;
...
}
從上述可以很明顯的看到,知道把寫回調函數放到avio_alloc_context函數對應的位置就可以了。
四、FFmpegMuxer代碼
基於FFmpeg的視音頻復用器,取了個名字稱為FFmpegMuxer
主要是FFmpegMuxer.cpp文件,代碼如下(基本上每一行都有注釋):

1 /***************************************************************************** 2 * Copyright (C) 2017-2020 Hanson Yu All rights reserved. 3 ------------------------------------------------------------------------------ 4 * File Module : FFmpegMuxer.cpp 5 * Description : FFmpegMuxer Demo 6 7 *先將H.264文件讀入內存, 8 *再輸出封裝格式文件。 9 10 輸出結果: 11 book@book-desktop:/work/project/FFmpegMuxer$ make clean;make 12 rm FFmpegMuxer 13 g++ FFmpegMuxer.cpp -I ./include -rdynamic ./lib/libavformat.so.57 ./lib/libavcodec.so.57 ./lib/libavutil.so.55 ./lib/libswresample.so.2 -o FFmpegMuxer 14 book@book-desktop:/work/project/FFmpegMuxer$ export LD_LIBRARY_PATH=./lib 15 book@book-desktop:/work/project/FFmpegMuxer$ ./FFmpegMuxer sintel.h264 sintel.mp4 16 Input #0, h264, from 'sintel.h264': 17 Duration: N/A, bitrate: N/A 18 Stream #0:0: Video: h264 (High), yuv420p(progressive), 640x360, 25 fps, 25 tbr, 1200k tbn, 50 tbc 19 Output #0, mp4, to 'sintel.mp4': 20 Stream #0:0: Unknown: none 21 [mp4 @ 0x9352d80] Using AVStream.codec.time_base as a timebase hint to the muxer is deprecated. Set AVStream.time_base instead. 22 [mp4 @ 0x9352d80] Using AVStream.codec to pass codec parameters to muxers is deprecated, use AVStream.codecpar instead. 23 Write iFrameIndex:1,stream_index:0,num:25,den:1 24 Write iFrameIndex:2,stream_index:0,num:25,den:1 25 Write iFrameIndex:3,stream_index:0,num:25,den:1 26 . 27 . 28 . 29 30 * Created : 2017.09.21. 31 * Author : Yu Weifeng 32 * Function List : 33 * Last Modified : 34 * History : 35 * Modify Date Version Author Modification 36 * ----------------------------------------------- 37 * 2017/09/21 V1.0.0 Yu Weifeng Created 38 ******************************************************************************/ 39 #include <stdio.h> 40 41 42 /* 43 __STDC_LIMIT_MACROS and __STDC_CONSTANT_MACROS are a workaround to allow C++ programs to use stdint.h 44 macros specified in the C99 standard that aren't in the C++ standard. The macros, such as UINT8_MAX, INT64_MIN, 45 and INT32_C() may be defined already in C++ applications in other ways. To allow the user to decide 46 if they want the macros defined as C99 does, many implementations require that __STDC_LIMIT_MACROS 47 and __STDC_CONSTANT_MACROS be defined before stdint.h is included. 48 49 This isn't part of the C++ standard, but it has been adopted by more than one implementation. 50 */ 51 #define __STDC_CONSTANT_MACROS 52 53 54 #ifdef _WIN32//Windows 55 extern "C" 56 { 57 #include "libavformat/avformat.h" 58 }; 59 #else//Linux... 60 #ifdef __cplusplus 61 extern "C" 62 { 63 #endif 64 #include <libavformat/avformat.h> 65 #ifdef __cplusplus 66 }; 67 #endif 68 #endif 69 70 #define IO_BUFFER_SIZE 32768 //緩存32k 71 72 static FILE * g_fileH264=NULL; 73 74 75 /***************************************************************************** 76 -Fuction : FillIoBuffer 77 -Description : FillIoBuffer 78 79 *在avformat_open_input()中會首次調用該回調函數, 80 *第二次一直到最后一次都是在avformat_find_stream_info()中循環調用, 81 *文件中的數據每次IO_BUFFER_SIZE字節讀入到內存中, 82 *經過ffmpeg處理,所有數據被有序地逐幀存儲到AVPacketList中。 83 *以上是緩存設為32KB的情況,緩存大小設置不同,調用機制也有所不同。 84 85 -Input : 86 -Output : 87 -Return : 返回讀取的長度 88 * Modify Date Version Author Modification 89 * ----------------------------------------------- 90 * 2017/09/21 V1.0.0 Yu Weifeng Created 91 ******************************************************************************/ 92 int FillIoBuffer(void *opaque, unsigned char *o_pbBuf, int i_iMaxSize) 93 { 94 int iRet=-1; 95 if (!feof(g_fileH264)) 96 { 97 iRet = fread(o_pbBuf, 1, i_iMaxSize, g_fileH264); 98 } 99 else 100 { 101 } 102 return iRet; 103 } 104 105 /***************************************************************************** 106 -Fuction : main 107 -Description : main 108 關鍵要在avformat_open_input()之前初始化一個AVIOContext, 109 而且將原本的AVFormatContext的指針pb(AVIOContext類型)指向這個自行初始化AVIOContext 110 -Input : 111 -Output : 112 -Return : 113 * Modify Date Version Author Modification 114 * ----------------------------------------------- 115 * 2017/09/21 V1.0.0 Yu Weifeng Created 116 ******************************************************************************/ 117 int main(int argc, char* argv[]) 118 { 119 AVInputFormat * ptInputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header(). 120 AVOutputFormat * ptOutputFormat = NULL;//The output container format.Muxing only, must be set by the caller before avformat_write_header(). 121 AVFormatContext * ptInFormatContext = NULL;//輸入文件的封裝格式上下文,內部包含所有的視頻信息 122 AVFormatContext * ptOutFormatContext = NULL;//輸出文件的封裝格式上下文,內部包含所有的視頻信息 123 AVPacket tOutPacket ={0};//存儲一幀壓縮編碼數據給輸出文件 124 const char * strInVideoFileName=NULL, * strOutFileName = NULL;//輸入文件名和輸出文件名 125 int iRet, i; 126 int iVideoStreamIndex = -1;//視頻流應該處在的位置 127 int iFrameIndex = 0; 128 long long llCurrentPts = 0; 129 int iOutVideoStreamIndex = -1; //輸出流中的視頻流所在的位置 130 AVStream * ptInStream=NULL,* ptOutStream=NULL;//輸入音視頻流和輸出音視頻流 131 unsigned char * pbIoBuf=NULL;//io數據緩沖區 132 AVIOContext * ptAVIO=NULL;//AVIOContext管理輸入輸出數據的結構體 133 134 if(argc!=3)//argc包括argv[0]也就是程序名稱 135 { 136 printf("Usage:%s InputVideoFileURL OutputFileURL\r\n",argv[0]); 137 printf("For example:\r\n"); 138 printf("%s InputFile.h264 OutputFile.mp4\r\n",argv[0]); 139 return -1; 140 } 141 strInVideoFileName = argv[1];//Input file URL 142 strOutFileName = argv[2];//Output file URL 143 144 av_register_all();//注冊FFmpeg所有組件 145 146 /*------------Input:填充ptInFormatContext------------*/ 147 g_fileH264 = fopen(strInVideoFileName, "rb+"); 148 ptInFormatContext = avformat_alloc_context(); 149 pbIoBuf = (unsigned char *)av_malloc(IO_BUFFER_SIZE); 150 //FillIoBuffer則是將數據讀取至pbIoBuf的回調函數。FillIoBuffer()形式(參數,返回值)是固定的,是一個回調函數, 151 ptAVIO = avio_alloc_context(pbIoBuf, IO_BUFFER_SIZE, 0, NULL, FillIoBuffer, NULL, NULL); //當系統需要數據的時候,會自動調用該回調函數以獲取數據 152 ptInFormatContext->pb = ptAVIO; //當自行指定了AVIOContext之后,avformat_open_input()里面的URL參數就不起作用了 153 154 ptInputFormat = av_find_input_format("h264");//得到ptInputFormat以便后面打開使用 155 //ps:函數調用成功之后處理過的AVFormatContext結構體;file:打開的視音頻流的文件路徑或者流媒體URL;fmt:強制指定AVFormatContext中AVInputFormat的,為NULL,FFmpeg通過文件路徑或者流媒體URL自動檢測;dictionay:附加的一些選項,一般情況下可以設置為NULL 156 //內部主要調用兩個函數:init_input():絕大部分初始化工作都是在這里做的。s->iformat->read_header():讀取多媒體數據文件頭,根據視音頻流創建相應的AVStream 157 if ((iRet = avformat_open_input(&ptInFormatContext, "", ptInputFormat, NULL)) < 0) //其中的init_input()如果指定了fmt(第三個參數,比如當前就有指定)就直接返回,如果沒有指定就調用av_probe_input_buffer2()推測AVInputFormat 158 {//打開輸入視頻源//自定義了回調函數FillIoBuffer()。在使用avformat_open_input()打開媒體數據的時候,就可以不指定文件的URL了,即其第2個參數為NULL(因為數據不是靠文件讀取,而是由FillIoBuffer()提供) 159 printf("Could not open input file\r\n"); 160 } 161 else 162 { 163 if ((iRet = avformat_find_stream_info(ptInFormatContext, 0)) < 0) 164 {//獲取視頻文件信息 165 printf("Failed to find input stream information\r\n"); 166 } 167 else 168 { 169 av_dump_format(ptInFormatContext, 0, strInVideoFileName, 0);//手工調試的函數,內部是log,輸出相關的格式信息到log里面 170 171 /*------------Output------------*/ 172 173 /*初始化一個用於輸出的AVFormatContext結構體 174 *ctx:函數調用成功之后創建的AVFormatContext結構體。 175 *oformat:指定AVFormatContext中的AVOutputFormat,用於確定輸出格式。如果指定為NULL, 176 可以設定后兩個參數(format_name或者filename)由FFmpeg猜測輸出格式。 177 PS:使用該參數需要自己手動獲取AVOutputFormat,相對於使用后兩個參數來說要麻煩一些。 178 *format_name:指定輸出格式的名稱。根據格式名稱,FFmpeg會推測輸出格式。輸出格式可以是“flv”,“mkv”等等。 179 *filename:指定輸出文件的名稱。根據文件名稱,FFmpeg會推測輸出格式。文件名稱可以是“xx.flv”,“yy.mkv”等等。 180 函數執行成功的話,其返回值大於等於0 181 */ 182 avformat_alloc_output_context2(&ptOutFormatContext, NULL, NULL, strOutFileName); 183 if (!ptOutFormatContext) 184 { 185 printf("Could not create output context\r\n"); 186 iRet = AVERROR_UNKNOWN; 187 } 188 else 189 { 190 ptOutputFormat = ptOutFormatContext->oformat; 191 //for (i = 0; i < ptInFormatContext->nb_streams; i++) 192 { 193 //Create output AVStream according to input AVStream 194 ptInStream = ptInFormatContext->streams[0];//0 video 195 ptOutStream = avformat_new_stream(ptOutFormatContext, ptInStream->codec->codec);//給ptOutFormatContext中的流數組streams中的 196 if (!ptOutStream) //一條流(數組中的元素)分配空間,也正是由於這里分配了空間,后續操作直接拷貝編碼數據(pkt)就可以了。 197 { 198 printf("Failed allocating output stream\r\\n"); 199 iRet = AVERROR_UNKNOWN; 200 //break; 201 } 202 else 203 { 204 iVideoStreamIndex=0; 205 iOutVideoStreamIndex = ptOutStream->index; //保存視頻流所在數組的位置 206 if (avcodec_copy_context(ptOutStream->codec, ptInStream->codec) < 0) //Copy the settings of AVCodecContext 207 {//avcodec_copy_context()函數可以將輸入視頻/音頻的參數拷貝至輸出視頻/音頻的AVCodecContext結構體 208 printf("Failed to copy context from input to output stream codec context\r\n"); 209 iRet = AVERROR_UNKNOWN; 210 //break; 211 } 212 else 213 { 214 ptOutStream->codec->codec_tag = 0; 215 if (ptOutFormatContext->oformat->flags & AVFMT_GLOBALHEADER) 216 ptOutStream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER; 217 218 } 219 } 220 } 221 if(AVERROR_UNKNOWN == iRet) 222 { 223 } 224 else 225 { 226 av_dump_format(ptOutFormatContext, 0, strOutFileName, 1);//Output information------------------ 227 //Open output file 228 if (!(ptOutputFormat->flags & AVFMT_NOFILE)) 229 { /*打開FFmpeg的輸入輸出文件,使后續讀寫操作可以執行 230 *s:函數調用成功之后創建的AVIOContext結構體。 231 *url:輸入輸出協議的地址(文件也是一種“廣義”的協議,對於文件來說就是文件的路徑)。 232 *flags:打開地址的方式。可以選擇只讀,只寫,或者讀寫。取值如下。 233 AVIO_FLAG_READ:只讀。AVIO_FLAG_WRITE:只寫。AVIO_FLAG_READ_WRITE:讀寫。*/ 234 iRet = avio_open(&ptOutFormatContext->pb, strOutFileName, AVIO_FLAG_WRITE); 235 if (iRet < 0) 236 { 237 printf("Could not open output file %s\r\n", strOutFileName); 238 } 239 else 240 { 241 //Write file header 242 if (avformat_write_header(ptOutFormatContext, NULL) < 0) //avformat_write_header()中最關鍵的地方就是調用了AVOutputFormat的write_header() 243 {//不同的AVOutputFormat有不同的write_header()的實現方法 244 printf("Error occurred when opening output file\r\n"); 245 } 246 else 247 { 248 while (1) 249 { 250 int iStreamIndex = -1;//用於標識當前是哪個流 251 iStreamIndex = iOutVideoStreamIndex; 252 //Get an AVPacket//從視頻輸入流中取出視頻的AVPacket 253 iRet = av_read_frame(ptInFormatContext, &tOutPacket);//從輸入文件讀取一幀壓縮數據 254 if (iRet < 0) 255 break; 256 else 257 { 258 do{ 259 ptInStream = ptInFormatContext->streams[tOutPacket.stream_index]; 260 ptOutStream = ptOutFormatContext->streams[iStreamIndex]; 261 if (tOutPacket.stream_index == iVideoStreamIndex) 262 { //H.264裸流沒有PTS,因此必須手動寫入PTS,應該放在av_read_frame()之后 263 //FIX:No PTS (Example: Raw H.264) 264 //Simple Write PTS 265 if (tOutPacket.pts == AV_NOPTS_VALUE) 266 { 267 //Write PTS 268 AVRational time_base1 = ptInStream->time_base; 269 //Duration between 2 frames (μs) 。假設25幀,兩幀間隔40ms //AV_TIME_BASE表示1s,所以用它的單位為us,也就是ffmpeg中都是us 270 //int64_t calc_duration = AV_TIME_BASE*1/25;//或40*1000;//(double)AV_TIME_BASE / av_q2d(ptInStream->r_frame_rate);//ptInStream->r_frame_rate.den等於0所以注釋掉 271 //幀率也可以從h264的流中獲取,前面dump就有輸出,但是不知道為何同樣的變量前面r_frame_rate打印正常,這里使用的時候卻不正常了,所以這個間隔時間只能使用avg_frame_rate或者根據假設幀率來寫 272 int64_t calc_duration =(double)AV_TIME_BASE / av_q2d(ptInStream->avg_frame_rate); 273 //Parameters pts(顯示時間戳)*pts單位(時間基*時間基單位)=真實顯示的時間(所謂幀的顯示時間都是相對第一幀來的) 274 tOutPacket.pts = (double)(iFrameIndex*calc_duration) / (double)(av_q2d(time_base1)*AV_TIME_BASE);//AV_TIME_BASE為1s,所以其單位為us 275 tOutPacket.dts = tOutPacket.pts; 276 tOutPacket.duration = (double)calc_duration / (double)(av_q2d(time_base1)*AV_TIME_BASE); 277 iFrameIndex++; 278 printf("Write iFrameIndex:%d,stream_index:%d,num:%d,den:%d\r\n",iFrameIndex, tOutPacket.stream_index,ptInStream->avg_frame_rate.num,ptInStream->avg_frame_rate.den); 279 } 280 llCurrentPts = tOutPacket.pts; 281 break; 282 } 283 } while (av_read_frame(ptInFormatContext, &tOutPacket) >= 0); 284 } 285 286 //Convert PTS/DTS 287 tOutPacket.pts = av_rescale_q_rnd(tOutPacket.pts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); 288 tOutPacket.dts = av_rescale_q_rnd(tOutPacket.dts, ptInStream->time_base, ptOutStream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX)); 289 tOutPacket.duration = av_rescale_q(tOutPacket.duration, ptInStream->time_base, ptOutStream->time_base); 290 tOutPacket.pos = -1; 291 tOutPacket.stream_index = iStreamIndex; 292 //printf("Write 1 Packet. size:%5d\tpts:%lld\n", tOutPacket.size, tOutPacket.pts); 293 //Write 294 /*av_interleaved_write_frame包括interleave_packet()以及write_packet(),將還未輸出的AVPacket輸出出來 295 *write_packet()函數最關鍵的地方就是調用了AVOutputFormat中寫入數據的方法。write_packet()實際上是一個函數指針, 296 指向特定的AVOutputFormat中的實現函數*/ 297 if (av_interleaved_write_frame(ptOutFormatContext, &tOutPacket) < 0) 298 { 299 printf("Error muxing packet\r\n"); 300 break; 301 } 302 av_free_packet(&tOutPacket);//釋放空間 303 } 304 //Write file trailer//av_write_trailer()中最關鍵的地方就是調用了AVOutputFormat的write_trailer() 305 av_write_trailer(ptOutFormatContext);//不同的AVOutputFormat有不同的write_trailer()的實現方法 306 } 307 if (ptOutFormatContext && !(ptOutputFormat->flags & AVFMT_NOFILE)) 308 avio_close(ptOutFormatContext->pb);//該函數用於關閉一個AVFormatContext->pb,一般情況下是和avio_open()成對使用的。 309 } 310 } 311 } 312 avformat_free_context(ptOutFormatContext);//釋放空間 313 } 314 } 315 avformat_close_input(&ptInFormatContext);//該函數用於關閉一個AVFormatContext,一般情況下是和avformat_open_input()成對使用的。 316 } 317 if(NULL!=g_fileH264) 318 fclose(g_fileH264); 319 return 0; 320 }
具體代碼見github:
https://github.com/fengweiyu/FFmpegFormat/FFmpegMuxer
五、參考原文:
https://blog.csdn.net/leixiaohua1020/article/details/25422685
https://blog.csdn.net/leixiaohua1020/article/details/39802913
https://blog.csdn.net/leixiaohua1020/article/details/12980423
https://blog.csdn.net/leixiaohua1020/article/details/39759163