• 音视频处理之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
    ",argv[0]);
    101         printf("For example:
    ");
    102         printf("%s InputFile.flv OutputFile.mp4
    ",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
    ");
    114     }
    115     else
    116     {
    117         if ((iRet = avformat_find_stream_info(ptInFormatContext, 0)) < 0) 
    118         {//获取视频文件信息
    119             printf("Failed to find input stream information
    ");
    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
    ");
    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
    \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
    ");
    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
    ", 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
    ");
    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
    ");
    221                                         break;
    222                                     }
    223                                     printf("Write %8d frames to output file
    ", 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 ");

    }

    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
    ",argv[0]);
    137         printf("For example:
    ");
    138         printf("%s InputFile.h264 OutputFile.mp4
    ",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
    ");
    160     }
    161     else
    162     {
    163         if ((iRet = avformat_find_stream_info(ptInFormatContext, 0)) < 0) 
    164         {//获取视频文件信息
    165             printf("Failed to find input stream information
    ");
    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
    ");
    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
    \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
    ");
    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
    ", 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
    ");
    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
    ",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	pts:%lld
    ", 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
    ");
    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

  • 相关阅读:
    如何查看操作系统的具体版本号
    Review of Image Super-resolution Reconstruction Based on Deep Learning
    opencv imshow图片显示不全
    Javaweb文件下载异常
    Microsoft Edge出现错误代码:STATUS_INVALID_IMAGE_HASH
    Javaweb导入excel数据
    Java读取execl数据
    浏览器网页左上角小图标实现方式
    Java LDAP验证
    Java JPA新增数据生成UUID
  • 原文地址:https://www.cnblogs.com/yuweifeng/p/9021360.html
Copyright © 2020-2023  润新知