当前位置: 首页 > news >正文

FFmpeg实战 - 解复用与解码

大纲目录

文章目录

  • 前置知识
    • 音视频基础概念
    • 解复用、解码的流程分析
    • FFMPEG有8个常用库
  • 常见音视频格式的介绍
    • aac格式介绍(ADTS)
    • h264格式分析
    • FLV和MP4格式介绍
  • FFmpeg解码解封装实战
    • 数据包和数据帧(AVPacket/AVFrame)
      • AVPacket/AVFrame的引用计数问题
      • API介绍
      • 注意事项
    • FFmpeg解复用实战
      • 分离流
      • 提取流
      • demo样例
    • FFmpeg解码实战
      • 用到的结构体与API
      • 解码流程
      • demo样例
  • 内容补充

前置知识

音视频基础概念

  • 容器:即特定格式的多媒体文件,比如mp4、 flv、 mkv等都是指的容器格式。容器并不直接参与处理音频流和视频流,只是负责存储这些流。
  • 压缩格式:音视频的压缩格式是指用来压缩和解压数据的算法和技术,它决定了数据如何从原始状态转换为压缩状态,以及如何从压缩状态还原回原始状态。例如音频压缩格式aac和视频压缩格式h264等。
  • 媒体流(Stream):一个完整的视频当中一般包含多种媒体流,如音频流、视频流、字幕流等。例如一个mp4视频中如果有一个视频流和两个音频流,则这个视频中就有三个媒体流(stream)。其中视频流绝大多数情况都是经过压缩的,例如h264格式。音频流大多数也是经过压缩的,如aac格式,但有时也会使用未经压缩的pcm数据。
  • 采样点(Sample):采样点是指在数字化的过程中,从连续信号中抽取出来的离散值。例如一个音频的采样频率(采样率)为48000Hz,就表示每秒钟采样48000次,即每秒钟会有48000个采样点。而音频采样点就等同于是像素。
  • 帧内采样点的数量:音频中一个帧所包含的采样点数量。例如一个音频的采样率为44.1 kHz,帧内采样点为1024个,则每一帧所持续的时间就为 1024/44410≈0.0232 秒。
  • 帧数据:视频是由一系列连续的图像组成的,每一幅图像被称为一帧。而一帧音频的长度是由其帧内采样点的数量决定的。
  • 数据包(Packet)/数据帧(Frame):一个媒体流由大量的数据包或数据帧构成。压缩(未经解码处理)的媒体流是由数据包(Packet)构成的,未压缩(解码后)的原生媒体流是由数据帧(Frame)构成的。一个packet/frame就表示一帧音频或视频数据。
  • 解复用器(AVformat):解复用器的主要功能有——容器识别(识别多媒体文件的容器格式,例如MP4、AVI、MKV、FLV等)、流提取(分离多媒体文件中的各个数据流,例如视频流、音频流、字幕流等)、解封装(将提取出来的媒体流解封装为大量的数据包packet)等。所以解复用器并不只是用于解封装
  • 编解码器(AVcodec):解码器主要用于将packet解码(解压)为frame,编码器则负责将frame编码(压缩)为packet。

解复用、解码的流程分析

参照下图分析音视频解封装、解码的过程

在这里插入图片描述

解复用(解封装):要先经过解复用器(AVformat)对其处理,过滤器会分离出不同的媒体流,并将其拆分为packet,然后将产生的packet按照帧的先后顺序放到对应的packet队列中。

解码:接着解码器就从packet队列中拿数据,将packet解码为原生媒体流,也按照顺序将其放到frame队列中。

之所以要经过这一系列的过程,是因为一个压缩的媒体文件是无法直接播放,需要经解码处理后将其恢复为可播放的原始数据。

FFMPEG有8个常用库

  1. AVUtil:核心工具库,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
  2. AVFormat:文件格式和协议库,该模块是最重要的模块之一,封装了Protocol层和Demuxer、 Muxer层,使得协议和格式对于开发者来说是透明的。
  3. AVCodec:编解码库,封装了Codec层,但是有一些Codec是具备自己的License的, FFmpeg是不会默认添加像libx264、 FDK-AAC等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。
  4. AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理是非常方便同时也非常高效的一种方式。
  5. AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要SDL的预先编译,因为该设备模块播放声音与播放视频使用的都是SDL库。
  6. SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。
  7. SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据,缩放尺寸由1280720变为800480。
  8. PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数。

常见音视频格式的介绍

aac格式介绍(ADTS)

aac的格式有两种:ADIF不常用,ADTS是主流,所以这里主要讲解ADTS。简单来说,ADTS可以在任意帧解码,也就是说它每⼀帧都有头信息。ADIF只有⼀个统⼀的header,所以必须得到所有的数据后解码。参考下图
在这里插入图片描述

⼀个AAC原始数据块⻓度是可变的,对原始帧加上ADTS头进⾏ADTS的封装,就形成了ADTS帧。参考下图

adts-header的长度一般为7字节,当protection_absent=0时,表示需要校验码,此时的adts-header就会额外添加一个2字节的校验码,此时的adts-header长度就为9字节。

⼀般情况下ADTS的头信息都是7个字节,分为2部分:

  1. adts_fixed_header
  2. adts_variable_header

其中,adts_fixed_header为固定头信息,adts_variable_header是可变头信息。固定头信息中的数据每⼀帧都相同,⽽可变头信息则在帧与帧之间不同。 参考下图

注:ADTS Header的长度可能为7字节或9字节,当protection_absent字段为时,表示需要校验码,此时是9字节;否则为7字节。

