文章目录
前置知识
音视频基础概念
- 容器:即特定格式的多媒体文件,比如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个常用库
- AVUtil:核心工具库,下面的许多其他模块都会依赖该库做一些基本的音视频处理操作。
- AVFormat:文件格式和协议库,该模块是最重要的模块之一,封装了Protocol层和Demuxer、 Muxer层,使得协议和格式对于开发者来说是透明的。
- AVCodec:编解码库,封装了Codec层,但是有一些Codec是具备自己的License的, FFmpeg是不会默认添加像libx264、 FDK-AAC等库的,但是FFmpeg就像一个平台一样,可以将其他的第三方的Codec以插件的方式添加进来,然后为开发者提供统一的接口。
- AVFilter:音视频滤镜库,该模块提供了包括音频特效和视频特效的处理,在使用FFmpeg的API进行编解码的过程中,直接使用该模块为音视频数据做特效处理是非常方便同时也非常高效的一种方式。
- AVDevice:输入输出设备库,比如,需要编译出播放声音或者视频的工具ffplay,就需要确保该模块是打开的,同时也需要SDL的预先编译,因为该设备模块播放声音与播放视频使用的都是SDL库。
- SwrRessample:该模块可用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换。
- SWScale:该模块是将图像进行格式转换的模块,比如,可以将YUV的数据转换为RGB的数据,缩放尺寸由1280720变为800480。
- PostProc:该模块可用于进行后期处理,当我们使用AVFilter的时候需要打开该模块的开关,因为Filter中会使用到该模块的一些基础函数
常见音视频格式的介绍
aac格式介绍
这里只是对aac格式的简单介绍,内容拓展:AAC-ADTS格式分析【转载】-CSDN博客
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部分:
- adts_fixed_header
- 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[ ]数组中查找得知采样率的值。
h264格式介绍
这里只是对h264格式的简单介绍,内容拓展:H264基础简介【转载】-CSDN博客
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
H.264原始码流(裸流)是由一个接一个NALU组成, 每个NALU之间都使用start code(起始码)分隔,NALU单元通常由[StartCode] [NALU Header] [NALU Payload] 三部分组成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是00 00 00 01 或00 00 01。每个 NALU包括一个头部信息(NAL header)和一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)
NALU(NAL Unit),也就是NAL 单元。每个NALU包含了一个字节大小的NALU头信息(NAL header),以及一个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)。RBSP 指原始字节序列载荷,它是 NAL 单元的数据部分的封装格式。
flv格式介绍
这里只是对flv格式的简单介绍,内容拓展:FLV文件格式分析【转载】-CSDN博客
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文件格式解析
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:
AVPacket *av_packet_alloc();
为AVPacket申请空间,此时并未创建AVBuffer。void av_init_packet(AVPacket *pkt);
初始化pkt中的相关字段,例如将整型数据设为0,将指针为null等操作。int av_new_packet(AVPacket *pkt, int size);
创建数据包,申请一个size字节大小的AVBuffer,并让pkt的AVBufferRef指向它。此时才是真正的创建了AVBuffer。int av_packet_ref(AVPacket *dst, const AVPacket *src);
对给定数据包设置一个新的引用。其作用是将dst的AVBufferRef指向src的AVBuffer,即让dst也关联到src的AVBuffer。此时对应的AVBuffer的引用计数加一。void av_packet_unref(AVPacket *pkt);
擦除一个数据包。取消pkt和它对应AVBuffer的关联,并使其引用计数减一。如果AVBuffer的引用计数减为0了,则FFmpeg会释放掉这块AVBuffer的空间。void av_packet_move_ref(AVPacket *dst, AVPacket *src);
将src中的每个字段移动到dst,并重置(清空)src。此时src与AVBuffer的关联断掉,转移到dst上面,AVBuffer的引用计数不变。AVPacket *av_packet_clone(const AVPacket *src);
AVPacket克隆,相当于av_packet_alloc + av_packet_ref。创建一个和src一样的AVPacket,并作为返回值返回给上层。此时对应的AVBuffer的引用计数加一。void av_packet_free(AVPacket **pkt);
释放AVPacket,要和av_packet_alloc搭配使用,成对出现。
AVFrame:
AVFrame *av_frame_alloc();
为AVFrame 申请空间,作用与av_packet_alloc一样。int av_frame_ref(AVFrame *dst, const AVFrame *src);
对给定数据包设置一个新的引用。作用与av_packet_ref一样。void av_frame_unref(AVFrame *frame);
擦除一个数据包。作用与av_packet_unref一样。void av_frame_move_ref(AVFrame *dst, AVFrame *src);
将src中的每个字段移动到dst,并重置(清空)src。作用与av_packet_move_ref一样。int av_frame_get_buffer(AVFrame*frame,int align);
为媒体数据分配新的缓冲区,根据AVFrame分配内存。AVFrame *av_frame_clone(const AVFrame *src);
作用与av_packet_clone一样。void av_frame_free(AVFrame **frame);
释放AVFrame,要和av_frame_alloc搭配使用,成对出现。
注意事项
- AVPacket/AVFrame和AVBuffer是两回事,AVBuffer是真实的数据缓冲空间,AVPacket/AVFrame并不直接存储媒体数据,而是有能够访问到AVBuffer的引用字段。所以AVPacket/AVFrame和AVBuffer都需要为其分配空间,就好像指针需要4/8字节空间,而它指向的数据也需要分配空间。
- av_init_packet会将字段下的所有指针置为null,所以如果此时的AVPacket字段中还关联的AVBuffer数据而没有释放,在其指针置为null后就会失去关联,此时的AVBuffer就永远无法得到释放了,就会造成内存泄漏。所以av_init_packet函数不能滥用,很容易导致内存泄漏。
解复用
解复用是指将一个复合的音视频文件或流中的不同数据流分离出来。
分离流
- 用到的结构体与API
AVFormatContext *avformat_alloc_context();
申请一个AVFormatContext结构内存,并进行简单的初始化。此时AVFormatContext中还没有数据。其中AVFormatContext
是解复用器上下文结构体。void avformat_free_context(AVFormatContext *s);
释放 AVFormatContext 及其所有流。int avformat_open_input(AVFormatContext **ps, const char *url, const AVInputFormat *fmt, AVDictionary **options);
打开输入的媒体文件,并识别容器读取header。参数ps为解复用器上下文对象的地址;url表示输入文件的路径或者网络地址;fmt表示设置输入格式,为null则表示自动识别(一般都设为null);options表示选项,一般也设为null。void avformat_close_input(AVFormatContext **s);
关闭打开的AVFormatContext。释放它及其所有内容并将 *s 置为null。其函数中已经包含了avformat_free_context操作,所以调用了avformat_close_input之后就就不用再调用avformat_open_input了。int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
读取媒体文件的数据包以及流信息等,用以填充AVFormatContext
结构体信息,options选项一般设为null。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”)。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流数据时并不能简单的手动写入头部数据,而是需要让FFmepg中的过滤器代为处理,将数据转为标准的Annex B格式的数据。大致需要用到如下内容:
AVBitStreamFilter
过滤器的结构体。AVBSFContext
过滤器上下文的结构体,BSF即为BitStreamFilter的简写。const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
根据名字查找指定的过滤器,不同的过滤器对应着不同的功能。- “h264_mp4toannexb”,一个过滤器的名字,其功能是将MP4格式转换成AnnexB格式。
int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);
为过滤器分配上下文,即将过滤器与过滤器上下文之间进行绑定。int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
复制编码器参数,以便过滤器正常运行。int av_bsf_init(AVBSFContext *ctx);
初始化过滤器上下文(在设置了所有参数和选项之后,准备好过滤器以便使用)int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
将pkt发送给ctx对应的那个过滤器,过滤器会将处理好的packet放到对应的缓冲区中。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, // 0x0
88200, // 0x1
64000, // 0x2
48000, // 0x3
44100, // 0x4
32000, // 0x5
24000, // 0x6
22050, // 0x7
16000, // 0x8
12000, // 0x9
11025, // 0xa
8000 // 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; // 默认48000hz
int adtsLen = data_length + 7; // data_length + adts_header_len
int frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);
for(int i = 0; i < frequencies_size; i++)
{
// 找到了对应的采样率,填充adts-header
if(sampling_frequencies[i] == sample_rate)
{
sampling_frequency_index = i;
// syncword:0xfff - 12bits
adts_header_buf[0] = 0xff; // 高8bits
adts_header_buf[1] = 0xf0; // 低4bits
// ID=0(MPEG-4) - 1bit
adts_header_buf[1] |= (0 << 3);
// Layer:0 - 2bits
adts_header_buf[1] |= (0 << 1);
// protection_absent=1(no CRC) - 1bit
adts_header_buf[1] |= 1;
// profile:${profile} - 2bits
adts_header_buf[2] = (profile) << 6;
// sampling frequency index=${sampling_frequency_index} - 4bits
adts_header_buf[2] |= (sampling_frequency_index & 0x0f)<<2;
//private bit:0 - 1bit
adts_header_buf[2] |= (0 << 1);
//channel configuration:channels 高1bit
adts_header_buf[2] |= (channels & 0x04)>>2;
//channel configuration:channels 低2bits
adts_header_buf[3] = (channels & 0x03)<<6;
//original:0 - 1bit
adts_header_buf[3] |= (0 << 5);
//home:0 - 1bit
adts_header_buf[3] |= (0 << 4);
//copyright id bit:0 - 1bit
adts_header_buf[3] |= (0 << 3);
//copyright id start:0 - 1bit
adts_header_buf[3] |= (0 << 2);
//frame length:value - 高2bits
adts_header_buf[3] |= ((adtsLen & 0x1800) >> 11);
//frame length:value - 中间8bits
adts_header_buf[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);
//frame length:value - 低3bits
adts_header_buf[5] = (uint8_t)((adtsLen & 0x7) << 5);
//buffer fullness:0x7ff - 高5bits
adts_header_buf[5] |= 0x1f;
//buffer fullness:0x7ff - 低6bits
adts_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;
// 打开一个输入流并读取其header
avformat_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格式转换成AnnexB
const 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 alloc
AVPacket* 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-header
char buf[7] = {0}; // adts-header的大小就为7字节
fill_ADTS_header(buf, packet->size, profile, sample_rate, channels);
// 写入aac-adts_header
out_audio.write(buf, 7);
// 写入aac-body
out_audio.write((char*)packet->data, packet->size);
}
else if(packet->stream_index == video_index) //视频流(暂定h264格式)
{
// send
av_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 exit
out_video.close();
out_audio.close();
avformat_close_input(&fmt_ctx);
av_bsf_free(&avbsf_ctx);
av_packet_free(&packet);
return 0;
}
解码
所谓解码就是指将压缩的音视频数据恢复为可播放的原始数据格式的过程。
用到的结构体与API
AVCodec
解码器的结构体。AVCodecParserContext
解析器上下文的结构体。const AVCodec *avcodec_find_decoder(enum AVCodecID id);
根据指定的id查找匹配的解码器。AVCodecParserContext *av_parser_init(int codec_id);
初始化id对应的AVCodecParserContext。AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
为AVCodecContext分配内存。int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
打开解码器(将解码器和解码器上下文进行关联)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,返回值为读取的字节数。int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
将AVPacket压缩数据给解码器,解码器会自动解码之后放到对应的缓冲区中。int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
获取到解码后的AVFrame数据(从对应的解码器缓冲区中取走一个frame)int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
获取每个样本sample中的字节数。
解码流程
对于解码操作而言,音频解码和视频解码的操作流大致一样,只不过在最后保存frame数据时要根据不同的格式采取不同的方式。
解码流程如下
首先创建环境:先用
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格式
#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)
{
// 解析packet
int 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 exit
in_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;
}