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\n",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\n");
        return -1;
    }
    av_log(NULL,AV_LOG_INFO,"Success to move\n");

    ret = avpriv_io_delete(filename);
    if(ret<0){
        av_log(NULL,AV_LOG_ERROR,"Failed to delete\n");
        return -1;
    }

    av_log(NULL,AV_LOG_INFO,"Success to delete %s\n",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\n");
        return -1;
    }
    av_log(NULL,AV_LOG_INFO,"Success to move\n");

    ret = avpriv_io_delete(filename);
    if(ret<0){
        av_log(NULL,AV_LOG_ERROR,"Failed to delete\n");
        return -1;
    }

    av_log(NULL,AV_LOG_INFO,"Success to delete %s\n",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\n",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\n",av_err2str(ret));
            goto _fail;
        }
        if(!entry) break;
        av_log(NULL,AV_LOG_INFO,"%12"PRId64" %s\n",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\n",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)!\n");
    }

    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!\n");
        return -1;
    }

    src = argv[1];
    dst = argv[2];
    if(!src||!dst){
        av_log(NULL,AV_LOG_ERROR,"src or dst is null!\n");
        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\n",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!\n");
        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)!\n");
    }
    //打印我们要获取的流的元数据
    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!\n");
        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!\n");
            }

            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!\n");
            }
        }
        //每读取一次包,就需要将包引用-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\n");
 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\n");
 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.\n");
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.\n");
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)\n",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!!!\n");
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!!\n");
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)!!!\n",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)!!!\n",
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!\n");
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)\n",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\n",
           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!\n");
        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 \n",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!\n");
        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!\n");
        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\n");
            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\n");
            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!\n",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\n",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\n");
            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\n",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 \n",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!\n");
        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!\n");
        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\n");
            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\n");
            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!\n",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\n",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\n",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\n");
            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\n",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!\n");
        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

 

作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

posted @   山上有风景  阅读(6080)  评论(2编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
历史上的今天:
2020-04-30 机器学习基础---逻辑回归(假设函数与线性回归不同)
2019-04-30 03--STL算法(常用算法)
2019-04-30 STL函数适配器
2019-04-30 02--STL算法(函数对象和谓词)
2018-04-30 python---图表的使用
点击右上角即可分享
微信分享提示