常见的header字段如下:

  • 同步字(syncword):2个字节(16位) 同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF。
  • ID (MPEG Version):1个字节(8位) ID指示使用的MPEG版本。值为0表示MPEG-4,值为1表示MPEG-2。
  • Layer:2个比特 Layer定义了音频流所属的层级,对于AAC来说,其值为0。
  • Protection Absent:1个比特 Protection Absent指示是否启用CRC错误校验。当该比特为0时,表明音频数据经过CRC校验,否则未经过CRC校验。
  • Profile:2个比特 Profile指示编码所使用的AAC规范类型,如AAC LC、AAC HE-AAC等。
  • Sampling Frequency Index (Sampling Rate):4个比特 Sampling Frequency Index表示采样率的索引,它告诉解码器当前音频数据的采样率。这个值的范围是0到15,每个值表示一个特定的采样率。参考下图
    在这里插入图片描述
  • Private Bit:1个比特 Private Bit为私有比特,通常被设置为0,没有实际作用。
  • Channel Configuration:3个比特 Channel Configuration指示音频的通道数,如单声道、立体声或多声道等。
  • Originality:1个比特 Originality指示编码数据是否被原始产生,通常为0。
  • Home:1个比特 Home bit通常被设置为0,没有实际作用。
  • Emphasis:2个比特 Emphasis指示对信号进行强调处理的类型,一般不使用。
  • sampling_frequency_index:表示使⽤的采样率下标,通过这个下标在Sampling Frequencies[ ]数组中查找得知采样率的值。

这里只是对aac格式的简单介绍,想要了解更多内容,参考:AAC-ADTS格式分析【转载】-CSDN博客

h264格式分析

H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。

H264主要分为两层:编码层(Video Coding Layer,VCL)和网络抽象层(NetworkAbstraction Layer (NAL));前者定义了各种编码的算法,后者将前者编码的数据按照一定的方式进行打包存储或者传输。而NAL单元(NALU)作为可以单独可以解码的结构,整个H264的码流可以理解为由多个NALU组成的。这里我们主要介绍NALU。

先来认识一些相关概念

  • SPS:序列参数集,SPS中保存了⼀组编码视频序列的全局参数。
  • PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
  • I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
  • P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
  • B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。
  • 在I帧之前,至少有一个SPS和PPS。
  • GOP:GOP是一组连续的视频帧,这些帧按照一定的编码规则被组织在一起。GOP的设计目的是为了提高视频数据的压缩效率,并且使得视频流能够支持随机访问。

H.264/AVC只是定义了一种标准,常见的具体格式有两种:AnnexB格式和AVCC格式。AnnexB格式主要用于实时播放(.h264文件就是这种格式),AVCC格式主要用于视频存储,即AnnexB是能够直接播放的,而AVCC不能直接播放。

AnnexB格式[start code]NALU | [start code] NALU | ...

SPS和PPS被嵌入到视频流中,其本身也是一种NALU。这种格式比较常见,也就是我们熟悉的每个帧前面都有0x00 00 00 01或者0x00 00 01作为起始码。

在这里插入图片描述

AVCC格式([extradata]) | ([length] NALU) | ([length] NALU) | ...

这里的NALU一般没有SPS PPS等参数信息,参数信息属于extradata位于文件的头部。比如ffmpeg中解析mp4文件后SPS PPS存在streams[index]->codecpar->extradata中。

在这里插入图片描述

AnnexB和AVCC的区别在于:

  1. NALU之间的分隔方式不同:AnnexB是通过在每一个NALU前面添加一个start code,而AVCC则是通过在NALU前都加上一个大端格式的前缀,表示NALU的长度。
    AnnexB格式的start code有两种:
       ①3字节 0x000001   单帧多[slice](即单帧多个NALU)之间间隔
       ②4字节 0x00000001  帧之间,或者SPS等之前
    而AVCC格式的长度前缀一般设置为4个字节。

  2. SPS和PPS的位置不同:AnnexB是将SPS和PPS直接嵌入到视频流中的,SPS和PPS也是一种NALU。即每一个GOP的起始位置都有一个SPS和PPS,所以解码器可以从AnnexB格式的视频流随机点开始进行解码。而AVCC格式格式是将视频的元数据和SPS和PPS等内容统一放到文件的头部,这部分内容通常称为extradata或者sequence header。所以AVCC格式主要用于视频存储,MP4、MKV通常用AVCC格式来存储。

这里主要介绍AnnexB格式和AVCC格式的区别,想要了解H264-NALU的结构,可以参考:H264基础简介【转载】-CSDN博客,这篇博客以AnnexB格式为例,介绍了h264的格式。

FLV和MP4格式介绍

  • FLV格式

FLV封装格式是由⼀个⽂件头(file header)和 ⽂件体(file Body)组成。其中,FLV body由⼀对对的(Previous Tag Size字段 + tag)组成。Previous Tag Size字段 排列在Tag之前,占⽤4个字节。Previous Tag Size记录了前⾯⼀个Tag的⼤⼩,⽤于逆向读取处理。FLV header后的第⼀个Pervious Tag Size的值为0。 参考下图

在这里插入图片描述

这里只是对flv格式的简单介绍,详情参考:FLV文件格式分析【转载】-CSDN博客

  • MP4格式

MP4协议本身没有多复杂,没啥特别难理解的地方,关键的“复杂”点就在于其“大”,嵌套的各种各样的子box。详情参考:整理mp4协议重点【转载】-CSDN博客

FFmpeg解码解封装实战

数据包和数据帧(AVPacket/AVFrame)

AVPacket/AVFrame的引用计数问题

在FFmepg中,数据包对应的结构体为AVPacket,数据帧对应的结构体为AVFrame。一个AVPacket/AVFrame就表示一帧视频数据或音频数据。

特别的是,AVPacket/AVFrame的内存模型比较特殊,因为可能出现多个AVPacket/AVFrame对应同一帧数据的情况,所以FFmepg采用了一种引用计数的方式,以避免内存浪费。

参考下图理解:

在这里插入图片描述

AVPacket/AVFrame变量本身并不直接存储数据,而是指向一块缓存空间AVBuffer,由缓冲区自身来维护引用计数和真正的媒体数据。以AVPacket为例,对于多个AVPacket共享同一个缓存空间的情况, FFmpeg引用计数的机制如下 :(AVFrame也是如此)

  • 初始化时引用计数的指为0,只有真正分配AVBuffer的时候,引用计数的值才加至1。
  • 当有新的Packet引用共享的缓存空间时, 就将引用计数+1。
  • 当释放了引用共享空间的Packet时,就将引用计数-1;
  • 引用计数减至0时,就释放掉引用的缓存空间AVBuffer。

API介绍

AVPacket:

  1. AVPacket *av_packet_alloc();为AVPacket申请空间,此时并未创建AVBuffer。
  2. void av_init_packet(AVPacket *pkt);初始化pkt中的相关字段,例如将整型数据设为0,将指针为null等操作。
  3. int av_new_packet(AVPacket *pkt, int size);创建数据包,申请一个size字节大小的AVBuffer,并让pkt的AVBufferRef指向它。此时才是真正的创建了AVBuffer。
  4. int av_packet_ref(AVPacket *dst, const AVPacket *src);对给定数据包设置一个新的引用。其作用是将dst的AVBufferRef指向src的AVBuffer,即让dst也关联到src的AVBuffer。此时对应的AVBuffer的引用计数加一。
  5. void av_packet_unref(AVPacket *pkt);擦除一个数据包。取消pkt和它对应AVBuffer的关联,并使其引用计数减一。如果AVBuffer的引用计数减为0了,则FFmpeg会释放掉这块AVBuffer的空间。av_packet_unref会有安全检查,所以不用担心uref一个空指针的情况。
  6. void av_packet_move_ref(AVPacket *dst, AVPacket *src);将src中的每个字段移动到dst,并重置(清空)src。此时src与AVBuffer的关联断掉,转移到dst上面,AVBuffer的引用计数不变。
  7. AVPacket *av_packet_clone(const AVPacket *src);AVPacket克隆,相当于av_packet_alloc + av_packet_ref。创建一个和src一样的AVPacket,并作为返回值返回给上层。此时对应的AVBuffer的引用计数加一。
  8. void av_packet_free(AVPacket **pkt);释放AVPacket,要和av_packet_alloc搭配使用,成对出现。

AVFrame:

  1. AVFrame *av_frame_alloc();为AVFrame 申请空间,作用与av_packet_alloc一样。
  2. int av_frame_ref(AVFrame *dst, const AVFrame *src);对给定数据包设置一个新的引用。作用与av_packet_ref一样。
  3. void av_frame_unref(AVFrame *frame);擦除一个数据包。作用与av_packet_unref一样。
  4. void av_frame_move_ref(AVFrame *dst, AVFrame *src);将src中的每个字段移动到dst,并重置(清空)src。作用与av_packet_move_ref一样。
  5. int av_frame_get_buffer(AVFrame*frame,int align);为媒体数据分配新的缓冲区,根据AVFrame分配内存。
  6. AVFrame *av_frame_clone(const AVFrame *src);作用与av_packet_clone一样。
  7. void av_frame_free(AVFrame **frame);释放AVFrame,要和av_frame_alloc搭配使用,成对出现。

注意事项

  1. AVPacket/AVFrame和AVBuffer是两回事,AVBuffer是真实的数据缓冲空间,AVPacket/AVFrame并不直接存储媒体数据,而是有能够访问到AVBuffer的引用字段。所以AVPacket/AVFrame和AVBuffer都需要为其分配空间,就好像指针需要4/8字节空间,而它指向的数据也需要分配空间。
  2. av_init_packet会将字段下的所有指针置为null,所以如果此时的AVPacket字段中还关联的AVBuffer数据而没有释放,在其指针置为null后就会失去关联,此时的AVBuffer就永远无法得到释放了,就会造成内存泄漏。所以av_init_packet函数不能滥用,很容易导致内存泄漏。

FFmpeg解复用实战

解复用是指将一个复合的音视频文件或流中的不同数据流分离出来。

分离流

  • 用到的结构体与API
  1. AVFormatContext *avformat_alloc_context();申请一个AVFormatContext结构内存,并进行简单的初始化。此时AVFormatContext中还没有数据。其中AVFormatContext是解复用器上下文结构体。

  2. void avformat_free_context(AVFormatContext *s);释放 AVFormatContext 及其所有流。

  3. int avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);
    打开输入的媒体文件,同时还会读取文件的头部信息。参数ps为解复用器上下文对象的地址,如果 *ps 为空avformat_open_input内部就会自动调用avformat_alloc_context;url表示输入文件的路径或者网络地址;fmt表示设置输入格式,为null则表示自动识别(一般都设为null);options表示选项,一般也设为null。

  4. void avformat_close_input(AVFormatContext **s);关闭打开的AVFormatContext。释放它及其所有内容并将 *s 置为null。其函数中已经包含了avformat_free_context操作,所以调用了avformat_close_input之后就就不用再调用avformat_open_input了。

    通过看源码发现,avformat_close_input之所以要传入一个二级指针,主要是为了在函数内部将原来的指针置为null,仅此而已。

  5. int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);读取媒体文件的数据包以及流信息等,用以填充AVFormatContext结构体信息,options选项一般设为null。

    由于需要读取数据包,所以avformat_find_stream_info接口会带来很大的延迟。

  6. int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, const struct AVCodec **decoder_ret, int flags);
    查找指定流的下标,其返回值为对应的流所在format->streams数组下的下标。参数ic表示指定的解复用器上下文,type就表示要查找的流(AVMEDIA_TYPE_AUDIO表示音频流、AVMEDIA_TYPE_VIDEO表示视频流等等),wanted_stream_nb和related_stream一般设为-1表示自动选择,decoder_ret是输出型参数,返回所选流的解码器,可以为null;flags,暂时未定义(“flags; none are currently defined”)。

  7. int av_read_frame(AVFormatContext *s, AVPacket *pkt);读取一帧音视频包,返回流的下一个帧。这个函数会自动读取下一帧数据。返回值为0表示成功,如果为AVERROR_EOF则表示读到末尾结束了。

  • 解复用流程

先用avformat_alloc_context分配一个解复用器上下文AVFormatContext,接着用avformat_open_input打开媒体文件。随后可以用avformat_find_stream_info读取媒体到AVFormatContext中,进而分离流,或者可以直接用av_find_best_stream分离流。

avformat_find_stream_info接口之所以不是必须的,这是因为avformat_open_input接口在调用时不只是打开了媒体文件,并且还会读取文件的头部信息并初始化AVFormatContext。所以即使在不调用avformat_find_stream_info的情况下,AVFormatContext中还是会有媒体文件的元数据的,可以保证正常的分离流操作。

分离流之后,就开始在一个循环中不断调用av_read_frame读取数据包并处理数据包,直到读完媒体文件。注意,虽然函数叫read frame,但读取的其实是packet。在此期间,根据packet->stream_index和av_find_best_stream的返回值匹配,区分处理音频数据和视频数据。

提取流

由于不同容器的封装格式不同,有些容器在分离流之后读取的packet中是裸流数据,即不包含头部信息,只有媒体流数据,音频和视频配置信息通常存储在元数据中。而有些容器在分离流之后读取的packet是包含头部信息的。所以对于不包含头部信息的媒体流数据,在提取流时就要为其加上头部信息再写入,而包含头部信息的就可以直接写入。

例如ts文件分离流之后读取的packet就是包含头部信息的,在提取的时候就可以直接写入,不用做其它处理。而mp4和flv等格式分离流之后读取的packet却是不包含头部信息的,其packet只有裸流数据,在这种情况下,就需要额外先为其写入头部信息,再写入packet(裸流数据)

如下是一些常见的格式:

  • packet为裸流数据的格式:FLV、MP4、MKV、WebM
  • packet为带有头部的媒体流数据的格式:TS、MPEG-2 PS、AVI、WMV / ASF

音频流 - aac

音频流以提取aac流为例,需要在写入packet->data之前,手动绘制一个7字节的adts头部数据,并将其写入。

视频流 - h264

而视频流的则比较麻烦了,以h264格式为例,由于h264在存储时通常是AVCC格式,而播放的话需要转为AnnexB格式。简单来说就是在提取h264流数据时并不能简单的手动写入头部数据,而是需要让FFmepg中的过滤器代为处理,将数据转为标准的Annex B格式的数据。大致需要用到如下内容:

  1. AVBitStreamFilter过滤器的结构体。
  2. AVBSFContext过滤器上下文的结构体,BSF即为BitStreamFilter的简写。
  3. const AVBitStreamFilter *av_bsf_get_by_name(const char *name);根据名字查找指定的过滤器,不同的过滤器对应着不同的功能。
  4. “h264_mp4toannexb”,一个过滤器的名字,其功能是将MP4格式转换成AnnexB格式。
  5. int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);为过滤器分配上下文,即将过滤器与过滤器上下文之间进行绑定。
  6. int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);复制编码器参数,以便过滤器正常运行。
  7. int av_bsf_init(AVBSFContext *ctx);初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)
  8. int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);将pkt发送给ctx对应的那个过滤器,过滤器会将处理好的packet放到对应的缓冲区中。
  9. int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);从对应的缓冲区中取出一个packet。

demo样例

解复用一个mp4文件,提取出mp4媒体文件中的aac和h264两个流的文件。

#include <iostream>
#include <fstream>
#include <string>
using namespace std;// FFmepg-7.0头文件引入
extern "C"
{
#include "libavutil/error.h"
#include "libavformat/avformat.h"
#include "libavcodec/bsf.h"
}// sampling_frequencies,用于获取sampling_frequency_index
const int sampling_frequencies[] = {96000,  // 0x088200,  // 0x164000,  // 0x248000,  // 0x344100,  // 0x432000,  // 0x524000,  // 0x622050,  // 0x716000,  // 0x812000,  // 0x911025,  // 0xa8000    // 0xb// 0xc 0xd 0xe 0xf 是保留的
};
/*** 填充aac-ADTS协议头** @param adts_header_buf  自定义ADTS-header的缓冲区* @param data_length      aac-body的长度(packet->size)* @param profile          AAC规范类型* @param sample_rate      采样率* @param channels         声道数
*/
bool fill_ADTS_header(char* adts_header_buf, const int data_length,const int profile, const int sample_rate, const int channels)
{int sampling_frequency_index = 3; // 默认48000hzint adtsLen = data_length + 7; // data_length + adts_header_lenint frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);for(int i = 0; i < frequencies_size; i++){// 找到了对应的采样率,填充adts-headerif(sampling_frequencies[i] == sample_rate){sampling_frequency_index = i;// syncword:0xfff - 12bitsadts_header_buf[0] = 0xff;         // 高8bitsadts_header_buf[1] = 0xf0;         // 低4bits// ID=0(MPEG-4) - 1bitadts_header_buf[1] |= (0 << 3);// Layer:0 - 2bitsadts_header_buf[1] |= (0 << 1);// protection_absent=1(no CRC) - 1bitadts_header_buf[1] |= 1;// profile:${profile} - 2bitsadts_header_buf[2] = (profile) << 6;// sampling frequency index=${sampling_frequency_index} - 4bitsadts_header_buf[2] |= (sampling_frequency_index & 0x0f)<<2;//private bit:0 - 1bitadts_header_buf[2] |= (0 << 1);//channel configuration:channels  高1bitadts_header_buf[2] |= (channels & 0x04)>>2;//channel configuration:channels 低2bitsadts_header_buf[3] = (channels & 0x03)<<6;//original:0 - 1bitadts_header_buf[3] |= (0 << 5);//home:0 - 1bitadts_header_buf[3] |= (0 << 4);//copyright id bit:0 - 1bitadts_header_buf[3] |= (0 << 3);//copyright id start:0 - 1bitadts_header_buf[3] |= (0 << 2);//frame length:value - 高2bitsadts_header_buf[3] |= ((adtsLen & 0x1800) >> 11);//frame length:value - 中间8bitsadts_header_buf[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);//frame length:value - 低3bitsadts_header_buf[5] = (uint8_t)((adtsLen & 0x7) << 5);//buffer fullness:0x7ff - 高5bitsadts_header_buf[5] |= 0x1f;//buffer fullness:0x7ff - 低6bitsadts_header_buf[6] = 0xfc;  //11111100// number_of_raw_data_blocks_in_frame://   表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。return true;}}// 没找到对应的采样率cerr << "unsupport samplerate: " << sample_rate << endl;return false;
}// usage: process <in_file> <out_audio> <out_video>
int main(int argc, char* argv[])
{// 打开文件ofstream out_audio(argv[2], ios_base::out | ios_base::binary);ofstream out_video(argv[3], ios_base::out | ios_base::binary);// 解复用器上下文AVFormatContext* fmt_ctx = nullptr;// 打开一个输入流并读取其headeravformat_open_input(&fmt_ctx, argv[1], nullptr, nullptr);// 获取媒体流信息(index)int audio_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);int video_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);// 指定过滤器:h264_mp4toannexb过滤器的功能是将MP4格式转换成AnnexBconst AVBitStreamFilter* avbsf = av_bsf_get_by_name("h264_mp4toannexb");// 为过滤器分配上下文AVBSFContext* avbsf_ctx = nullptr;av_bsf_alloc(avbsf, &avbsf_ctx);// 复制编码器参数,以便过滤器正常运行(为过滤器填充音频流的编码器参数)avcodec_parameters_copy(avbsf_ctx->par_in, fmt_ctx->streams[video_index]->codecpar);// 初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)av_bsf_init(avbsf_ctx);// packet allocAVPacket* packet = av_packet_alloc();av_init_packet(packet);// 提取流;while(av_read_frame(fmt_ctx, packet) != AVERROR_EOF){if(packet->stream_index == audio_index) // 音频流(暂定aac格式){// 手动添加aac-adts header// header 信息int profile = fmt_ctx->streams[audio_index]->codecpar->profile;int sample_rate = fmt_ctx->streams[audio_index]->codecpar->sample_rate;int channels = fmt_ctx->streams[audio_index]->codecpar->ch_layout.nb_channels;// 填充adts-headerchar buf[7] = {0}; // adts-header的大小就为7字节fill_ADTS_header(buf, packet->size, profile, sample_rate, channels);// 写入aac-adts_headerout_audio.write(buf, 7);// 写入aac-bodyout_audio.write((char*)packet->data, packet->size);}else if(packet->stream_index == video_index) //视频流(暂定h264格式){// sendav_bsf_send_packet(avbsf_ctx, packet);// receive     // 一个输入数据包可能被过滤器拆分成多个输出数据包,所以这里要用循环while(av_bsf_receive_packet(avbsf_ctx, packet) == 0){out_video.write((char*)packet->data, packet->size); // 写入文件av_packet_unref(packet); // 释放packet,防止内存泄漏}}// 及时清理buf,防止内存泄漏av_packet_unref(packet);}// clear and exitout_video.close();out_audio.close();avformat_close_input(&fmt_ctx);av_bsf_free(&avbsf_ctx);av_packet_free(&packet);return 0;
}

FFmpeg解码实战

所谓解码就是指将压缩的音视频数据恢复为可播放的原始数据格式的过程。

用到的结构体与API

  1. AVCodec解码器的结构体。
  2. AVCodecParserContext解析器上下文的结构体。
  3. const AVCodec *avcodec_find_decoder(enum AVCodecID id);根据指定的id查找匹配的解码器。
  4. AVCodecParserContext *av_parser_init(int codec_id);初始化id对应的AVCodecParserContext。
  5. AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);为AVCodecContext分配内存。
  6. int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);打开解码器(将解码器和解码器上下文进行关联)
  7. int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx, uint8_t **poutbuf, int *poutbuf_size, const uint8_t *buf, int buf_size, int64_t pts, int64_t dts, int64_t pos);
    解析⼀个Packet。从buf中读取一个数据包到poutbuf中,并设置poutbuf_size,返回值为读取的字节数。
  8. int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);将AVPacket压缩数据给解码器,解码器会自动解码之后放到对应的缓冲区中。
  9. int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);获取到解码后的AVFrame数据(从对应的解码器缓冲区中取走一个frame)
  10. int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);获取每个样本sample中的字节数。

解码流程

对于解码操作而言,音频解码和视频解码的操作流大致一样,只不过在最后保存frame数据时要根据不同的格式采取不同的方式。只需要注意,在解码之前需要用解析器对packet进行解析才能进行解码。

在解析时有些内容可能包含头部信息header,所以在解码开始时出现一些send出错的情况这是正常的。

解码流程如下

首先创建环境:先用avcodec_find_decoder查找解码器,接着用 av_parser_init初始化裸流的解析器,用avcodec_alloc_context3分配解码器上下文,用avcodec_open2将解码器和解码器上下文进行关联。

然后循环处理:先用av_parser_parse2解析一个数据包,接着用avcodec_send_packet将packet发送给解码器,然后用avcodec_receive_frame 接收编码后的frame,最后写入解析帧,生成PCM数据。

demo样例

对一个媒体文件中的音频流进行解码,音频流为aac格式。

注意,如果输入文件为mp3格式,解码刚开始的时候会报错这是正常的。这是因为mp3格式是包含头部信息的,而decoder只认识帧数据,无法识别mp3的头部信息。

