音視頻處理之FFmpeg封裝格式20180510


一、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 }
FFmpegRemuxer.cpp

具體代碼見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 }
FFmpegMuxer.cpp

 

具體代碼見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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM