两种方法实现抽取一个视频文件中音频流(基于ffmpeg)

使用到的ffmpeg API
av_log_set_level()
设置日志打印的标准, 高于这个标准的将不会打印

av_log()
打印ffmpeg中的日志,当做printf用

av_dump_format()
打印流的各种信息

av_register_all()
初始化所有组件

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options)

打开一个文件并解析,传入文件地址,输入文件的上下文会保存在 ps
主要是完成了对输入的AVFormatContext中的AVInputFormat的初始化,对AVIOContext打开输入控制,以及创建AVStream。

avformat_close_input()
传入上下文,关闭文件

av_find_best_stream()
输入流的上下文和要找的流的类型,返回最好流的索引

av_init_packet()
初始化一个包,保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息

av_read_frame()
返回流的下一帧,当不再需要数据包时,必须使用av_packet_unref释放该数据包,当到文件结尾时返回负数

av_packet_unref()
取消引用数据包引用的缓冲区,并将剩余的数据包字段重置为其默认值。

avformat_alloc_context()
用来申请AVFormatContext类型变量空间并初始化默认参数,返回AVFormatContext类型上下文

av_guess_format()
根据输入的文件名生成最适合的输出容器,也可以指定文件类型,返回容器的上下文

avformat_new_stream()
创建流通道,返回AVStream
首先调用av_mallocz()为AVStream分配内存。
接着给新分配的AVStream的各个字段赋上默认值。
然后调用了另一个函数avcodec_alloc_context3()初始化AVStream中的AVCodecContext。

avcodec_parameters_copy()
拷贝avstream中的codecpar的信息,这里将输入流的参数信息拷贝到输出流中

*int av_strerror(int errnum, char errbuf, size_t errbuf_size)
errnum指错误码,errbuf返回错误的buf,错误buf的size,给错误信息赋值,为打印做准备

avformat_write_header
写输出文件流的头

av_rescale_q_rnd
计算 “a * b / c” 的值并分五种方式来取整.

av_rescale_q
av_rescale_q_rnd的特殊情况

av_interleaved_write_frame
此函数将根据需要在内部缓冲数据包,以确保输出文件中的数据包按照增加dts的顺序正确地交织在一起

av_write_trailer
将流尾随写入输出媒体文件并释放文件私有数据。只能在成功调用avformat_write_header后调用。

avio_open
创建并初始化一个AVIOContext,用于访问由url指示的资源。

avio_close
关闭

使用到的ffmpeg的数据结构

AVFormatContext(统领全局,主要存储视音频封装格式中包含的信息)

  |------AVInputFormat(存储输入视音频使用的封装格式)

     |------AVOutputFormat(存储输出音视频使用的封装格式)

     |------AVIOContext(处在协议层)

     |              |---------URLContext(存储视音频使用的协议的类型以及状态)

     |                                   |--------- URLProtocol(存储输入视音频使用的封装格式)

     |------AVStream(存储一个视频/音频流的相关数据)

                      |---------AVCondecContext(存储该视频/音频流使用解码方式的相关数据)

                                               |---------AVCodec(视频/音频对应的解码器)

AVOutputFormat

对流(Stream)的封装和抽象,描述了视频、音频等流的编码格式等基本流信息。此外也是音频、视频、字母数据流的重要载体。

AVStream
存储每一个视频/音频流信息的结构体
AVCodecContext *codec        // 已过时,使用另一个 codecpar 结构体代替。
AVRational time_base         // 时间基数。
int64_t duration            // 总时长。流的总时长,该参数不可靠。
AVRational avg_frame_rate       // 帧率。
AVCodecParameters *codecpar;     // 包含音视频参数的结构体。很重要,可以用来获取音视频参数中的宽度、高度、采样率、编码格式等信息。

AVPacket
保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息
关于数据的属性有以下字段:

pts: (int64_t)显示时间,结合AVStream->time_base转换成时间戳

dts: (int64_t)解码时间,结合AVStream->time_base转换成时间戳

size: (int)data的大小

stream_index: (int)packet在stream的index位置

flags: (int)标示,结合AV_PKT_FLAG使用,其中最低为1表示该数据是一个关键帧。

#define AV_PKT_FLAG_KEY 0x0001 //关键帧

#define AV_PKT_FLAG_CORRUPT 0x0002 //损坏的数据

#define AV_PKT_FLAG_DISCARD 0x0004 /丢弃的数据

side_data_elems: (int)边缘数据元数个数

duration: (int64_t)数据的时长,以所属媒体流的时间基准为单位,未知则值为默认值0

pos: (int64_t )数据在流媒体中的位置,未知则值为默认值-1

convergence_duration:该字段已deprecated,不在使用

关于数据缓存,AVPacket本身只是个容器,不直接的包含数据,而是通过数据缓存的指针引用数据。AVPacket包含两种数据