#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
using namespace std;// FFmepg-7.0头文件引入
extern "C"
{
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
}// 解码器ID
const AVCodecID codec_id = AV_CODEC_ID_AAC;
// 数据包缓冲区大小
const int in_buf_size = 25600;
// 缓冲区阈值
const int threshold_size = 1024;// 解码操作
void decode(AVCodecContext *codec_ctx, AVPacket *packet, AVFrame *frame, ofstream &out_file)
{// 将带有压缩数据的数据包发送到解码器avcodec_send_packet(codec_ctx, packet);// 读取所有输出帧(在文件中,一般可能有任意数量的输出帧)while (avcodec_receive_frame(codec_ctx, frame) == 0){// 获取每个样本的字节数int data_size = av_get_bytes_per_sample(codec_ctx->sample_fmt);// 写入文件for (int i = 0; i < frame->nb_samples; i++){// if(av_sample_fmt_is_planar()) // 判断是否为平面格式// 交错的方式写入for (int j = 0; j < frame->ch_layout.nb_channels; j++){out_file.write((char *)frame->data[j] + data_size * i, data_size);}}}
}// 播放范例: ffplay -ar 48000*2 out.pcm
// Usage: <input file> <output file>
int main(int argc, char *argv[])
{// 解码器const AVCodec *codec = avcodec_find_decoder(codec_id);// 裸流的解析器上下文AVCodecParserContext *parser_ctx = av_parser_init(codec->id);// 解码器上下文AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);// 打开解码器(将解码器和解码器上下文进行关联)avcodec_open2(codec_ctx, codec, nullptr);// 打开io文件ifstream in_file(argv[1], ios_base::in | ios_base::binary);ofstream out_file(argv[2], ios_base::out | ios_base::binary);// 获取输入文件的长度in_file.seekg(0, ios_base::end);int in_file_len = in_file.tellg();in_file.seekg(0);// 从文件中读取一次uint8_t *in_buf = new uint8_t[in_buf_size + AV_INPUT_BUFFER_PADDING_SIZE]{0};in_file.read((char *)in_buf, in_buf_size);uint8_t *data = in_buf;int data_size = in_file.tellg();// 解析+解码AVPacket *packet = av_packet_alloc();AVFrame *frame = av_frame_alloc();while (in_file.tellg() != in_file_len || data_size > 0){// 解析packetint parse_size = av_parser_parse2(parser_ctx, codec_ctx, &packet->data, &packet->size,data, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);// 更新数据信息data += parse_size;data_size -= parse_size;// 进行解码decode(codec_ctx, packet, frame, out_file);// 边界检查if (data_size < threshold_size && in_file.tellg() != in_file_len){memmove(in_buf, data, data_size);int read_count = min(in_buf_size - data_size, in_file_len - (int)in_file.tellg());if (!in_file.read((char *)in_buf + data_size, read_count)){cerr << "file read error! " << "[" << __FILE__ << ":" << __LINE__ << "]" << endl;}data_size += read_count;data = in_buf;}}// over: clear and exitin_file.close();out_file.close();av_parser_close(parser_ctx);avcodec_free_context(&codec_ctx);av_packet_free(&packet);av_frame_free(&frame);return 0;
}

内容补充

  1. int av_strerror(int errnum, char *errbuf, size_t errbuf_size);将错误码转换为错误信息的函数。
  2. EAGAIN是一个预定义的错误码,通常用于指示某种资源暂时不可用。
  3. AVPacket 数据包不仅可以包含视频数据,还可以包含其他元数据。
  4. 虽然FFmpeg是C语言写的,但FFmpeg的使用却体现着面向对象思想。例如AVCodec表示解码器方法,AVCodecContext表示对应的解码器上下文,数据并不会直接保存在AVCodec中,而是保存在AVCodecContext中的。AVCodec就相当于class中的函数/方法,AVCodecContext就相当于class中的成员变量/数据。像这种AVxxx + AVxxxContext就类似于方法+成员变量的用法,体现了FFmpeg的面向对象思想。
  5. FFmpeg的send + receive用法:解复用器、解码器、解析器等,都没有直接提供对应的方法,而是先通过一个send函数,将要处理的数据发送过去,在FFmpeg后台自动处理,待处理好了之后就会放到对应的缓冲取区中,然后就可以通过receive函数从缓冲区中取出处理好的数据。也就是说,这些音视频处理组件并不会直接暴漏给用户,而是间接的使用。receive操作一般都需要放在循环中,这是因为操作之后的经过处理之后可能会生成多个packet/frame。
  6. 解复用器是用于分离流和处理packet的,其直接处理媒体文件,经解复用器处理之后的packet就是媒体的裸流数据了。数据包packet并不能直接交给解码器处理,而是要先经过解析器的解析才能交由解码器处理。而解码器只能读取媒体的裸流数据,在解析时有些内容可能包含头部信息header,所以在解码开始时出现一些send出错的情况这是正常的。

参考资料:

  1. H264—封装格式 - 博客园 (cnblogs.com)
  2. 视频编解码-解码篇
  3. H264/AVC-基本概念和码流结构-CSDN博客
  4. H264码流中的avcC和Annex-B | 老木匠 (xujingkai.cn)
  5. H.264媒体流AnnexB和AVCC格式分析 - CSDN博客
  6. FLV文件格式解析 - boonya - 博客园 (cnblogs.com)

相关文章:

FFmpeg实战 - 解复用与解码

大纲目录 文章目录 前置知识音视频基础概念解复用、解码的流程分析FFMPEG有8个常用库 常见音视频格式的介绍aac格式介绍&#xff08;ADTS&#xff09;h264格式分析FLV和MP4格式介绍 FFmpeg解码解封装实战数据包和数据帧&#xff08;AVPacket/AVFrame&#xff09;AVPacket/AVFra…...

8.5作业

1.思维导图 2.提示并输入一个字符串&#xff0c;统计该字符中大写、小写字母个数、数字个数、空格个数以及其他字符个数&#xff0c;要求使用C风格字符串完成 #include <iostream>using namespace std;int main() {string str;cout << "请输入一个字符串&quo…...

