• FFmpeg编程(一)FFmpeg初级开发


    FFmpeg代码结构

    libavformat 实现了流协议,容器格式及其基本IO访问

    一:日志系统的使用

    日志级别:(依次降低)

    AV_LOG_ERROR
    AV_LOG_WARNING
    AV_LOG_INFO
    AV_LOG_DEBUG

    (一)日志系统编程

    #include <stdio.h>
    #include <libavutil/log.h>
    
    int main(int argc,char* argv[])
    {
        av_log_set_level(AV_LOG_DEBUG);
        av_log(NULL,AV_LOG_INFO,"...Hello world:%s %s
    ",argv[0],argv[1]);
        return 0;
    }
    日志输出编程01log.c

    编译.c文件:

    gcc 01log.c -o 01log -lavutil

    运行结果: 

    (二)回顾gcc编译如何寻找头文件、库文件(gcc -I -L -l区别

    我们用gcc编译程序时,可能会用到“-I”(大写i),“-L”(大写l),“-l”(小写l)等参数,例:

    gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld

    上面这句表示在编译hello.c时:

    -I /home/hello/include : 表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include-->/usr/include-->/usr/local/include
    -L /home/hello/lib : 表示将/home/hello/lib目录作为第一个寻找库文件的目录,寻找的顺序是:/home/hello/lib-->/lib-->/usr/lib-->/usr/local/lib
     -lworld : 表示在上面的lib的路径中寻找libworld.so动态库文件或libworld.a静态库,同时存在时候动态库优先,
    如果要强制链接静态库可以用-static或直接用libword.a, gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib /home/hello/lib/libworld.a

    (三)linux中的动态库和静态库

    1.概念和区别:

    静态库就是在编译过程中一些目标文件的集合。静态库在程序链接的时候使用,链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行程序的时候就不需要静态库了。由于每个使用静态库的应用程序都需要拷贝所用函数的代码,所以静态链接的文件会比较大。

    相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

    2.命名:

    静态库的名字一般为libxxxx.a,其中xxxx是该lib的名称。
    动态库的名字一般为libxxxx.so.major.minor,xxxx是该lib的名称,major是主版本号,minor是副版本号。版本号也可以没有,一般都会建立个没有版本号的软连接文件链接到全名的库文件。 

    3.创建:

    无论静态库还是动态库,创建都分为两步,第一步创建目标文件,第二步生产库。
    1).静态库的创建:

    gcc -c test.c -o test.o #生成编译文件
    ar rcs libtest.a test.o #生成静态库

    名字为libtest.a的静态库就生产了,其中选项:
    r 表明将模块加入到静态库中;
    c 表示创建静态库;
    s 表示生产索引;
    还有更多选项像增加、删除库中的目标文件,包括将静态库解包等可以通过man来获得。
    2).动态库的创建:

    gcc -fPIC -c test.c -o test.c
    gcc --share test.o -o libtest.so

    -fPIC 为了跨平台

    4.使用:

    编译链接目标程序的方法是一样的:

    gcc main.c -L. -ltest -o main

    -L.  :  指定现在本目录下搜索库,如果没有,会到系统默认的目录下搜索,一般为/lib、/usr/lib下
    对于静态库,这个步骤之后就可以将libtest.a库删掉,因为它已经被编译进了目标程序,不再需要它了。
    而对于动态库,libtest.so库只是在目标程序里做了标记,在运行程序时才会动态加载,那么从哪加载呢?

    加载目录会由/etc/ld.so.conf来指定,一般默认是/lib、/usr/lib,所以要想让动态库顺利加载,你可以将库文件copy到上面的两个目录下
    或者设置export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX/YYY,后面为你自己动态库的目录,再或者修改/etc/ld.so.conf文件,把库所在的路径加到文件末尾,并执行ldconfig刷新。这样,加入的目录下的所有库文件都可见。

    5.补充

    另外还有个文件需要了解/etc/ld.so.cache,里面保存了常用的动态函数库,且会先把他们加载到内存中,因为内存的访问速度远远大于硬盘的访问速度,这样可以提高软件加载动态函数库的速度了。

    最后提一点,当同一目录下既有动态库又有静态库,并且两个库的名字相同时,编译时会如何链接呢?

    gcc编译时默认都是动态链接,如果要指定优先链接静态库,需要指定参数static。

    6.使用案例:https://blog.csdn.net/ayz671101/article/details/101812040(重点)

    (四)分析 gcc 01log.c -o 01log -lavutil

    1.回顾安装FFmpeg时的配置:Fmpeg学习(一)FFmpeg安装与测试

    因此,我们早就将FFmpeg动态库目录加入/etc/ld.so.conf文件中,因此-lavutil会先去FFmpeg库目录下查找

    2.查看库目录

    存在我们所需要的动态库文件,所以编译成功!!

    二:文件的删除与重命名

    (一)文件编程

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc,char* argv[])
    {
        int ret;    //获取返回值状态
        char* filename = "./2.txt";
        av_log_set_level(AV_LOG_DEBUG); //设置日志级别
    
        //1.移动文件测试
        ret = avpriv_io_move("1.txt","3.txt");
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Failed to move
    ");
            return -1;
        }
        av_log(NULL,AV_LOG_INFO,"Success to move
    ");
    
        ret = avpriv_io_delete(filename);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Failed to delete
    ");
            return -1;
        }
    
        av_log(NULL,AV_LOG_INFO,"Success to delete %s
    ",filename);
        return 0;
    }
    ffmpeg_io.c

    编译文件:

     gcc -o fio ffmpeg_io.c -I /usr/local/ffmpeg/include -L /usr/local/ffmpeg/lib -lavutil -lavformat

    注意:编译过程中我们可以不需要指定-I 但是我们必须指定-L (没有搞明白)  ,虽然我们在去掉-L后也可以编译成功,但是运行会出现以下问题:

    我们可以修改程序:添加av_register_all() 初始化libavformat并注册所有muxer、demuxer和协议(我们的File操作也在里面<可以自己查看源码,推荐3.0版本,太高太多东西看不到,太低和自己使用的方法不兼容)。如果不调用此函数,则可以选择希望支持的格式。

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc,char* argv[])
    {
        int ret;    //获取返回值状态
        char* filename = "./2.txt";
        av_log_set_level(AV_LOG_DEBUG); //设置日志级别
        av_register_all();
    
        //1.移动文件测试
        ret = avpriv_io_move("1.txt","3.txt");
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Failed to move
    ");
            return -1;
        }
        av_log(NULL,AV_LOG_INFO,"Success to move
    ");
    
        ret = avpriv_io_delete(filename);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Failed to delete
    ");
            return -1;
        }
    
        av_log(NULL,AV_LOG_INFO,"Success to delete %s
    ",filename);
        return 0;
    }
    ffmpeg_io.c

    编译方案推荐第一种,虽然还没搞明白,但是兼容性看来好些,不容易出错!!!

    三:目录操作

    (一)重要结构体

    AVIODirContext:操作目录的上下文,由avio_open_dir方法进行赋值

    typedef struct URLContext {
        const AVClass *av_class;    /**< information for av_log(). Set by url_open(). */
        struct URLProtocol *prot;
        void *priv_data;
        char *filename;             /**< specified URL */
        int flags;
        int max_packet_size;        /**< if non zero, the stream is packetized with this max packet size */
        int is_streamed;            /**< true if streamed (no seek possible), default = false */
        int is_connected;
        AVIOInterruptCB interrupt_callback;
        int64_t rw_timeout;         /**< maximum time to wait for (network) read/write operation completion, in mcs */
        const char *protocol_whitelist;
    } URLContext
    struct URLContext
    typedef struct AVIODirContext {
        struct URLContext *url_context;
    } AVIODirContext;
    struct AVIODirContext
    int avio_open_dir(AVIODirContext **s, const char *url, AVDictionary **options)
    {
        URLContext *h = NULL;
        AVIODirContext *ctx = NULL;
        int ret;
        av_assert0(s);
    
        ctx = av_mallocz(sizeof(*ctx));
        if (!ctx) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    
        if ((ret = ffurl_alloc(&h, url, AVIO_FLAG_READ, NULL)) < 0)
            goto fail;
    
        if (h->prot->url_open_dir && h->prot->url_read_dir && h->prot->url_close_dir) {
            if (options && h->prot->priv_data_class &&
                (ret = av_opt_set_dict(h->priv_data, options)) < 0)
                goto fail;
            ret = h->prot->url_open_dir(h);
        } else
            ret = AVERROR(ENOSYS);
        if (ret < 0)
            goto fail;
    
        h->is_connected = 1;
        ctx->url_context = h;
        *s = ctx;
        return 0;
    
      fail:
        av_free(ctx);
        *s = NULL;
        ffurl_close(h);
        return ret;
    }
    avio_open_dir

    AVIODirEntry:目录项。用于存放文件名、文件大小等信息

    typedef struct AVIODirEntry {
        char *name;                           /**< Filename */
        int type;                             /**< Type of the entry */
        int utf8;                             /**< Set to 1 when name is encoded with UTF-8, 0 otherwise.
                                                   Name can be encoded with UTF-8 even though 0 is set. */
        int64_t size;                         /**< File size in bytes, -1 if unknown. */
        int64_t modification_timestamp;       /**< Time of last modification in microseconds since unix
                                                   epoch, -1 if unknown. */
        int64_t access_timestamp;             /**< Time of last access in microseconds since unix epoch,
                                                   -1 if unknown. */
        int64_t status_change_timestamp;      /**< Time of last status change in microseconds since unix
                                                   epoch, -1 if unknown. */
        int64_t user_id;                      /**< User ID of owner, -1 if unknown. */
        int64_t group_id;                     /**< Group ID of owner, -1 if unknown. */
        int64_t filemode;                     /**< Unix file mode, -1 if unknown. */
    } AVIODirEntry;
    struct AVIODirEntry
    int avio_read_dir(AVIODirContext *s, AVIODirEntry **next)
    {
        URLContext *h;
        int ret;
    
        if (!s || !s->url_context)
            return AVERROR(EINVAL);
        h = s->url_context;
        if ((ret = h->prot->url_read_dir(h, next)) < 0)
            avio_free_directory_entry(next);
        return ret;
    }
    avio_read_dir

    (二)目录信息编程

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc,char* argv[])
    {
        int ret;
        AVIODirContext* ctx=NULL; //操作目录的上下文,将由avio_open_dir赋值
        AVIODirEntry* entry=NULL; //获取文件项的信息
        av_log_set_level(AV_LOG_INFO);
    
        //打开目录
        ret = avio_open_dir(&ctx,"./",NULL);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t open dir:%s
    ",av_err2str(ret));
            goto _fail;
        }
        //读取文件项
        while(1){
            ret = avio_read_dir(ctx,&entry);
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Can`t read dir:%s
    ",av_err2str(ret));
                goto _fail;
            }
            if(!entry) break;
            av_log(NULL,AV_LOG_INFO,"%12"PRId64" %s
    ",entry->size,entry->name); //PRId64表示打印64位数据,前面12是占位
            avio_free_directory_entry(&entry); //释放使用过的空间
        }
    _fail:
        avio_close_dir(&ctx);
    
        return 0;
    }
    目录操作

    四:处理流数据的基本概念

    (一)基本概念

    多媒体文件(mp4、flv...)其实是个容器,在容器里有很多流(Stream/Track)(音频流、视频流、....没有交叉性<即便是多路音频、视频>),每种流是由不同的编码器编码的。

    从流中读出的数据称为包(帧压缩后),在一个包中包含着一个或多个帧(未压缩)。

    (二)几个重要的结构体

    分别对应多媒体文件上下文、流、包

    (三)ffmpeg 操作流数据的基本步骤

    1.解复用:打开多媒体文件
    2.获取文件中多路流中想要的
    3.获取数据包,进行解码,对原始数据进行处理,变声、变速、滤波
    4.释放相关资源

    五:打印音/视频Meta信息

    av_register_all() : 初始化libavformat并注册所有muxer、demuxer和协议。(所有FFmpeg程序开始前都要去调用他)
    avformat_open_input()/avformat_close_input() : 打开、关闭多媒体文件,结合前面的结构体
    av_dump_format() : 打印多媒体meta信息

    (一)多媒体文件meta数据获取

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    
    int main(int argc,char* argv[])
    {
        int ret;
        AVFormatContext* fmt_ctx = NULL;
        AVStream* stm = NULL;
        AVPacket* pkt = NULL;
    
        av_register_all();
        av_log_set_level(AV_LOG_INFO);
    
        ret = avformat_open_input(&fmt_ctx,"./gfxm.mp4",NULL,NULL);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t open file: %s
    ",av_err2str(ret));
            return -1;
        }
        
        ret = avformat_find_stream_info(fmt_ctx, 0); //获取流详细信息
        if(ret<0){
            av_log(NULL,AV_LOG_WARNING,"Can`t get stream information!just show aac, not show aac(LC)!
    ");
        }
    
        av_dump_format(fmt_ctx,0,"./gfxm.mp4",0); //第一个0是流的索引值,,第二个表示输入/输出流,由于是输入文件,所以为0
        //关闭上下文
        avformat_close_input(&fmt_ctx);
        return 0;
    }
    元数据获取

    其中Input是我们设置的0号索引流,其中Stream表示多路流(第一路流为视频流,第二路为音频流)

    1.没有加上avformat_find_stream_info时,缺少部分信息(后面数据处理时需要用到,如acc(LC),下面并没有显示LC)

    2.使用avformat_find_stream_info后,显示完整信息

    六:FFmpeg抽取音频数据

    av_init_packet() 初始化一个数据包结构体
    
    av_find_best_stream() 由四(一)可以知道在多媒体文件中有多种流,而每种流可能存在多路,该函数可以帮助找到其中最佳的一路流
    
    av_read_frame()/av_packet_unref() 拿到流之后使用av_read_frame()获取流的数据包

    从流中读取数据包之后,数据包就会增减引用基数,当包不用的时候,调用av_packet_unref(),将包的引用基数减 1。ffmpeg 检测到包的引用基数为0的时候,就是释放相应的资源,防止内存泄露。

    补充:抽取出来的aac文件需要加adts头才能正常播放

    AAC的ADTS头文件信息介绍:https://blog.csdn.net/qq_29028177/article/details/54694861重点

    void adts_header(char *szAdtsHeader, int dataLen){
    
        int audio_object_type = 2;             //通过av_dump_format显示音频信息或者ffplay获取多媒体文件的音频流编码acc(LC),对应表格中Object Type ID -- 2
        int sampling_frequency_index = 4;      //音频信息中采样率为44100 Hz 对应采样率索引0x4
        int channel_config = 2;                   //音频信息中音频通道为双通道2
    
        int adtsLen = dataLen + 7;             //采用头长度为7字节,所以protection_absent=1   =0时为9字节,表示含有CRC校验码
    
        szAdtsHeader[0] = 0xff;         //syncword :总是0xFFF, 代表一个ADTS帧的开始, 用于同步. 高8bits
    
        szAdtsHeader[1] = 0xf0;         //syncword:0xfff                          低4bits
        szAdtsHeader[1] |= (0 << 3);    //MPEG Version:0 : MPEG-4(mp4a),1 : MPEG-2  1bit
        szAdtsHeader[1] |= (0 << 1);    //Layer:0                                   2bits 
        szAdtsHeader[1] |= 1;           //protection absent:1  没有CRC校验            1bit
    
        szAdtsHeader[2] = (audio_object_type - 1)<<6;            //profile=(audio_object_type - 1) 表示使用哪个级别的AAC  2bits
        szAdtsHeader[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index  4bits 
        szAdtsHeader[2] |= (0 << 1);                             //private bit:0                                      1bit
        szAdtsHeader[2] |= (channel_config & 0x04)>>2;           //channel configuration:channel_config               高1bit
    
        szAdtsHeader[3] = (channel_config & 0x03)<<6;     //channel configuration:channel_config      低2bits
        szAdtsHeader[3] |= (0 << 5);                      //original:0                               1bit
        szAdtsHeader[3] |= (0 << 4);                      //home:0                                   1bit ----------------固定头完结,开始可变头
        szAdtsHeader[3] |= (0 << 3);                      //copyright id bit:0                       1bit  
        szAdtsHeader[3] |= (0 << 2);                      //copyright id start:0                     1bit
        szAdtsHeader[3] |= ((adtsLen & 0x1800) >> 11);    //frame length:value                       高2bits  000|1 1000|0000 0000
    
        szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);     //frame length:value    中间8bits             0000  0111 1111 1000
        
        szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5);       //frame length:value    低 3bits              0000  0000 0000 0111
        //number_of_raw_data_blocks_in_frame:表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。所以说number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
        szAdtsHeader[5] |= 0x1f;                                 //buffer fullness:0x7ff 高5bits   0x7FF 说明是码率可变的码流 ---> 111 1111 1111 00----> 1 1111 1111 1100--->0x1f与0xfc
    
        szAdtsHeader[6] = 0xfc;                                        
    }
    adts_header头信息添加
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    
    #define ADTS_HEAD_LEN 7
    
    void adts_header(char *szAdtsHeader, int dataLen){
    
        int audio_object_type = 2;             //通过av_dump_format显示音频信息或者ffplay获取多媒体文件的音频流编码acc(LC),对应表格中Object Type ID -- 2
        int sampling_frequency_index = 4;      //音频信息中采样率为44100 Hz 对应采样率索引0x4
        int channel_config = 2;                   //音频信息中音频通道为双通道2
    
        int adtsLen = dataLen + 7;             //采用头长度为7字节,所以protection_absent=1   =0时为9字节,表示含有CRC校验码
    
        szAdtsHeader[0] = 0xff;         //syncword :总是0xFFF, 代表一个ADTS帧的开始, 用于同步. 高8bits
    
        szAdtsHeader[1] = 0xf0;         //syncword:0xfff                          低4bits
        szAdtsHeader[1] |= (0 << 3);    //MPEG Version:0 : MPEG-4(mp4a),1 : MPEG-2  1bit
        szAdtsHeader[1] |= (0 << 1);    //Layer:0                                   2bits 
        szAdtsHeader[1] |= 1;           //protection absent:1  没有CRC校验            1bit
    
        szAdtsHeader[2] = (audio_object_type - 1)<<6;            //profile=(audio_object_type - 1) 表示使用哪个级别的AAC  2bits
        szAdtsHeader[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index  4bits 
        szAdtsHeader[2] |= (0 << 1);                             //private bit:0                                      1bit
        szAdtsHeader[2] |= (channel_config & 0x04)>>2;           //channel configuration:channel_config               高1bit
    
        szAdtsHeader[3] = (channel_config & 0x03)<<6;     //channel configuration:channel_config      低2bits
        szAdtsHeader[3] |= (0 << 5);                      //original:0                               1bit
        szAdtsHeader[3] |= (0 << 4);                      //home:0                                   1bit ----------------固定头完结,开始可变头
        szAdtsHeader[3] |= (0 << 3);                      //copyright id bit:0                       1bit  
        szAdtsHeader[3] |= (0 << 2);                      //copyright id start:0                     1bit
        szAdtsHeader[3] |= ((adtsLen & 0x1800) >> 11);    //frame length:value                       高2bits  000|1 1000|0000 0000
    
        szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);     //frame length:value    中间8bits             0000  0111 1111 1000
        
        szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5);       //frame length:value    低 3bits              0000  0000 0000 0111
        //number_of_raw_data_blocks_in_frame:表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。所以说number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
        szAdtsHeader[5] |= 0x1f;                                 //buffer fullness:0x7ff 高5bits   0x7FF 说明是码率可变的码流 ---> 111 1111 1111 00----> 1 1111 1111 1100--->0x1f与0xfc
    
        szAdtsHeader[6] = 0xfc;                                        
    }
    
    int main(int argc,char* argv[])
    {
        //参数初始化以及检测
        int ret,audio_idx,len;
        AVFormatContext* fmt_ctx = NULL;
        AVPacket pkt; //不是指针
        char* src,*dst;
        FILE* dst_fd = NULL; //文件句柄
        char AdtsHeader[ADTS_HEAD_LEN]; 
    
        if(argc<3){
            av_log(NULL,AV_LOG_ERROR,"the count of params should be more than 3!
    ");
            return -1;
        }
    
        src = argv[1];
        dst = argv[2];
        if(!src||!dst){
            av_log(NULL,AV_LOG_ERROR,"src or dst is null!
    ");
            return -1;
        }
    
        //环境设置
        av_register_all();
        av_log_set_level(AV_LOG_INFO);
    
        //打开多媒体文件
        ret = avformat_open_input(&fmt_ctx,src,NULL,NULL); //第三个参数强制指定AVFormatContext中AVInputFormat的。这个参数一般情况下可以设置为NULL,这样FFmpeg可以自动检测AVInputFormat;第四个为附加选项,一般为NULL
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t open file: %s
    ",av_err2str(ret));
            if(fmt_ctx)
                avformat_close_input(&fmt_ctx);
            return -1;
        }
        
        //开始获取流(和元数据不一样),这里自动去获取获取最佳流
        //媒体文件句柄 / 流类型 / 请求的流编号(-1则自动去找) / 相关流索引号(比如音频对应的视频流索引号),不指定则-1 / 如果非空,则返回所选流的解码器(指针获取) / flag当前未定义
        ret = av_find_best_stream(fmt_ctx,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0); //成功则返回非负流号
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t find the best stream!
    ");
            avformat_close_input(&fmt_ctx);
            return -1;
        }
        audio_idx = ret;
        
        ret = avformat_find_stream_info(fmt_ctx, 0); //获取流详细信息
        if(ret<0){
            av_log(NULL,AV_LOG_WARNING,"Can`t get stream information!just show aac, not show aac(LC)!
    ");
        }
        //打印我们要获取的流的元数据
        av_dump_format(fmt_ctx,audio_idx,src,0); ////第一个0是流的索引值,,第二个表示输入/输出流,由于是输入文件,所以为0
        
        //打开目标文件
        dst_fd = fopen(dst,"wb");
        if(!dst_fd){
            avformat_close_input(&fmt_ctx);
            av_log(NULL,AV_LOG_ERROR,"Can`t open dst file!
    ");
            return -1;
        }
    
        //开始从流中读取包,先初始化包结构
        av_init_packet(&pkt); 
        while(av_read_frame(fmt_ctx,&pkt)>=0){ //读取包
            if(pkt.stream_index==audio_idx){
                adts_header(AdtsHeader,pkt.size); //设置ADTS头部
                len = fwrite(AdtsHeader,1,ADTS_HEAD_LEN,dst_fd); //写入ADTS头部到文件中去
                if(len!=ADTS_HEAD_LEN){
                    av_log(NULL,AV_LOG_WARNING,"warning,ADTS Header is not send to dest file!
    ");
                }
    
                len = fwrite(pkt.data,1,pkt.size,dst_fd); //写入音频数据到文件中去
                if(len!=pkt.size){
                    av_log(NULL,AV_LOG_WARNING,"warning,length of data is not equal size of packet!
    ");
                }
            }
            //每读取一次包,就需要将包引用-1,使得内存释放
            av_packet_unref(&pkt);
        }
    
        //关闭文件
        avformat_close_input(&fmt_ctx);
        if(dst_fd){
            fclose(dst_fd);
        }
    
        return 0;
    }
    音频抽取代码实现
    gcc ffmpeg_ad.c -o fad -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec

    ffplay gfxm.aac

    七: FFmpeg转换H264数据视频,从MP4(AVCC)格式到(AnnexB实时流)

    (一)基础知识

    H264流媒体协议解析

    FFmpeg AVPacket 剖析以及使用

    typedef struct AVPacket {
        /**
         * A reference to the reference-counted buffer where the packet data is
         * stored.
         * May be NULL, then the packet data is not reference-counted.
         */
        AVBufferRef *buf;
        /**
         * Presentation timestamp in AVStream->time_base units; the time at which
         * the decompressed packet will be presented to the user.
         * Can be AV_NOPTS_VALUE if it is not stored in the file.
         * pts MUST be larger or equal to dts as presentation cannot happen before
         * decompression, unless one wants to view hex dumps. Some formats misuse
         * the terms dts and pts/cts to mean something different. Such timestamps
         * must be converted to true pts/dts before they are stored in AVPacket.
         */
        int64_t pts;
        /**
         * Decompression timestamp in AVStream->time_base units; the time at which
         * the packet is decompressed.
         * Can be AV_NOPTS_VALUE if it is not stored in the file.
         */
        int64_t dts;
        uint8_t *data;
        int   size;
        int   stream_index;
        /**
         * A combination of AV_PKT_FLAG values
         */
        int   flags;
        /**
         * Additional packet data that can be provided by the container.
         * Packet can contain several types of side information.
         */
        AVPacketSideData *side_data;
        int side_data_elems;
    
        /**
         * Duration of this packet in AVStream->time_base units, 0 if unknown.
         * Equals next_pts - this_pts in presentation order.
         */
        int64_t duration;
    
        int64_t pos;                            ///< byte position in stream, -1 if unknown
    
    #if FF_API_CONVERGENCE_DURATION
        /**
         * @deprecated Same as the duration field, but as int64_t. This was required
         * for Matroska subtitles, whose duration values could overflow when the
         * duration field was still an int.
         */
        attribute_deprecated
        int64_t convergence_duration;
    #endif
    } AVPacket;
    struct AVPacket
    typedef struct AVBufferRef {
        AVBuffer *buffer;
    
        /**
         * The data buffer. It is considered writable if and only if
         * this is the only reference to the buffer, in which case
         * av_buffer_is_writable() returns 1.
         */
        uint8_t *data;
        /**
         * Size of data in bytes.
         */
    #if FF_API_BUFFER_SIZE_T
        int      size;
    #else
        size_t   size;
    #endif
    } AVBufferRef;
    struct AVBufferRef

    细心发现pkt.buf->size总比pkt.size多64个字节,对应着宏AV_INPUT_BUFFER_PADDING_SIZE值,所以,如果实际应用中要修改pkt中的数据,pkt.buf->size是无时无刻都要比pkt.size多64个字节。

    (二)代码实现

    流程图转自:https://blog.csdn.net/ty13392186270/article/details/106826367

      1 #include <stdio.h>
      2 #include <libavutil/log.h>
      3 #include <libavformat/avio.h>
      4 #include <libavformat/avformat.h>
      5 #include <libavcodec/avcodec.h>
      6 
      7 //这里是将前面的(startcode+SPS+PPS,size)+(NALU数据,size)传入函数中,使得函数在NALU 前面加入startcode;这里的startcode不是4字节,而是3字节
      8 static int alloc_and_copy(AVPacket* out,const uint8_t* sps_pps,uint32_t sps_pps_size,
      9                                         const uint8_t* in,uint32_t in_size)
     10 {
     11     uint32_t offset = out->size;            //偏移量,就是out已有数据的大小,后面再写入数据就要从偏移量处开始操作
     12     uint8_t start_code_size = sps_pps==NULL?3:4;   //特征码的大小,SPS/PPS占4字节,其余占3字节
     13     int err;
     14 
     15     err = av_grow_packet(out,sps_pps_size+in_size+start_code_size); //包扩容,在原来out已有数据基础上进行扩容,使得可以加入所有数据
     16     if(err<0)
     17         return err;
     18 
     19     //1.含有SPS、PPS数据,直接写入。注意:在SPS、PPS前面已经写入了start code
     20     if(sps_pps)    
     21         memcpy(out->data+offset,sps_pps,sps_pps_size);  //先写入SPS、PPS数据
     22 
     23     //2.在NALU前面加入start code
     24     for(int i=0;i<start_code_size;i++){
     25         (out->data+offset+sps_pps_size)[i] = i==start_code_size-1?1:0;
     26     }
     27 
     28     //3.最后将NALU数据拷贝过去
     29     memcpy(out->data+offset+sps_pps_size+start_code_size,in,in_size);
     30 
     31     return 0;
     32 }
     33 
     34 //从AVCC中的extradata中获取SPS、PPS数据;此外由于SPS、PPS前面的start code为4字节,所以我们这里直接写入进去吧
     35 //注意:第4个参数padding,是表示AVBufferRef中填充的字节数。取决于ffmpeg版本,这里是64字节;所以AVPacket中data大小最大字节数为INT_MAX - 64;而这里64被宏定义为AV_INPUT_BUFFER_PADDING_SIZE
     36 int h264_extradata_to_annexb(const uint8_t* codec_extradata,const int codec_extradata_size,AVPacket* out_extradata,int padding)
     37 {
     38     uint16_t unit_size;     //读取两个字节,用来获取SPS、PPS的大小
     39     uint64_t total_size = 0;    //用来记录从extradata中读取的全部SPS、PPS大小,最后来验证大小不要超过AVPacket中data的大小限制(因为我们最后是将数据存放在AVPacket中返回的)
     40 
     41     uint8_t* out = NULL;//out:是一个指向一段内存的指针,这段内存用于存放所有拷贝的sps/pps数据和其特征码数据
     42     uint8_t unit_nb;    //unit_nb:sps/pps个数
     43     uint8_t sps_done = 0;   //表示sps数据是否已经处理完毕,当sps_done为0,表示没有处理,处理完成后不为0
     44     uint8_t sps_seen = 0, sps_offset = 0;   //sps_seen:是否有sps数据 sps_offset:sps数据的偏移,为0
     45     uint8_t pps_seen = 0, pps_offset = 0;   //pps_seen:是否有pps数据 pps_offset:pps数据的偏移,因为pps数据在sps后面,所以其偏移就是所有sps数据长度+sps的特征码所占字节数
     46 
     47     static const uint8_t start_code[4] = {0,0,0,1}; //记录start code
     48 
     49     const uint8_t* extradata = codec_extradata + 4; //扩展数据的前4字节无用,跳过
     50     int length_size = ((*extradata++)&0x3) + 1;     //第一个字节的后两位存放NALULengthSizeMinusOne字段的值(0,1,2,3)+1=(1,2,3,4)其中3不被使用;这个字段要被返回的
     51 
     52     sps_offset = pps_offset = -1;
     53 
     54     //先获取SPS的个数 大小1字节,在后5位中
     55     unit_nb = (*extradata++)&0x1f;
     56     if(!unit_nb){   //没有SPS
     57         goto pps;   //就直接去获取PPS数据
     58     }else{          //有SPS数据
     59         sps_offset = 0; //SPS是最开始的,不需要偏移
     60         sps_seen = 1;   //表示有SPS数据
     61     }
     62 
     63     while(unit_nb--){   //开始处理SPS、PPS类型的每一个数据,一般SPS、PPS都是1个
     64         int err;
     65         //先读取2个字节的数据,用来表示SPS/PPS的数据长度
     66         unit_size = (extradata[0] << 8) | extradata[1];
     67         total_size += unit_size + 4;    //+4是加开始码,注意:total_size是累加了每一次获取SPS、PPS的数据量
     68         if(total_size > INT_MAX - padding){ //防止数据溢出AVPacket的data大小
     69             av_log(NULL,AV_LOG_ERROR,"Too big extradata size, corrupted stream or invalid MP4/AVCC bitstream
    ");
     70             av_free(out);
     71             return AVERROR(EINVAL);
     72         }
     73         //判断数据是否越界
     74         if(extradata+2+unit_size>codec_extradata+codec_extradata_size){
     75             av_log(NULL,AV_LOG_ERROR,"Packet header is not contained in global extradata, corrupted stream or invalid MP4/AVCC bitstream
    ");
     76             av_free(out);   //释放前面的空间
     77             return AVERROR(EINVAL);
     78         }
     79         //开始为out指针分配空间
     80         if((err = av_reallocp(&out,total_size+padding))<0)  //reallocp是在原来空间上扩充,已经存在的数据不会被丢弃
     81             return err;
     82         memcpy(out+total_size-unit_size-4,start_code,4);    //先拷贝start code到out中
     83         memcpy(out+total_size-unit_size,extradata+2,unit_size); //拷贝对应的SPS、PPS数据
     84         extradata += unit_size+2;   //注意多加2,前面没有跳过长度信息
     85 
     86 pps:    //获取完成SPS后,会开始从这里更新PPS的信息到上面的unit_nb中
     87         if(!unit_nb && !sps_done++){    //当SPS获取完成以后,unit_nb=0;!sps_done=1;  注意,sps_done++,导致不为0,获取一次PPS之后,后面就不会在进入这里
     88             unit_nb = *extradata++;     //当读取了所有SPS数据以后,再读取一个字节,用来表示PPS的个数,然后再循环去获取PPS的数据
     89             if(unit_nb){    //PPS存在
     90                 pps_offset = total_size;//表示前面的SPS已经获取完成,后面偏移写入PPS数据即可    
     91                 pps_seen = 1;           //表示获取了PPS数据
     92             }
     93         }
     94     }
     95 
     96     if(out) //开始进行数据0填充
     97         memset(out+total_size,0,padding);
     98     if(!sps_seen)   //没有获取到SPS数据
     99         av_log(NULL,AV_LOG_WARNING,"Warning: SPS NALU missing or invalid. The resulting stream may not play.
    ");
    100 
    101     if(!pps_seen)   //没有获取到PPS数据
    102         av_log(NULL,AV_LOG_WARNING,"Warning: PPS NALU missing or invalid. The resulting stream may not play.
    ");
    103     //将数据赋值给AVPacket中返回
    104     out_extradata->data = out;
    105     out_extradata->size = total_size;
    106 
    107     return length_size; //返回前缀长度
    108 }
    109 
    110 //负责将H264格式的本地mp4文件从AVCC格式转为实时流AnnexB格式
    111 int h264_mp4toannexb(AVFormatContext * fmt_ctx, AVPacket* in,FILE* dst_fd)
    112 {
    113     AVPacket* out = NULL;   //设置输入包信息
    114     AVPacket spspps_pkt;    //用来存放SPS、PPS信息,对于AnnexB,我们需要在所有I帧前面加上SPS、PPS数据
    115 
    116     int len;                //保存fwrite返回写入的数据长度
    117     uint8_t unit_type;      //存放NALU的header,长度为8bits
    118     
    119     uint8_t nal_size_len;   //AVCC格式数据采用NALU长度(固定字节,一般为4字节,取决与extradata中的NALULengthSizeMinusOne字段)分隔NALU
    120     int32_t nal_size;      //由nal_size_len可以知道,保存NALU长度一般可以取(1、2、4字节),我们这里取4字节,兼容所有
    121 
    122     uint32_t cumul_size = 0;//存放当前包中已经处理多少字节数据,当==buf_size表示都处理完了,退出循环
    123     uint32_t buf_size;      //存放in中数据data的大小
    124     const uint8_t* buf;     //采访in中数据的起始地址(注意:使用uint_8,按单字节增长)
    125     const uint8_t* buf_end; //采访in中数据的结束地址
    126 
    127     int ret = 0,i;          //存放返回值,以及循环变量i
    128 
    129     buf = in->data;         //指向AVPacket数据data开头
    130     buf_end = in->data + in->size;  //指向AVPacket数据data的末尾
    131     buf_size = in->size;    //记录AVPacket数据data的大小
    132 
    133     //我们是将AVCC格式数据转为AnnexB格式,所以首先去读取SPS、PPS数据,因为AVCC格式数据保存在extradata中。
    134     //而且AVCC格式用于存储,比如MP4,并非实时流,所以SPS、PPS不会在中间被修改,所以我们获取一次即可!!!!
    135     nal_size_len = h264_extradata_to_annexb(fmt_ctx->streams[in->stream_index]->codec->extradata,
    136                                             fmt_ctx->streams[in->stream_index]->codec->extradata_size,
    137                                             &spspps_pkt,
    138                                             AV_INPUT_BUFFER_PADDING_SIZE
    139                                             );  //获取SPS、PPS数据,并且返回前缀值
    140     if(nal_size_len<0)
    141         return -1;
    142 
    143     out = av_packet_alloc();    //为out AVPacket分配数据空间
    144 
    145     do{
    146         ret = AVERROR(EINVAL);  //初始一个返回错误码,无效参数值(不用管)
    147         if(buf + nal_size_len > buf_end)
    148             goto fail;          //我们输入数据是AVCC格式,该格式数据前4字节用于存放NALU长度信息,如果连这个4字节都不存在,则返回错误
    149         //先假设nal_size_len=4
    150         for(nal_size=0,i=0;i<nal_size_len;i++){  //开始获取NALU长度信息
    151             nal_size = (nal_size<<8) | buf[i];   //注意:视频数据存放时,是大端格式,我们要**读取**长度信息,需要进行相应处理(如果只是单纯写入,就不需要处理,但是我们需要去读取长度)!!!
    152         }
    153 
    154         //buf指针后移,指向NALU数据的header部分
    155         buf += nal_size_len;    //跳过NALU长度部分数据,进入NALU主要数据区域
    156         unit_type = (*buf) & 0x1f;  //header长度为1字节,前3bits影响不大,我们获取后面5bits,去获取NALU类型下信息
    157 
    158         if(nal_size>buf_end-buf||nal_size<0)   //检查长度,是否有效
    159             goto fail;
    160 
    161         //开始判断NALU单元的类型,是否为关键帧,如果是关键帧,我们需要在其前面加入SPS、PPS信息
    162         if(unit_type == 5){
    163             FILE* sp = fopen("spspps.h264","ab");
    164 
    165             len = fwrite(spspps_pkt.data,1,spspps_pkt.size,sp);
    166 
    167             fflush(sp);
    168             fclose(sp);
    169             //先写入start code和SPS和PPS数据,都被保存在前面spspps_pkt的data中,我们转放入out中
    170             if((ret=alloc_and_copy(out,spspps_pkt.data,spspps_pkt.size,buf,nal_size))<0)    //这里是将前面的startcode+SPS+PPS+NALU数据传入函数中,使得函数在NALU 前面加入startcode;这里的startcode不是4字节,而是3字节
    171                 goto fail;
    172         }else{  //对于非关键帧,不需要SPS、PPS数据
    173             if((ret=alloc_and_copy(out,NULL,0,buf,nal_size))<0)    //这里是将前面的NALU数据传入函数中,使得函数在NALU 前面加入startcode
    174                 goto fail;
    175         }
    176 
    177         //将上面的数据,无论是关键帧、非关键帧 都组织好,输出到目标文件中去
    178         len = fwrite(out->data,1,out->size,dst_fd);
    179         if(len != out->size){
    180             av_log(NULL,AV_LOG_DEBUG,"Warning, length of writed data isn`t equal pkt.size(%d,%d)
    ",len,out->size);
    181         }
    182 
    183         fflush(dst_fd);
    184         //开始判断下一个nalu
    185         buf += nal_size;
    186         cumul_size += nal_size + nal_size_len; //算上前缀长度才能对应
    187     }while(cumul_size<buf_size); //循环继续条件
    188 
    189 fail:   //进行统一错误处理
    190     av_packet_free(&out);
    191     return ret;
    192 }
    193 
    194 int main(int argc,char* argv[])
    195 {
    196     int err_code;       //获取返回值
    197     char errors[1024]; //获取ffmpeg返回根据错误码返回的错误信息
    198     char* src = NULL;   //输入文件路径
    199     char* dst = NULL;   //输出文件路径
    200     
    201     av_log_set_level(AV_LOG_INFO);  //设置日志级别
    202 
    203     if(argc<3){         //无法获取src,dst,则返回错误
    204         av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 3!!!
    ");
    205         return -1;
    206     }
    207 
    208     //设置文件路径
    209     src = argv[1];
    210     dst = argv[2];
    211     if(!src || !dst){
    212         av_log(NULL,AV_LOG_ERROR,"the file path of src or dst can`t be empty!!
    ");
    213         return -1;
    214     }
    215 
    216     av_register_all();  //初始化libavformat并注册所有muxer、demuxer和协议
    217 
    218     //打开输入多媒体文件,获取上格式下文
    219     AVFormatContext* fmt_ctx = NULL;
    220     err_code = avformat_open_input(&fmt_ctx,src,NULL,NULL);
    221     if(err_code<0){
    222         av_strerror(err_code,errors,1024);
    223         av_log(NULL,AV_LOG_ERROR,"open media %s file failure : %d,(%s)!!!
    ",src,err_code,errors);
    224         return -1;
    225     }
    226 
    227     //获取找到其中最佳的一路视频流
    228     int video_idx;
    229     //媒体文件句柄 / 流类型 / 请求的流编号(-1则自动去找) / 相关流索引号(比如音频对应的视频流索引号),不指定则-1 / 如果非空,则返回所选流的解码器(指针获取) / flag当前未定义
    230     video_idx = av_find_best_stream(fmt_ctx,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
    231     if(video_idx<0){
    232         av_log(NULL,AV_LOG_DEBUG,"Can`t find %s stream in input file (%s)!!!
    ",
    233             av_get_media_type_string(AVMEDIA_TYPE_VIDEO),src);  //去获取AVMEDIA_TYPE_VIDEO对应的string
    234         avformat_close_input(&fmt_ctx); //释放前面的空间
    235         return -1;
    236     }
    237 
    238     //输出我们获取的流的元信息
    239     err_code = avformat_find_stream_info(fmt_ctx, 0); //获取流详细信息,0表示没有额外参数
    240     if(err_code<0){
    241         av_log(NULL,AV_LOG_WARNING,"Can`t get detail stream information!
    ");
    242     }
    243     //打印我们要获取的流的元数据
    244     av_dump_format(fmt_ctx,video_idx,src,0); ////video_idx是流的索引值,,0表示输入/输出流,由于是输入文件,所以为0
    245     
    246     //打开目标文件
    247     FILE* dst_fd = fopen(dst,"wb");
    248     if(!dst_fd){
    249         av_log(NULL,AV_LOG_ERROR,"Can`t open destination file(%s)
    ",dst);
    250         avformat_close_input(&fmt_ctx); //释放前面的空间
    251         return -1;
    252     }
    253 
    254     //开始从流中读取数据包
    255     //初始化包结构
    256     AVPacket pkt;   
    257     av_init_packet(&pkt);
    258     pkt.data = NULL;
    259     pkt.size = 0;
    260 
    261     while(av_read_frame(fmt_ctx,&pkt)>=0){  //循环获取下一个包
    262         if(pkt.stream_index == video_idx){  //是我们想要的数据包
    263             h264_mp4toannexb(fmt_ctx,&pkt,dst_fd);         //开始进行包写入,先将AVCC格式数据转为AnnexB格式,然后写入目标文件中去
    264         }
    265         //对每一个获取的包进行减引用
    266         av_packet_unref(&pkt);
    267     }
    268 
    269     //开始进行空间释放
    270     avformat_close_input(&fmt_ctx);
    271     if(dst_fd){
    272         fclose(dst_fd);
    273     }
    274 
    275     return 0;
    276 }
    View Code

    (三)程序测试

    gcc ffmpeg_av.c -o fav -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
    ./fav gfxm.mp4 out.h264

    八:多媒体格式转换---将MP4转成FLV格式(数据与参数不变)

    FFmpeg hevc codec_tag兼容问题:https://juejin.cn/post/6854573210579501070

    (一)基础函数了解

    avformat_alloc_output_context2():在基于FFmpeg的音视频编码器程序中,该函数通常是第一个调用的函数(除了组件注册函数av_register_all())。avformat_alloc_output_context2()函数可以初始化一个用于输出的AVFormatContext结构体

    AVFormatContext :
      unsigned int nb_streams;    记录stream通道数目。
      AVStream **streams;    存储stream通道。
    avformat_new_stream() 在 AVFormatContext 中创建 Stream 通道。之后,我们就可以自行设置 AVStream 的一些参数信息。

    AVStream 即是流通道。例如我们将 H264 和 AAC 码流存储为MP4文件的时候,就需要在 MP4文件中增加两个流通道,一个存储Video:H264,一个存储Audio:AAC。(假设H264和AAC只包含单个流通道)。 AVStream包含很多参数,用于记录通道信息,其中最重要的是  :   AVCodecParameters 
    * codecpar  :用于记录编码后的流信息,即通道中存储的流的编码信息。   AVRational time_base :AVStream通道的时间基,时间基是个相当重要的概念。 需要注意的是:现在的 ffmpeg 3.1.4版本已经使用AVCodecParameters * codecpar替换了原先的CodecContext* codec !

    AVStream :
      int index;   在AVFormatContext 中所处的通道索引
    avcodec_parameters_copy() 在new stream之后,还需要把相应的参数拷贝过去。比如SPS、PPS中的参数
    avformat_write_header()    生成多媒体文件头
    av_write_frame()/av_interleaved_write_frame()   后者用得多,用来写入数据
    av_write_trailer()     写多媒体文件尾部(有的文件是包含尾部信息的) 

    (二)代码实现

    补充:时间基的转换FFmpeg学习(四)视频基础

    1、打开输入文件;
    2、创建并打开一个空文件存储 flv 格式音视频数据;
    3、遍历输入文件的每一路流,每个输入流对应创建一个输出流,并将输入流中的编解码参数直接拷贝到输出流中;
    4、写入新的多媒体文件的头;
    5、在循环遍历输入文件的每一帧,对每一个packet进行时间基的转换;
    6、写入新的多媒体文件;
    7、给新的多媒体文件写入文件尾;
    8、释放相关资源。
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavutil/timestamp.h>
    
    static void log_packet(const AVFormatContext *fmt_ctx, const AVPacket *pkt, const char *tag)
    {
        AVRational *time_base = &fmt_ctx->streams[pkt->stream_index]->time_base;
    
        printf("%s: pts:%s pts_time:%s dts:%s dts_time:%s duration:%s duration_time:%s stream_index:%d
    ",
               tag,
               av_ts2str(pkt->pts), av_ts2timestr(pkt->pts, time_base),
               av_ts2str(pkt->dts), av_ts2timestr(pkt->dts, time_base),
               av_ts2str(pkt->duration), av_ts2timestr(pkt->duration, time_base),
               pkt->stream_index);
    }
    
    int main(int argc,char* argv[])
    {
        AVOutputFormat* ofmt = NULL;    //输出格式
        AVFormatContext* ifmt_ctx = NULL,*ofmt_ctx=NULL;    //输入、输出上下文
    
        AVPacket pkt;    //数据包
        const char* in_filename,*out_filename;
    
        int ret,i;
        int stream_idx = 0;
        int* stream_mapping = NULL;        //数组:用来存放各个流通道的新索引值(对于不要的流,设置-1,对于需要的流从0开始递增
        int stream_mapping_size = 0;    //输入文件中流的总数量
    
        av_log_set_level(AV_LOG_INFO);
        if(argc < 3){
            av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 3!
    ");
            return -1;
        }
    
        av_register_all();
        //设置文件路径
        in_filename = argv[1];
        out_filename = argv[2];
    
         //打开输入多媒体文件,获取上下文格式
        ret = avformat_open_input(&ifmt_ctx,in_filename,NULL,NULL);//第三个参数强制指定AVFormatContext中AVInputFormat,一般设置为NULL,自动检测。第四个为附加选项
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t open input file %s 
    ",in_filename);
            goto fail;
        }
    
        //检索输入文件的流信息
        ret = avformat_find_stream_info(ifmt_ctx,NULL);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Fail to retrieve input stream information!
    ");
            goto fail;
        }
    
        //打印关于输入或输出格式的详细信息,例如持续时间,比特率,流,容器,程序,元数据,边数据,编解码器和时基。
        av_dump_format(ifmt_ctx,0,in_filename,0);    //第一个0表示流的索引值,第二个0表示是输入文件
    
        //为输出上下文环境分配空间
        avformat_alloc_output_context2(&ofmt_ctx,NULL,NULL,out_filename);    //第二个参数:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。第三个参数为文件格式比如.flv,也可以通过第四个参数获取
        if(!ofmt_ctx){
            av_log(NULL,AV_LOG_ERROR,"Can`t create output context!
    ");
            ret = AVERROR_UNKNOWN;
            goto fail;
        }
    
        //记录输入文件的stream通道数目
        stream_mapping_size = ifmt_ctx->nb_streams;    
        //为数组分配空间,sizeof(*stream_mapping)是分配了一个int空间,为stream_mapping分配了stream_mapping_size个int空间
        stream_mapping = av_mallocz_array(stream_mapping_size,sizeof(*stream_mapping));    
        if(!stream_mapping){
            ret = AVERROR(ENOMEM);    //内存不足
            goto fail;
        }
    
        //输出文件格式
        ofmt = ofmt_ctx->oformat;
        //遍历输入文件中的每一路流,对于每一路流都要创建一个新的流进行输出
        for(i=0;i<stream_mapping_size;i++){
            AVStream* out_stream = NULL;    //输出流
            AVStream* in_stream = ifmt_ctx->streams[i];    //输入流获取
            AVCodecParameters* in_codecpar = in_stream->codecpar;    //获取输入流的编解码参数
    
            //只保留音频、视频、字母流;对于其他流丢弃(实际上是设置对应的数组值为-1)
            if(in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE){
                stream_mapping[i] = -1;
                continue;
            }
            //对于输出流的index重新编号,从0开始,写入stream_mapping数组对应空间中去
            stream_mapping[i] = stream_idx++;
    
            //重点:为输出格式上下文,创建一个对应的输出流
            out_stream = avformat_new_stream(ofmt_ctx,NULL);    //第二个参数为对应的视频所需要的编码方式,为NULL则自动推导
            if(!out_stream){
                av_log(NULL,AV_LOG_ERROR,"Failed to allocate output stream
    ");
                ret = AVERROR_UNKNOWN;
                goto fail;
            }
    
            //直接将输入流的编解码参数拷贝到输出流中
            ret = avcodec_parameters_copy(out_stream->codecpar,in_codecpar);
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Failed to copy codec parameters
    ");
                goto fail;
            }
    
            //详见:https://juejin.cn/post/6854573210579501070
            //avformat_write_header写入封装容器的头信息时,会检查codec_tag:若AVStream->codecpar->codec_tag有值,则会校验AVStream->codecpar->codec_tag是否在封装格式(比如MAP4)支持的codec_tag列表中,若不在,就会打印错误信息;
            //若AVStream->codecpar->codec_tag为0,则会根据AVCodecID从封装格式的codec_tag列表中,找一个匹配的codec_tag。
            out_stream->codecpar->codec_tag = 0;
        }
    
        //打印要输出多媒体文件的详细信息
        av_dump_format(ofmt_ctx,0,out_filename,1);    //1表示输出文件
    
        if(!(ofmt->flags&AVFMT_NOFILE)){    //查看文件格式状态,如果文件不存在(未打开),则开启文件
            ret = avio_open(&ofmt_ctx->pb,out_filename,AVIO_FLAG_WRITE);    //打开文件,可写
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Can`t open output file %s!
    ",out_filename);
                goto fail;
            }
        }
    
        //开始写入新的多媒体文件头部
        ret = avformat_write_header(ofmt_ctx,NULL);    //NULL为附加选项
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t write format header into file: %s
    ",out_filename);
            goto fail;
        }
    
        //循环写入多媒体数据
        while(1){
            AVStream* in_stream,* out_stream;    //获取输入输出流
            //循环读取每一帧
            ret = av_read_frame(ifmt_ctx,&pkt);
            if(ret<0){    //读取完成,退出喜欢
                break;
            }
            //获取输入流在stream_mapping中的数组值,看是否保留
            in_stream = ifmt_ctx->streams[pkt.stream_index];    //先获取所属的流的信息
            if(pkt.stream_index>=stream_mapping_size||stream_mapping[pkt.stream_index]<0){    //判断是否是我们想要的音频、视频、字幕流,不是的话就跳过
                av_packet_unref(&pkt);
                continue;
            }
            //需要对流进行重新编号(因为原来输入流部分被跳过),输出流编号应该从0开始递增;索引就是我们上面保存的数组值
            pkt.stream_index = stream_mapping[pkt.stream_index];    //按照输出流的编号对pakcet进行重新编号
            //根据上面的索引,获取ofmt_cxt输出格式上下文对应的输出流,进行处理
            out_stream = ofmt_ctx->streams[pkt.stream_index];
    
            //开始对pakcet进行时间基的转换,因为音视频的采用率不同,所以不进行转换,会导致时间不同步。最终使得音频对应音频刻度,视频对应视频刻度
            //PTS(Presentation Time Stamp, 显示时间戳),是渲染用的时间戳,播放器会根据这个时间戳进行渲染播放
            //DTS(Decoding Time Stamp, 解码时间戳),解码时间戳,在视频packet进行解码成frame的时候会使用到
            pkt.pts = av_rescale_q_rnd(pkt.pts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
            pkt.dts = av_rescale_q_rnd(pkt.dts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
            pkt.duration = av_rescale_q(pkt.duration,in_stream->time_base,out_stream->time_base);
            pkt.pos = -1;
            
            log_packet(ofmt_ctx,&pkt,"out");
    
            //将处理好的packet写入输出文件中
            ret = av_interleaved_write_frame(ofmt_ctx,&pkt);
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Error muxing packet
    ");
                break;
            }        
            av_packet_unref(&pkt);
        }
    
        av_write_trailer(ofmt_ctx);    //写入文件尾部
    fail:
        //关闭输入文件格式上下文
        avformat_close_input(&ifmt_ctx);
        //关闭输出文件
        if(ofmt_ctx&&!(ofmt_ctx->flags&AVFMT_NOFILE))
            avio_closep(&ofmt_ctx->pb);
    
        avformat_free_context(ofmt_ctx);    //关闭输出格式上下文
        av_freep(&stream_mapping);    //释放数组空间
        if(ret<0&&ret!=AVERROR_EOF){    //异常退出
            av_log(NULL,AV_LOG_ERROR,"Error occurred: %s
    ",av_err2str(ret));
            return 1;
        }
        return 0;
    }
    View Code

    (三)程序测试

    gcc ffmpeg_flv.c -o fflv -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec

    九:音视频裁剪

    (一)基础函数了解

    FFmpeg提供了一个seek函数,原型如下:

    int av_seek_frame(AVFormatContext *s, int stream_index, int64_t timestamp, int flags);
    

    参数说明:

    s:操作上下文;
    
    stream_index:基本流索引,表示当前的seek是针对哪个基本流,比如视频或者音频等等。
    
    timestamp:要seek的时间点,以time_base或者AV_TIME_BASE为单位。
    
    Flags:seek标志,可以设置为按字节,在按时间seek时取该点之前还是之后的关键帧,以及不按关键帧seek等,详细请参考FFmpeg的avformat.h说明。基于FFmpeg的所有track mode几乎都是用这个函数来直接或间接实现的。

    (二)代码实现

    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavutil/timestamp.h>
    
    int cut_video(char* in_filename,char* out_filename,int starttime,int endtime){
        AVOutputFormat* ofmt = NULL;    //输出格式
        AVFormatContext* ifmt_ctx = NULL,*ofmt_ctx=NULL;    //输入、输出上下文
    
        AVPacket pkt;    //数据包
    
        int ret,i;
        int stream_idx = 0;
        int* stream_mapping = NULL;        //数组:用来存放各个流通道的新索引值(对于不要的流,设置-1,对于需要的流从0开始递增
        int stream_mapping_size = 0;    //输入文件中流的总数量
    
         //打开输入多媒体文件,获取上下文格式
        ret = avformat_open_input(&ifmt_ctx,in_filename,NULL,NULL);//第三个参数强制指定AVFormatContext中AVInputFormat,一般设置为NULL,自动检测。第四个为附加选项
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t open input file %s 
    ",in_filename);
            goto fail;
        }
    
        //检索输入文件的流信息
        ret = avformat_find_stream_info(ifmt_ctx,NULL);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Fail to retrieve input stream information!
    ");
            goto fail;
        }
    
        //打印关于输入或输出格式的详细信息,例如持续时间,比特率,流,容器,程序,元数据,边数据,编解码器和时基。
        av_dump_format(ifmt_ctx,0,in_filename,0);    //第一个0表示流的索引值,第二个0表示是输入文件
    
        //为输出上下文环境分配空间
        avformat_alloc_output_context2(&ofmt_ctx,NULL,NULL,out_filename);    //第二个参数:指定AVFormatContext中的AVOutputFormat,用于确定输出格式。如果指定为NULL,可以设定后两个参数(format_name或者filename)由FFmpeg猜测输出格式。第三个参数为文件格式比如.flv,也可以通过第四个参数获取
        if(!ofmt_ctx){
            av_log(NULL,AV_LOG_ERROR,"Can`t create output context!
    ");
            ret = AVERROR_UNKNOWN;
            goto fail;
        }
    
        //记录输入文件的stream通道数目
        stream_mapping_size = ifmt_ctx->nb_streams;    
        //为数组分配空间,sizeof(*stream_mapping)是分配了一个int空间,为stream_mapping分配了stream_mapping_size个int空间
        stream_mapping = av_mallocz_array(stream_mapping_size,sizeof(*stream_mapping));    
        if(!stream_mapping){
            ret = AVERROR(ENOMEM);    //内存不足
            goto fail;
        }
    
        //输出文件格式
        ofmt = ofmt_ctx->oformat;
        //遍历输入文件中的每一路流,对于每一路流都要创建一个新的流进行输出
        for(i=0;i<stream_mapping_size;i++){
            AVStream* out_stream = NULL;    //输出流
            AVStream* in_stream = ifmt_ctx->streams[i];    //输入流获取
            AVCodecParameters* in_codecpar = in_stream->codecpar;    //获取输入流的编解码参数
    
            //只保留音频、视频、字母流;对于其他流丢弃(实际上是设置对应的数组值为-1)
            if(in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
                in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE){
                stream_mapping[i] = -1;
                continue;
            }
            //对于输出流的index重新编号,从0开始,写入stream_mapping数组对应空间中去
            stream_mapping[i] = stream_idx++;
    
            //重点:为输出格式上下文,创建一个对应的输出流
            out_stream = avformat_new_stream(ofmt_ctx,NULL);    //第二个参数为对应的视频所需要的编码方式,为NULL则自动推导
            if(!out_stream){
                av_log(NULL,AV_LOG_ERROR,"Failed to allocate output stream
    ");
                ret = AVERROR_UNKNOWN;
                goto fail;
            }
    
            //直接将输入流的编解码参数拷贝到输出流中
            ret = avcodec_parameters_copy(out_stream->codecpar,in_codecpar);
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Failed to copy codec parameters
    ");
                goto fail;
            }
    
            //详见:https://juejin.cn/post/6854573210579501070
            //avformat_write_header写入封装容器的头信息时,会检查codec_tag:若AVStream->codecpar->codec_tag有值,则会校验AVStream->codecpar->codec_tag是否在封装格式(比如MAP4)支持的codec_tag列表中,若不在,就会打印错误信息;
            //若AVStream->codecpar->codec_tag为0,则会根据AVCodecID从封装格式的codec_tag列表中,找一个匹配的codec_tag。
            out_stream->codecpar->codec_tag = 0;
        }
    
        //打印要输出多媒体文件的详细信息
        av_dump_format(ofmt_ctx,0,out_filename,1);    //1表示输出文件
    
        if(!(ofmt->flags&AVFMT_NOFILE)){    //查看文件格式状态,如果文件不存在(未打开),则开启文件
            ret = avio_open(&ofmt_ctx->pb,out_filename,AVIO_FLAG_WRITE);    //打开文件,可写
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Can`t open output file %s!
    ",out_filename);
                goto fail;
            }
        }
    
        //开始写入新的多媒体文件头部
        ret = avformat_write_header(ofmt_ctx,NULL);    //NULL为附加选项
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t write format header into file: %s
    ",out_filename);
            goto fail;
        }
    
        //--------------seek定位---------
        ret = av_seek_frame(ifmt_ctx,-1,starttime*AV_TIME_BASE,AVSEEK_FLAG_ANY);
        if(ret<0){
            av_log(NULL,AV_LOG_ERROR,"Can`t seek input file: %s
    ",in_filename);
            goto fail;
        }
    
        //循环写入多媒体数据
        while(1){
            AVStream* in_stream,* out_stream;    //获取输入输出流
            //循环读取每一帧
            ret = av_read_frame(ifmt_ctx,&pkt);
            if(ret<0){    //读取完成,退出喜欢
                break;
            }
            //获取输入流在stream_mapping中的数组值,看是否保留
            in_stream = ifmt_ctx->streams[pkt.stream_index];    //先获取所属的流的信息
            if(pkt.stream_index>=stream_mapping_size||stream_mapping[pkt.stream_index]<0){    //判断是否是我们想要的音频、视频、字幕流,不是的话就跳过
                av_packet_unref(&pkt);
                continue;
            }
            //---------判断是否到结束时间----------
            if(av_q2d(in_stream->time_base)*pkt.pts>endtime){    //av_q2d获取该流的时间基
                av_free_packet(&pkt);
                break;
            }
    
            //需要对流进行重新编号(因为原来输入流部分被跳过),输出流编号应该从0开始递增;索引就是我们上面保存的数组值
            pkt.stream_index = stream_mapping[pkt.stream_index];    //按照输出流的编号对pakcet进行重新编号
            //根据上面的索引,获取ofmt_cxt输出格式上下文对应的输出流,进行处理
            out_stream = ofmt_ctx->streams[pkt.stream_index];
    
            //开始对pakcet进行时间基的转换,因为音视频的采用率不同,所以不进行转换,会导致时间不同步。最终使得音频对应音频刻度,视频对应视频刻度
            //PTS(Presentation Time Stamp, 显示时间戳),是渲染用的时间戳,播放器会根据这个时间戳进行渲染播放
            //DTS(Decoding Time Stamp, 解码时间戳),解码时间戳,在视频packet进行解码成frame的时候会使用到
            pkt.pts = av_rescale_q_rnd(pkt.pts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
            pkt.dts = av_rescale_q_rnd(pkt.dts,in_stream->time_base,out_stream->time_base,AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
            pkt.duration = av_rescale_q(pkt.duration,in_stream->time_base,out_stream->time_base);
            pkt.pos = -1;
            
            //将处理好的packet写入输出文件中
            ret = av_interleaved_write_frame(ofmt_ctx,&pkt);
            if(ret<0){
                av_log(NULL,AV_LOG_ERROR,"Error muxing packet
    ");
                break;
            }        
            av_packet_unref(&pkt);
        }
    
        av_write_trailer(ofmt_ctx);    //写入文件尾部
    fail:
        //关闭输入文件格式上下文
        avformat_close_input(&ifmt_ctx);
        //关闭输出文件
        if(ofmt_ctx&&!(ofmt_ctx->flags&AVFMT_NOFILE))
            avio_closep(&ofmt_ctx->pb);
    
        avformat_free_context(ofmt_ctx);    //关闭输出格式上下文
        av_freep(&stream_mapping);    //释放数组空间
        if(ret<0&&ret!=AVERROR_EOF){    //异常退出
            av_log(NULL,AV_LOG_ERROR,"Error occurred: %s
    ",av_err2str(ret));
            return 1;
        }
        return 0;
    }
    
    int main(int argc,char* argv[])
    {
        av_log_set_level(AV_LOG_INFO);
        if(argc < 5){
            av_log(NULL,AV_LOG_ERROR,"The number of parameters must be greater than 5!
    ");
            return -1;
        }
    
        av_register_all();
        //设置文件路径
        int starttime = atoi(argv[3]);
        int endtime = atoi(argv[4]);
        cut_video(argv[1],argv[2],starttime,endtime);
        return 0;
    }
    View Code

    (三)程序测试

    gcc ffmpeg_seek.c -o fs -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec
    ./fs gfxm.mp4 gfxm_2.mp4 10 20

    ffplay gfxm_2.mp4

  • 相关阅读:
    五种提高 SQL 性能的方法
    join 使用详解方式
    关于MagicAjax的用法
    收藏几段SQL Server语句和存储过程
    ubuntu nfs配置 以及mount.nfs:access denied by server while mounting问题解决
    Hisi开发板上 SQLite3.3.8移植
    父进程非阻塞回收子进程(适用LINUX下C语言的clientserver模型)
    busybox asm/page.h: No such find.
    ubuntu11.10 samba服务器配置
    errno定义
  • 原文地址:https://www.cnblogs.com/ssyfj/p/14579909.html
Copyright © 2020-2023  润新知