uint8_t *data:指向保存压缩数据的指针,这就是AVPacket的实际数据。

AVPacketSideData *side_data:容器提供的一些附加数据

AVBufferRef *buf:用来管理data指针引用的数据缓存,其使用在后面介绍

第一种方式
先取出一个个音频包,然后在每个音频包前边手动的加 ADTS Header写成一个文件。

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef __cplusplus
extern "C" {
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfilter.h>
#include <libavutil/log.h>

#ifdef __cplusplus
};
#endif

void adts_header(char *szAdtsHeader, int dataLen)
{

    int audio_object_type = 2;
    int sampling_frequency_index = 7;
    int channel_config = 2;

    int adtsLen = dataLen + 7;

    szAdtsHeader[0] = 0xff;      //syncword:0xfff                          高8bits
    szAdtsHeader[1] = 0xf0;      //syncword:0xfff                          低4bits
    szAdtsHeader[1] |= (0 << 3); //MPEG Version:0 for MPEG-4,1 for MPEG-2  1bit
    szAdtsHeader[1] |= (0 << 1); //Layer:0                                 2bits
    szAdtsHeader[1] |= 1;        //protection absent:1                     1bit

    szAdtsHeader[2] = (audio_object_type - 1) << 6;            //profile:audio_object_type - 1                      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

    szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value    中间8bits
    szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5);   //frame length:value    低3bits
    szAdtsHeader[5] |= 0x1f;                             //buffer fullness:0x7ff 高5bits
    szAdtsHeader[6] = 0xfc;
}