【问题】C++:有哪些类型的智能指针,区别?

智能指针是一种在 C 中管理动态分配内存的工具&#xff0c;可以帮助避免内存泄漏和提高程序的安全性。在 C11 标准引入之后&#xff0c;C 提供了三种主要类型的智能指针&#xff0c;它们分别是 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。这些智能指针有不同的所有权…...

Go-反射

概念 在Go语言中&#xff0c;反射&#xff08;reflection&#xff09;是指在运行时检查程序的结构、变量和接口的机制。可以通过反射获取和修改变量的值、获取变量的类型信息、调用方法等操作。 反射主要由reflect包提供&#xff0c;它定义了两个重要的类型&#xff1a;Type和…...

【深度学习】DeepSpeed,ZeRO 数据并行的三个阶段是什么?

文章目录 ZeRO实验实验设置DeepSpeed ZeRO Stage-2 实验性能比较进一步优化DeepSpeed ZeRO Stage-3 和 CPU 卸载结论ZeRO ZeRO(Zero Redundancy Optimizer)是一种用于分布式训练的大规模深度学习模型的优化技术。它通过分片模型状态(参数、梯度和优化器状态)来消除数据并行…...

代码随想录算法训练营第三十六天 | 1049. 最后一块石头的重量 II、494. 目标和、474.一和零

一、1049. 最后一块石头的重量 II 题目链接&#xff1a;1049. 最后一块石头的重量 II - 力扣&#xff08;LeetCode&#xff09; 文章讲解&#xff1a;代码随想录 (programmercarl.com)——1049. 最后一块石头的重量 II 视频讲解&#xff1a;动态规划之背包问题&#xff0c;这个…...

Pandas行列变换指南:数据重塑的艺术

数据分析中&#xff0c;数据的形态至关重要。pandas库提供了一系列工具&#xff0c;让我们能够轻松地重塑数据。以下是一些常见的pandas行列变换方法&#xff0c;每种方法都配有完整的代码示例。 环境准备 首先&#xff0c;确保你的环境中安装了pandas和numpy库&#xff1a; …...

1.MySQL面试题之innodb如何解决幻读

1. 写在前面 在数据库系统中&#xff0c;幻读&#xff08;Phantom Read&#xff09;是指在一个事务中&#xff0c;两次读取同一范围的数据集时&#xff0c;由于其他事务的插入操作&#xff0c;导致第二次读取结果集发生变化的问题。InnoDB 作为 MySQL 的一个存储引擎&#xff…...

Nginx中$http_host、$host、$proxy_host的区别

知识巩固&#xff01; 网上看到这篇文章&#xff0c;这里转载记录一下。 简介 变量是否显示端口值是否存在 host 浏览器请求的ip&#xff0c;不显示端口 否 "Host:value"显示 值为a:b的时候&#xff0c;只显示a http_host 浏览器请求的ip和端口号 是"Host:v…...

C# Unity 面向对象补全计划 七大原则 之 里氏替换(LSP) 难度:☆☆☆ 总结:子类可以当父类用,牛马是马,骡马也是马

本文仅作学习笔记与交流&#xff0c;不作任何商业用途&#xff0c;作者能力有限&#xff0c;如有不足还请斧正 本系列作为七大原则和设计模式的进阶知识&#xff0c;看不懂没关系 请看专栏&#xff1a;http://t.csdnimg.cn/mIitr&#xff0c;尤其是关于继承的两篇文章&#xff…...

PXE批量安装操作系统

PXE批量安装操作系统 系统环境rhedhat7.9关闭vmware内的dhcp服务 kickstart自动安装脚本的制作 在rhel7系统中提供图形的kickstart制作方式 在rhel8中已经把图形的工具取消&#xff0c;并添加到rhn网络中 在rhel8中如果无法通过rhn网络制作kickstart&#xff0c;可以使用模板…...

float32转float16、snorm/sunorm8/16 学习及实现

1、基础 彻底搞懂float16与float32的计算方式-CSDN博客 例1&#xff1a;float32 0x3fd00000 32b0 011_1111 _1 101_0000_0000_0000_0000_0000 sign0 exp8b0111_1111 h7f d127 >0ffset 127-127 0 mantissa b101_0000_0000_0000_0000_0000(补1&#xff0c;1.1010…...

小型养猫空气净化器怎么选?小型养猫空气净化器产品评测

家养四只猫猫&#xff0c;对于各个角落的猫毛&#xff0c;感觉家里已经被猫毛占领了。感受一下40度高温的养猫人&#xff0c;给掉毛怪疏毛浮毛飘飘&#xff0c;逃不过的饮水机&#xff0c;各个角落&#xff0c;多猫拉臭传来的异味。 一、养猫带来的麻烦 掉毛&#xff1a;每到换…...

数学建模--二分法

目录 二分法的基本原理 应用实例 求解方程根 查找有序数组中的元素 注意事项 Python代码示例 ​编辑 延伸 二分法在数学建模中的具体应用案例有哪些&#xff1f; 如何选择二分法的初始区间以确保收敛速度和精度&#xff1f; 在使用二分法求解方程时&#xff0c;如何…...

如何使用 Puppeteer 绕过 Akamai

摘要&#xff1a; 本文深入探讨了在面对Akamai强大防护下的网页抓取挑战时&#xff0c;如何运用Puppeteer这一强大的Node.js库&#xff0c;通过模拟真实用户行为、动态请求处理等策略&#xff0c;高效且隐蔽地收集数据。我们将一步步揭开Puppeteer绕过Akamai的神秘面纱&#x…...

【硬件知识】车规级开发等级——AEQ-100和ISO26262标准