int main(int argc,char * argv[])//argc为参数的个数,argv[0]为程序的名字,之后为输入的参数
{

    char* src=NULL;
    char* dst=NULL;
    int ret;
    int len;
    int audio_index;
    AVFormatContext *fmt_ctx=NULL;
    AVPacket  pkt;

    av_log_set_level(AV_LOG_INFO);
    av_register_all();//注册,必须要

    //拿到输入参数
    if(argc<3)
    {
        av_log(NULL, AV_LOG_ERROR, "can't read dir!\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;
    }

    //打开src文件
    ret=avformat_open_input(&fmt_ctx,src,NULL,NULL);//输出fmt_ctx
    if(ret<0)
    {
    av_log(NULL, AV_LOG_ERROR, "can't read dir:%s\n",av_err2str(ret));//将错误码转化为字符串打印
    avformat_close_input(&fmt_ctx);
    return -1;
    }

    //打印mata信息
    av_dump_format(fmt_ctx,0,src,0);//第四个参数0为输入,1为输出  打印mata信息api

    //打开输出文件
    FILE* dst_fd=fopen(dst,"wb");
    if(dst_fd<0)
    {
        av_log(NULL, AV_LOG_ERROR, "can't open out file \n");
        return  -1;
    }

    //2.get stream
    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");
        fclose(dst_fd);
        avformat_close_input(&fmt_ctx);
        return  -1;
    }
    audio_index=ret;
    //初始化包
    av_init_packet(&pkt);
    while(av_read_frame(fmt_ctx,&pkt)>=0)//从媒体流中读取帧填充到填充到Packet的数据缓存空间,到文件末尾返回<0退出while
    {
        if(pkt.stream_index==audio_index)//判断该AVPacket所属的视频/音频流
        {
            //每一帧音频数据前加入adts_header
            char adts_header_buf[7];
            adts_header(adts_header_buf,pkt.size);
            fwrite(adts_header_buf,1,7,dst_fd);

            //3.write audio data to acc file
            len=fwrite(pkt.data,1,pkt.size,dst_fd);
            if(len!=pkt.size)
            {
                av_log(NULL, AV_LOG_WARNING, "wanning ,length of data is no equal \n");
            }
        }
        av_packet_unref(&pkt);//将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间
    }


    avformat_close_input(&fmt_ctx);

    if(dst_fd)
    {
        fclose(dst_fd);
    }


//输出一下视频信息 
/*   avformat_open_input(&fmt_ctx,"/home/ky/ffmpeg_demo/new/test.aac",NULL,NULL);
    av_dump_format(fmt_ctx,0,src,0);
    avformat_close_input(&fmt_ctx);
    return 0;
}
*/

第一种方式只对HE-AAC有效,而对于 LC-AAC,建立使用通用的第二种方式

第二种方法

使用 ffmpeg API 直接创建一个 AAC 文件,在 ffmpeg库内部会自己查找到对应的多媒体格式帮你做好 ADTS Header,并最终写好AAC 文件。

核心流程:
调用 av_guess_format 让ffmpeg帮你找到一个合适的文件格式。
调用 avformat_new_stream 为输出文件创建一个新流。
调用 avio_open 打开新创建的文件。
调用 avformat_write_header 写文件头。
调用 av_interleaved_write_frame 写文件内容。
调用 av_write_trailer 写文件尾。
调用 avio_close 关闭文件。

#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avformat.h>

int main(int argc,char * argv[])
{

    int err_code;
    char errors[1024];

    char *src_fileName = NULL;
    char *dst_fileName = NULL;
    //上下文
    AVFormatContext *fmt_ctx = NULL;
    AVFormatContext *ofmt_ctx = NULL;

    //支持各种各样的输出文件格式,MP4,FLV,3GP等等
    AVOutputFormat *output_fmt = NULL;

    //输入流
    AVStream *in_stream = NULL;

    //输出流
    AVStream *out_stream = NULL;

    //存储压缩数据
    AVPacket packet;

    //要拷贝的流
    int audio_stream_index = -1;
    //注册
    av_register_all();
    //拿到输入参数
    if(argc < 3)
    {
        av_log(NULL, AV_LOG_DEBUG, "the count of parameters should be more than three!\n");
        return -1;
    }
    src_fileName = argv[1];
    dst_fileName = argv[2];
            
    //打开输入文件,关于输入文件的所有就保存到fmt_ctx中了
    err_code = avformat_open_input(&fmt_ctx, src_fileName, NULL, NULL);
    if (err_code < 0)
    {
        av_log(NULL, AV_LOG_ERROR, "cant open 1 file:%s\n", av_err2str(err_code));
        return -1;
    }

    if(fmt_ctx->nb_streams<2)
    {
        //流数小于2,说明这个文件音频、视频流这两条都不能保证,输入文件有错误
        av_log(NULL, AV_LOG_ERROR, "输入文件错误,流不足2条\n");
        exit(1);
    }

    //拿到文件中音频流
    in_stream = fmt_ctx->streams[1];
    //aac参数信息
    AVCodecParameters *in_codecpar = in_stream->codecpar;

    //找到最好的音频流
    audio_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if(audio_stream_index < 0)
    {
        av_log(NULL, AV_LOG_DEBUG, "寻找最好音频流失败,请检查输入文件!\n");
        return AVERROR(EINVAL);
    }

    //2.准备 output file and stream
    // 输出上下文
    ofmt_ctx = avformat_alloc_context();

    //根据目标文件名生成最适合的输出容器
    output_fmt = av_guess_format(NULL,dst_fileName,NULL);
    if(!output_fmt)
    {
        av_log(NULL, AV_LOG_DEBUG, "根据目标生成输出容器失败!\n");
        exit(1);
    }

    ofmt_ctx->oformat = output_fmt;

    //新建输出流
    out_stream = avformat_new_stream(ofmt_ctx, NULL);
    if(!out_stream)
    {
        av_log(NULL, AV_LOG_DEBUG, "创建输出流失败!\n");
        exit(1);
    }
    
    
    //3.data copy
    // 将参数信息拷贝到输出流中,我们只是抽取音频流,并不做音频处理,所以这里只是Copy
    if((err_code = avcodec_parameters_copy(out_stream->codecpar, in_codecpar)) < 0 )
    {
        av_strerror(err_code, errors, 1024);
        av_log(NULL, AV_LOG_ERROR,"拷贝编码参数失败!, %d(%s)\n",
        err_code, errors);
    }
    
    //初始化AVIOContext,文件操作由它完成
    if((err_code = avio_open(&ofmt_ctx->pb, dst_fileName, AVIO_FLAG_WRITE)) < 0)//open new file 
    {
        av_strerror(err_code, errors, 1024);
        av_log(NULL, AV_LOG_DEBUG, "Could not open file %s, %d(%s)\n",
        dst_fileName,
        err_code,
        errors);
        exit(1);
    }


    //初始化 AVPacket, 我们从文件中读出的数据会暂存在其中
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    // 写头部信息
    if (avformat_write_header(ofmt_ctx, NULL) < 0)
    {
        av_log(NULL, AV_LOG_DEBUG, "Error occurred when opening output file");
        exit(1);
    }


    //每读出一帧数据
    while(av_read_frame(fmt_ctx, &packet) >=0 )
    {
        if(packet.stream_index == audio_stream_index)
        {
            //时间基计算,音频pts和dts一致
            packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
            packet.dts = packet.pts;
            packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
            packet.pos = -1;
            packet.stream_index = 0;
            //将包写到输出媒体文件
            av_interleaved_write_frame(ofmt_ctx, &packet);
            //减少引用计数,避免内存泄漏
            av_packet_unref(&packet);
    }
    }

    //写尾部信息
    av_write_trailer(ofmt_ctx);

    //最后别忘了释放内存
    avformat_close_input(&fmt_ctx);
    avio_close(ofmt_ctx->pb);
    return 0;
    }

参考链接
https://blog.csdn.net/weixin_33980459/article/details/88010202?utm_medium=distribute.pc_relevant.none-task-blog-title-3&spm=1001.2101.3001.4242

https://www.imooc.com/article/254733

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值