文章目录 一、定义二、区别1.应用场景2.使用方法 总结 一、定义 AEQ-100&#xff08;Automotive Electronics Council Q100&#xff09;是一个由汽车电子委员会&#xff08;AEC&#xff09;制定的标准&#xff0c;主要用于保证汽车电子元件的可靠性。它是一个关于汽车级半导体…...

Qt | QStackedBarSeries(堆叠条形图)+QPercentBarSeries(堆叠百分比条形图)

点击上方"蓝字"关注我们 01、QBarSet 1. 首先,需要创建一个名为QBarSet的类。 2. 在QBarSet类中,定义所需的属性和方法。 3. 属性可能包括条形的名称、颜色、值等。 4. 方法可能包括添加条形、删除条形、计算总和等。 5. 确保QBarSet类能够与QBar类协同工作,…...

C++——多态经典案例(一)组装电脑

案例&#xff1a;小明打算买两台组装电脑&#xff0c;假设电脑零部件包括CPU、GPU和内存组成。 一台电脑使用intel的CPU、GPU和内存条 一台电脑使用Huawei的CPU、GPU和Intel的内存条 分析&#xff1a;使用多态进行实现 将CPU、GPU和内存条定义为抽象类&#xff0c;内部分别定义…...

从传统监控到智能化升级:EasyCVR视频汇聚平台的一站式解决方案

随着科技的飞速发展和社会的不断进步&#xff0c;视频监控已经成为现代社会治安防控、企业管理等场景安全管理中不可或缺的一部分。而在视频监控领域&#xff0c;EasyCVR视频汇聚平台凭借其强大的多协议接入能力&#xff0c;在复杂多变的网络环境中展现出了卓越的性能和广泛的应…...

Windows下,已知程序PID,取得其窗口句柄HWND

我需要实现这么一个功能&#xff1a;在知道某个程序的PID的情况下&#xff0c;最大化并且置顶显示这个程序的窗口。经过一番资料的查找&#xff0c;并且借助了一些科技的力量&#xff0c;找到了解决办法&#xff1a; struct FindWindowData {DWORD processId;HWND hWnd; };BOO…...

Java获取exe文件详细信息:产品名称,产品版本等

使用Maven项目&#xff0c;在pom.xml文件中注入&#xff1a; <dependency><groupId>com.kichik.pecoff4j</groupId><artifactId>pecoff4j</artifactId><version>0.4.1</version></dependency> 程序代码&#xff1a; import …...

ORB-SLAM2运行环境搭建

操作系统&#xff1a;Ubuntu20.04 1.安装Eigen3 推荐大家安装版本 3.2.10 链接&#xff1a;https://eigen.tuxfamily.org/index.php?titleMain_Page mkdir build cd build cmake .. sudo make install2.安装Pangolin 推荐安装0.5版本 链接&#xff1a;https://github.com…...

Nginx高频核心面试题2

目录 高级问题1. **Nginx中如何实现URL重写&#xff1f;**2. **如何在Nginx中设置基本的HTTP身份验证&#xff1f;**3. **如何限制Nginx中的请求速率&#xff1f;**4. **如何在Nginx中设置自定义错误页面&#xff1f;**5. **Nginx的worker_processes和worker_connections参数有…...

全面提升PDF编辑效率,2024年五大顶级PDF编辑器推荐!

在这个数字化飞速发展的时代&#xff0c;PDF文件已经成为我们日常工作和学习中不可或缺的一部分。然而&#xff0c;面对PDF文件的编辑和管理&#xff0c;许多人仍然感到困惑和无助。今天&#xff0c;就让我们一起探索几款高效、易用的PDF编辑器&#xff0c;它们将彻底改变你的工…...

代码随想录算法训练营第二十天|235. 二叉搜索树的最近公共祖先 701.二叉搜索树中的插入操作 450.删除二叉搜索树中的节点

写在前边的话 235. 二叉搜索树的最近公共祖先 题目链接 力扣题目链接 题目难度 中等 看到题目的第一想法 看到题目的第一想法&#xff0c;除了昨天做过的普通二叉树的最近祖先的解法利用回溯从底向上搜索&#xff0c;我会想到使用迭代法&#xff0c;但我好像不太会使用到二…...

视频美颜SDK与直播美颜插件在实时视频中的应用

视频美颜技术作为提升视频质量的重要手段&#xff0c;已经成为了许多视频和直播应用中不可或缺的一部分。本篇文章&#xff0c;笔者将探讨视频美颜SDK与直播美颜插件在实时视频中的应用&#xff0c;并分析其在用户体验和技术实现方面的重要性。 一、视频美颜SDK的应用场景 视…...

【Linux】yum(工具篇)

文章目录 前言&#xff1a;什么是软件包yum 的介绍yum源yum源的配置第三方源的配置官方源的配置镜像站点安装wget包备份本地yum源配置网易yum源重新生成yum缓存 前言&#xff1a;什么是软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程…...

3GPP入门

官网地址 3GPP – The Mobile Broadband Standard 协议下载链接 Directory Listing /ftp/specs/archive 总纲 重点series Signalling protocols ("stage 3") - user equipment to network24 series信令Radio aspects25 series3G 基础LTE (Evolved UTRA), LTE-Adva…...

FFmpeg内存对齐简述

目录 引文 行字节数的计算 ffmpeg中的align ffmpeg中的linesize 内容参考 引文 在ffmpeg的使用过程中有时会发现align这个参数&#xff0c;那么这个参数代表什么意思&#xff0c;不同的值会产生什么影响呢&#xff0c;详见下文。 行字节数的计算 理解内存对齐之前首先要…...

手机号码归属地查询接口如何对接?(一)

一、什么是手机号码归属地接口&#xff1f; 通过手机号查询归属地信息、是否虚拟运营商等。 二、手机号码归属地接口适用哪些场景&#xff1f; 例如&#xff1a;市场营销领域 &#xff08;1&#xff09;精准营销&#xff1a;企业可以通过手机号归属地查询接口了解客户的大致…...