目录
- 1 基本知识
- 1.1 解封装
- 1.2 AAC和ADTS
- 说明
- 1.3 H264
- 1.3.1 H264编码结构解析
- 1.3.2 NALU
- 1.3.2 分类
- 2 实验1 探究音视频信息
- 2.1 重要结构体介绍
- 2.2 相关的API
- 3 实验二 提取AAC数据
- 4 实验三 提取h264
1 基本知识
1.1 解封装
封装的逆向操作:封装是把音频流、视频流、字幕流等不同成分按一定规则组合成视频文件(如 MP4、FLV ),复用器负责此过程。解封装则相反,是用解复用器(针对 MP4、FLV 等格式有对应解复用器)将视频文件再按规则拆分回音频流、视频流、字幕流等各个成分 。
流索引标识:解封装后,为区分各流,会给音频流、视频流等分配索引,如图中 audio_index = 1 表示音频流索引为 1 ,video_index = 0 表示视频流索引为 0 。程序后续可依这些索引分别处理对应流,比如解码视频流、播放音频流等 。
1.2 AAC和ADTS
AAC⾳频格式:Advanced Audio Coding(⾼级⾳频解码),是⼀种由MPEG-4
标准定义的有损⾳频压缩格式,由Fraunhofer发展,Dolby, Sony和AT&T是主要的贡献者。
分为两种封装格式
- ADIF:Audio Data Interchange Format ⾳频数据交换格式。这种格式的特
征是可以确定的找到这个⾳频数据的开始,不需进⾏在⾳频数据流中间开始
的解码,即它的解码必须在明确定义的开始处进⾏。故这种格式常⽤在磁盘⽂件中。 - ADTS的全称是Audio Data Transport Stream。是AAC⾳频的传输流格
式。AAC⾳频格式在MPEG-2(ISO-13318-7 2003)中有定义。AAC后来
⼜被采⽤到MPEG-4标准中。这种格式的特征是它是⼀个有同步字的⽐特
流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式
ADTS
有的时候当你编码AAC裸流的时候,会遇到写出来的AAC⽂件并不能在PC和⼿
机上播放,很⼤的可能就是AAC⽂件的每⼀帧⾥缺少了ADTS头信息⽂件的包装
拼接。
只需要加⼊头⽂件ADTS即可。⼀个AAC原始数据块⻓度是可变的,对原始帧加上ADTS头进⾏ADTS的封装,就形成了ADTS帧
ADTS帧
每⼀帧的ADTS的头⽂件都包含了⾳频的采样率,声道,帧⻓度等信息,这样解
码器才能解析读取。
⼀般情况下ADTS的头信息都是7个字节,分为2部分。
- adts_fixed_header();
- adts_variable_header();
其⼀为固定头信息,紧接着是可变头信息。固定头信息中的数据每⼀帧都相
同,⽽可变头信息则在帧与帧之间可变。
typedef struct {
uint16_t syncword; // 同步字
uint8_t id; // MPEG标识符
uint8_t layer; // 层(固定为00)
uint8_t protection_absent; // 是否有CRC校验
uint8_t profile; // AAC Profile
uint8_t sampling_frequency_index; // 采样率索引
uint8_t private_bit; // 私有位
uint8_t channel_configuration; // 声道配置
uint8_t original_copy; // 原始/拷贝标志
uint8_t home; // 主标志
uint8_t copyright_identification_bit; // 版权标志位
uint8_t copyright_identification_start; // 版权标志起始位
uint16_t frame_length; // 帧长度
uint8_t adts_buffer_fullness; // 缓存填充度
uint8_t number_of_raw_data_blocks_in_frame; // 原始数据块数量
} ADTSHeader;
好的,以下是ADTS固定头参数的详细表格,包括字段名称、位长度、取值范围、含义和作用:
字段名称 | 位长度 | 取值范围 | 含义 | 作用 |
---|---|---|---|---|
syncword | 12 | 0xFFF | 同步字 | 标识一个ADTS帧的开始,用于帧同步。 |
ID | 1 | 0 或 1 | MPEG标识符 | 区分MPEG版本: 0 = MPEG-4 1 = MPEG-2 |
Layer | 2 | 00 | 层 | 固定为00,表示AAC编码。 |
protection_absent | 1 | 0 或 1 | 误码校验标志 | 表示是否包含CRC校验: 0 = 有CRC校验 1 = 无CRC校验 |
profile | 2 | 00-11 | AAC Profile(编码复杂度级别) | 表示编码复杂度: 00 = Main 01 = Low Complexity (LC) 10 = SSR |
sampling_frequency_index | 4 | 0000-1111 | 采样率索引 | 通过索引查找采样率,例如: 0000 = 96 kHz 0100 = 44.1 kHz |
private_bit | 1 | 0 或 1 | 私有位 | 用于私有用途,通常无具体含义。 |
channel_configuration | 3 | 0-7 | 声道配置 | 指定声道数和配置: 1 = 单声道 2 = 立体声 6 = 5.1声道 |
original_copy | 1 | 0 或 1 | 原始/拷贝标志 | 标识帧是否为原始数据: 0 = 拷贝 1 = 原始 |
home | 1 | 0 或 1 | 主标志 | 用于标识主音频流。 |
copyright_identification_bit | 1 | 0 或 1 | 版权标志位 | 标识是否包含版权信息。 |
copyright_identification_start | 1 | 0 或 1 | 版权标志起始位 | 标识版权信息的起始位置。 |
说明
- syncword 是固定值
0xFFF
,用于快速定位帧的起始位置。 - Layer 在AAC中始终为
00
,因为AAC不使用MPEG音频的分层机制。 - protection_absent 决定了帧头的长度(7字节或9字节)。
- profile 和 sampling_frequency_index 是关键字段,分别定义了编码复杂度和音频采样率。
- channel_configuration 描述了音频的声道布局,例如立体声或5.1声道。
- private_bit、original_copy、home、copyright_identification_bit 和 copyright_identification_start 主要用于扩展功能和版权保护。
1.3 H264
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC
H264编码原理
在⾳视频传输过程中,视频⽂件的传输是⼀个极⼤的问题;⼀段分辨率为19201080,每个像素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是:19201080325/1024/1024=148.315MB/s,换成bps则意味着视频每秒带宽为1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。
对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了16*16的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码
H264中的I帧、P帧和B帧
以下是提取的表格内容:
帧的分类 | 中文 | 意义 |
---|---|---|
I帧 | 帧内编码帧 | I帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图像。I帧可以看成是一个图像经过压缩后的产物。自身可以通过视频解压算法解压成一张单独的完整的图片。 |
P帧 | 前向预测编码帧 | 通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧。需要参考其前面的一个I frame 或者P frame来生成一张完整的图片。 |
B帧 | 双向预测帧 | 既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像, 也叫双向预测帧。则要参考其前一个I或者P帧及其后面的一个P帧来生成一张完整的图片。 |
1.3.1 H264编码结构解析
IDR(Instantaneous Decoding Refresh,即时解码刷新)⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像。I和IDR帧都使⽤帧内预测。I帧不⽤参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样
原始图像 IDR1 B2 B3 P4 B5 B6 P7 B8 B9 I10
解码顺序:IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15
B8可以跨过I10去参考P7
IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 这⾥的B9就只能参照IDR8和P1
其核⼼作⽤是,是为了解码的重同步,当解码器解码到 IDR 图像时,⽴即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始⼀个新的序列。这样,如果前⼀个序列出现重⼤错误,在这⾥可以获得重新同步的机会。IDR图像之后的图像永远不会使⽤IDR之前的图像的数据来解码。
**GOP (图像组)**主要⽤作形容⼀个IDR帧 到下⼀个IDR帧之间的间隔了多少个帧
H264除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚策略;类似于⽹络数据封装成IP帧,在H264中将其称为组(GOP, group of pictures)、⽚(slice)、宏块(Macroblock)这些⼀起组成了H264的码流分层结构;H264将其组织成为序列(GOP)、图⽚(pictrue)、⽚(Slice)、宏块(Macroblock)、⼦块(subblock)五个层次
1.3.2 NALU
SPS(序列参数集)
- 作用:SPS包含了整个视频序列的全局参数,这些参数对整个视频序列都是有效的。
- 内容:
视频的分辨率(宽度和高度)。
帧率信息。
编码使用的Profile和Level。
其他全局参数,例如色域、比特深度等。
重要性:解码器需要这些信息来初始化解码环境,并正确解析后续的视频数据。
PPS(图像参数集)
- 作用:PPS包含了特定图像(或几幅图像)的参数,这些参数对图像层面的操作是必需的。
- 内容:
宏块的编码参数。
量化参数。
其他图像特定的设置。
重要性:解码器需要这些信息来正确解码每个图像(帧)。
因此,在发送I帧之前,必须先发送SPS和PPS,以确保解码器能够正确处理后续的视频数据。
NALU 是 GOP 的组成部分:一个 GOP 中的图像帧(I 帧、P 帧、B 帧等 )会被进一步封装成一个个 NALU 进行存储和传输 。比如一个 I 帧可能会被分割成多个 NALU,每个 NALU 包含 I 帧的一部分编码数据 ;P 帧和 B 帧也会以类似方式处理 。
编码与传输层面的关联:GOP 侧重于视频编码结构设计,决定帧间预测关系和编码顺序等 ,影响视频压缩效率和随机访问性能 。NALU 则更关注在网络环境下如何有效传输编码数据,通过 NALU 头信息标识数据类型和重要性等,便于网络适配和差错控制 。实际应用中,视频编码先按 GOP 结构组织帧,再将每个帧的编码数据封装成 NALU 在网络中传输 。接收端先按 NALU 接收数据,再依据 GOP 结构和 NALU 信息进行解码恢复视频 。
NALU结构
H.264原始码流(裸流)是由⼀个接⼀个NALU组成,它的功能分为两层,VCL(视频编码层)和NAL(⽹络提取层):
- VCL:包括核⼼压缩引擎和块,宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹络进⾏⾼效的编码;
- NAL:负责将VCL产⽣的⽐特字符串适配到各种各样的⽹络和多元环境中,覆盖了所有⽚级以上的语法级别
NALU结构单元
- Start Code(起始码)
作用:用于标识一个NALU单元的开始。
格式:通常是00 00 00 01或00 00 01。
说明:在H.264码流中,起始码帮助解码器识别NALU的边界。00 00 00 01通常用于完整帧被编码为多个slice(片)的场合,而00 00 01用于其他场合。 - NALU Header(NALU头)
长度:1字节。
组成:- forbidden_zero_bit:1位,必须为0,用于错误检测。
- nal_ref_idc:2位,表示NALU的重要性,取值范围为00到11。值越大,表示当前NALU越重要,需要优先保护。
- nal_unit_type:5位,表示NALU的类型,如I帧、P帧、B帧、SPS、PPS等。
- NALU Payload(NALU负载)
内容:实际的编码数据,也称为RBSP(Raw Byte Sequence Payload,原始字节序列负载)。
说明:负载部分包含具体的视频数据,如编码后的图像数据、参数集等。
1.3.2 分类
H264有两种封装
⼀种是annexb模式,传统模式,有startcode,SPS和PPS是在ES中
⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息
被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度
很多解码器只⽀持annexb这种模式,因此需要将mp4做转换:在ffmpeg中⽤
h264_mp4toannexb_filter可以做转换
const AVBitStreamFilter *bsfilter =av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
//2 初始化过滤器上下⽂
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);
2 实验1 探究音视频信息
2.1 重要结构体介绍
以下是将上述代码中涉及的重要结构体的常用成员整理成的表格:
AVFormatContext
结构体
作用:描述一个媒体文件或媒体流的构成和基本信息,如文件格式、流的数量和类型、时长、比特率等。它是 FFmpeg 处理媒体文件的核心结构体,许多操作都围绕它展开。
成员名称 | 类型 | 描述 |
---|---|---|
nb_streams | unsigned int | 媒体文件中流的数量,例如视频、音频、字幕流等的总数 |
bit_rate | int64_t | 媒体文件的平均比特率,单位为比特每秒(bps),反映数据传输的速率 |
duration | int64_t | 媒体文件的总时长,单位为微秒(us) |
streams | AVStream ** | 指向 AVStream 结构体数组的指针,每个 AVStream 代表一个流,通过该指针可访问各流的信息 |
url | char * | 媒体文件的路径或网络地址,调用 avformat_open_input 时读取到的文件标识 |
AVStream
结构体
作用:存储媒体文件中单个流(音频流、视频流、字幕流等 )的相关信息
成员名称 | 类型 | 描述 |
---|---|---|
index | int | 该流在媒体文件中的索引,用于唯一标识每个流 |
time_base | AVRational | 流的时间基,是一个表示分数的结构体,包含分子和分母,用于将时间戳转换为实际时间 |
duration | int64_t | 流的总时长,以 time_base 为单位,若值为 AV_NOPTS_VALUE 表示时长未知 |
codecpar | AVCodecParameters * | 指向 AVCodecParameters 结构体的指针,存储该流的编解码器参数信息 |
avg_frame_rate | AVRational | 视频流的平均帧率,以每秒帧数(fps)为单位,反映视频的播放速度 |
AVCodecParameters
结构体
作用:存储编解码器的参数信息,如编解码器 ID、媒体类型、数据格式(像素格式或采样格式 )、视频的宽高(视频流 )、音频的采样率和声道数(音频流 )等。
成员名称 | 类型 | 描述 |
---|---|---|
codec_id | enum AVCodecID | 编解码器的 ID,用于标识具体的编解码器,如 AV_CODEC_ID_H264 代表 H.264 视频编解码器 |
codec_type | enum AVMediaType | 媒体类型,表明该流是视频、音频还是字幕等,如 AVMEDIA_TYPE_VIDEO 代表视频流 |
format | int | 数据格式,对于视频流是像素格式(如 AV_PIX_FMT_YUV420P ),对于音频流是采样格式(如 AV_SAMPLE_FMT_FLTP ) |
width | int | 视频帧的宽度,单位为像素,仅适用于视频流 |
height | int | 视频帧的高度,单位为像素,仅适用于视频流 |
sample_rate | int | 音频采样率,单位为 Hz,表示每秒采集的样本数,仅适用于音频流 |
channels | int | 音频声道数,如单声道为 1,立体声为 2,仅适用于音频流 |
AVPacket
结构体
AVPacket
作用:存储从媒体文件中读取的一帧压缩数据(音频帧或视频帧 ),包含时间戳、数据大小、流索引等信息。
成员名称 | 类型 | 描述 |
---|---|---|
pts | int64_t | 显示时间戳(Presentation Timestamp),用于标识该帧在播放时的时间点 |
dts | int64_t | 解码时间戳(Decoding Timestamp),用于标识该帧在解码时的时间点 |
size | int | 数据包中数据的大小,单位为字节 |
stream_index | int | 该数据包所属流的索引,用于确定是音频流、视频流还是其他流的数据 |
duration | int64_t | 该帧的时长,以 AVStream 的 time_base 为单位,反映帧的持续时间 |
2.2 相关的API
avformat_network_init
void avformat_network_init(void);
初始化网络组件,让 FFmpeg 支持网络流的读取,像 RTSP、HTTP 这类网络协议的流int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);
ps:指向 AVFormatContext 指针的指针。函数会分配并初始化 AVFormatContext,并将其地址存储在 *ps 中。
url:输入媒体文件的路径或者网络地址。
fmt:指定输入格式,通常设为 NULL,让 FFmpeg 自动探测。
options:传递给解复用器的选项,是一个 AVDictionary 指针,通常设为 NULL。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
avformat_find_stream_info函数通过读取媒体文件的头部信息以及可能的一些额外数据,来填充AVFormatContext结构体中的各个字段,从而获取到流的信息,如流的数量、每个流的编码参数(包括视频的分辨率、帧率,音频的采样率、声道数等)、时长、比特率等。这些信息对于后续正确地解码和处理音视频数据非常重要
ic:指向 AVFormatContext 的指针,代表已打开的输入媒体文件。
options:传递给解复用器的选项,是一个 AVDictionary 指针,通常设为 NULL。
void av_dump_format(AVFormatContext *ic, int index, const char *url, int is_output);
功能:打印媒体文件的格式信息,涵盖流的数量、每个流的详细参数等。
参数:
ic:指向 AVFormatContext 的指针,代表已打开的输入媒体文件。
index:要打印信息的流的索引,通常设为 0 表示打印所有流的信息。
url:输入媒体文件的路径或者网络地址,用于显示信息。
is_output:指示是输入文件(0)还是输出文件(1)。
int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);
功能:在媒体文件的流中查找指定类型(如音频、视频)的最佳流。
参数:
ic:指向 AVFormatContext 的指针,代表已打开的输入媒体文件。
type:要查找的流的类型,如 AVMEDIA_TYPE_AUDIO 或 AVMEDIA_TYPE_VIDEO。
wanted_stream_nb:指定要查找的流的编号,若为 -1 则查找任意流。
related_stream:相关流的编号,通常设为 -1。
decoder_ret:若不为 NULL,则返回找到的流的解码器。
flags:查找标志,通常设为 0。
- int av_read_frame(AVFormatContext *s, AVPacket *pkt);
AVFormatContext 结构体 ifmt_ctx 表示)中读取一帧压缩数据(音频帧或视频帧)的函数。每次调用该函数时,它会从文件或流中解析并提取下一帧数据,并将其存储在 AVPacket 结构体 pkt 中
参数:
s:指向 AVFormatContext 的指针,代表已打开的输入媒体文件。
pkt:指向 AVPacket 的指针,用于存储读取到的帧数据。
返回:成功返回 0,文件结束返回 AVERROR_EOF,失败返回负错误码。
av_packet_alloc AVPacket *av_packet_alloc(void);
功能:分配一个 AVPacket 结构体,用于存储读取到的媒体帧数据。
参数:无。
返回值:成功返回分配的 AVPacket 指针,失败返回 NULL。
void av_packet_unref(AVPacket *pkt);
功能:减少 AVPacket 的引用计数,当引用计数为 0 时释放相关资源。
参数:
pkt:指向 AVPacket 的指针,要释放引用的数据包。
返回值:无。
void av_packet_free(AVPacket **pkt);
功能:释放 AVPacket 结构体及其关联的资源。
参数:
pkt:指向 AVPacket 指针的指针,要释放的数据包。
返回值:无
3 实验二 提取AAC数据
int adts_header(char * const p_adts_header, const int data_length,
const int profile, const int samplerate,
const int channels)
{
int sampling_frequency_index = 3; // 默认使用48000hz
int adtsLen = data_length + 7;
int frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);
int i = 0;
for(i = 0; i < frequencies_size; i++)
{
if(sampling_frequencies[i] == samplerate)
{
sampling_frequency_index = i;
break;
}
}
if(i >= frequencies_size)
{
printf("unsupport samplerate:%d\n", samplerate);
return -1;
}
p_adts_header[0] = 0xff; //syncword:0xfff 高8bits
p_adts_header[1] = 0xf0; //syncword:0xfff 低4bits
p_adts_header[1] |= (0 << 3); //MPEG Version:0 for MPEG-4,1 for MPEG-2 1bit
p_adts_header[1] |= (0 << 1); //Layer:0 2bits
p_adts_header[1] |= 1; //protection absent:1 1bit
p_adts_header[2] = (profile)<<6; //profile:profile 2bits
p_adts_header[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index 4bits
p_adts_header[2] |= (0 << 1); //private bit:0 1bit
p_adts_header[2] |= (channels & 0x04)>>2; //channel configuration:channels 高1bit
p_adts_header[3] = (channels & 0x03)<<6; //channel configuration:channels 低2bits
p_adts_header[3] |= (0 << 5); //original:0 1bit
p_adts_header[3] |= (0 << 4); //home:0 1bit
p_adts_header[3] |= (0 << 3); //copyright id bit:0 1bit
p_adts_header[3] |= (0 << 2); //copyright id start:0 1bit
p_adts_header[3] |= ((adtsLen & 0x1800) >> 11); //frame length:value 高2bits
p_adts_header[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value 中间8bits
p_adts_header[5] = (uint8_t)((adtsLen & 0x7) << 5); //frame length:value 低3bits
p_adts_header[5] |= 0x1f; //buffer fullness:0x7ff 高5bits
p_adts_header[6] = 0xfc; //11111100 //buffer fullness:0x7ff 低6bits
// number_of_raw_data_blocks_in_frame:
// 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。
return 0;
}
补充:
|:或运算 有 1 这个位就是1
&:与运行 全为 1 才能是1
<<:左移运算符:移运算符规则:左移运算符 << 用于将一个二进制数向左移动指定的位数。其基本规则是,将操作数的二进制表示向左移动指定的位数,右边空出的位用 0 填充,而左边超出的位则被丢弃。
(0 << 3) 的计算:
数字 0 的二进制表示(假设为 8 位)是 0000 0000。
对其进行左移 3 位操作,即将所有位向左移动 3 位,右边空出的 3 位用 0 填充,得到的结果仍然是 0000 0000,也就是十进制的 0。
最终:p_adts_header[1] =0xf1;
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr:指向用于存储读取数据的内存块的指针。
size:每个数据项的大小(以字节为单位)。
nmemb:要读取的数据项的数量。
stream:指向 FILE 对象的指针,表示要读取的文件流。
FILE *fopen(const char *filename, const char *mode);
- filename:这是一个字符串,代表要打开的文件的名称。如果文件在当前工作目录之外,需要提供完整的文件路径。
- mode:同样是字符串,用于指定文件的打开模式。常见的打开模式如下:
“r”:以只读模式打开文本文件。文件必须存在,否则打开失败。
“w”:以写入模式打开文本文件。若文件存在,其内容会被清空;若文件不存在,则创建新文件。
“a”:以追加模式打开文本文件。若文件存在,写入的数据会添加到文件末尾;若文件不存在,则创建新文件。
“r+”:以读写模式打开文本文件。文件必须存在。
“w+”:以读写模式打开文本文件。若文件存在,内容会被清空;若不存在,则创建新文件。
“a+”:以读写模式打开文本文件。若文件存在,写入的数据会添加到文件末尾;若不存在,则创建新文件。
若要以二进制模式操作文件,在上述模式后面加上字母 b,例如 “rb”、“wb” 等。
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#define ADTS_HEADER_LEN 7;
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 d e f是保留的
};
int adts_header(char * const p_adts_header, const int data_length,
const int profile, const int samplerate,
const int channels)
{
int sampling_frequency_index = 3; // 默认使用48000hz
int adtsLen = data_length + 7;
int frequencies_size = sizeof(sampling_frequencies) / sizeof(sampling_frequencies[0]);//计算大小
int i = 0;
for(i = 0; i < frequencies_size; i++)
{
if(sampling_frequencies[i] == samplerate)
{
sampling_frequency_index = i;
break;
}
}//音频采样
if(i >= frequencies_size)
{
printf("unsupport samplerate:%d\n", samplerate);
return -1;
}
p_adts_header[0] = 0xff; //syncword:0xfff 高8bits
p_adts_header[1] = 0xf0; //syncword:0xfff 低4bits
p_adts_header[1] |= (0 << 3); //MPEG Version:0 for MPEG-4,1 for MPEG-2 1bit
p_adts_header[1] |= (0 << 1); //Layer:0 2bits
p_adts_header[1] |= 1; //protection absent:1 1bit
p_adts_header[2] = (profile)<<6; //profile:profile 2bits
p_adts_header[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index 4bits
p_adts_header[2] |= (0 << 1); //private bit:0 1bit
p_adts_header[2] |= (channels & 0x04)>>2; //channel configuration:channels 高1bit
p_adts_header[3] = (channels & 0x03)<<6; //channel configuration:channels 低2bits
p_adts_header[3] |= (0 << 5); //original:0 1bit
p_adts_header[3] |= (0 << 4); //home:0 1bit
p_adts_header[3] |= (0 << 3); //copyright id bit:0 1bit
p_adts_header[3] |= (0 << 2); //copyright id start:0 1bit
p_adts_header[3] |= ((adtsLen & 0x1800) >> 11); //frame length:value 高2bits
p_adts_header[4] = (uint8_t)((adtsLen & 0x7f8) >> 3); //frame length:value 中间8bits
p_adts_header[5] = (uint8_t)((adtsLen & 0x7) << 5); //frame length:value 低3bits
p_adts_header[5] |= 0x1f; //buffer fullness:0x7ff 高5bits
p_adts_header[6] = 0xfc; //11111100 //buffer fullness:0x7ff 低6bits
// number_of_raw_data_blocks_in_frame:
// 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。
return 0;
}
int main(int argc, char *argv[])
{
int ret = -1;
char errors[1024];
char *in_filename = NULL;
char *aac_filename = NULL;
FILE *aac_fd = NULL;
int audio_index = -1;
int len = 0;
AVFormatContext *ifmt_ctx = NULL;
AVPacket pkt;
// 设置打印级别
av_log_set_level(AV_LOG_DEBUG);
if(argc < 3)
{
av_log(NULL, AV_LOG_DEBUG, "the count of parameters should be more than three!\n");
return -1;
}
in_filename = argv[1]; // 输入文件
aac_filename = argv[2]; // 输出文件
if(in_filename == NULL || aac_filename == NULL)
{
av_log(NULL, AV_LOG_DEBUG, "src or dts file is null, plz check them!\n");
return -1;
}
aac_fd = fopen(aac_filename, "wb");
if (!aac_fd)
{
av_log(NULL, AV_LOG_DEBUG, "Could not open destination file %s\n", aac_filename);
return -1;
}
// 打开输入文件
if((ret = avformat_open_input(&ifmt_ctx, in_filename, NULL, NULL)) < 0)
{
av_strerror(ret, errors, 1024);
av_log(NULL, AV_LOG_DEBUG, "Could not open source file: %s, %d(%s)\n",
in_filename,
ret,
errors);
return -1;
}
// 获取解码器信息
if((ret = avformat_find_stream_info(ifmt_ctx, NULL)) < 0)
{
av_strerror(ret, errors, 1024);
av_log(NULL, AV_LOG_DEBUG, "failed to find stream information: %s, %d(%s)\n",
in_filename,
ret,
errors);
return -1;
}
// dump媒体信息
av_dump_format(ifmt_ctx, 0, in_filename, 0);
// 初始化packet
av_init_packet(&pkt);
// 查找audio对应的steam index
audio_index = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if(audio_index < 0)
{
av_log(NULL, AV_LOG_DEBUG, "Could not find %s stream in input file %s\n",
av_get_media_type_string(AVMEDIA_TYPE_AUDIO),
in_filename);
return AVERROR(EINVAL);
}
// 打印AAC级别
printf("audio profile:%d, FF_PROFILE_AAC_LOW:%d\n",
ifmt_ctx->streams[audio_index]->codecpar->profile,
FF_PROFILE_AAC_LOW);
if(ifmt_ctx->streams[audio_index]->codecpar->codec_id != AV_CODEC_ID_AAC)
{
printf("the media file no contain AAC stream, it's codec_id is %d\n",
ifmt_ctx->streams[audio_index]->codecpar->codec_id);
goto failed;
}
// 读取媒体文件,并把aac数据帧写入到本地文件
while(av_read_frame(ifmt_ctx, &pkt) >=0 )
{
if(pkt.stream_index == audio_index)
{
char adts_header_buf[7] = {0};
adts_header(adts_header_buf, pkt.size,
ifmt_ctx->streams[audio_index]->codecpar->profile,
ifmt_ctx->streams[audio_index]->codecpar->sample_rate,
ifmt_ctx->streams[audio_index]->codecpar->channels);
fwrite(adts_header_buf, 1, 7, aac_fd); // 写adts header , ts流不适用,ts流分离出来的packet带了adts header
len = fwrite( pkt.data, 1, pkt.size, aac_fd); // 写adts data
if(len != pkt.size)
{
av_log(NULL, AV_LOG_DEBUG, "warning, length of writed data isn't equal pkt.size(%d, %d)\n",
len,
pkt.size);
}
}
av_packet_unref(&pkt);
}
failed:
// 关闭输入文件
if(ifmt_ctx)
{
avformat_close_input(&ifmt_ctx);
}
if(aac_fd)
{
fclose(aac_fd);
}
return 0;
}
4 实验三 提取h264
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
av_strerror(errnum, err_buf, 128);
return err_buf;
}
int main(int argc, char **argv)
{
AVFormatContext *ifmt_ctx = NULL;
int videoindex = -1;
AVPacket *pkt = NULL;
int ret = -1;
int file_end = 0; // 文件是否读取结束
if(argc < 3)
{
printf("usage inputfile outfile\n");
return -1;
}
FILE *outfp=fopen(argv[2],"wb");
printf("in:%s out:%s\n", argv[1], argv[2]);
// 分配解复用器的内存,使用avformat_close_input释放
ifmt_ctx = avformat_alloc_context();
if (!ifmt_ctx)
{
printf("[error] Could not allocate context.\n");
return -1;
}
// 根据url打开码流,并选择匹配的解复用器
ret = avformat_open_input(&ifmt_ctx,argv[1], NULL, NULL);
if(ret != 0)
{
printf("[error]avformat_open_input: %s\n", av_get_err(ret));
return -1;
}
// 读取媒体文件的部分数据包以获取码流信息
ret = avformat_find_stream_info(ifmt_ctx, NULL);
if(ret < 0)
{
printf("[error]avformat_find_stream_info: %s\n", av_get_err(ret));
avformat_close_input(&ifmt_ctx);
return -1;
}
// 查找出哪个码流是video/audio/subtitles
videoindex = -1;
// 推荐的方式
videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if(videoindex == -1)
{
printf("Didn't find a video stream.\n");
avformat_close_input(&ifmt_ctx);
return -1;
}
// 分配数据包
pkt = av_packet_alloc();
av_init_packet(pkt);
// 1 获取相应的比特流过滤器
//FLV/MP4/MKV等结构中,h264需要h264_mp4toannexb处理。添加SPS/PPS等信息。
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
// 2 初始化过滤器上下文
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);
file_end = 0;
while (0 == file_end)
{
if((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
{
// 没有更多包可读
file_end = 1;
printf("read file end: ret:%d\n", ret);
}
if(ret == 0 && pkt->stream_index == videoindex)
{
#if 1//
int input_size = pkt->size;
int out_pkt_count = 0;
if (av_bsf_send_packet(bsf_ctx, pkt) != 0) // bitstreamfilter内部去维护内存空间
{
av_packet_unref(pkt); // 你不用了就把资源释放掉
continue; // 继续送
}
av_packet_unref(pkt); // 释放资源
while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
{
out_pkt_count++;
// printf("fwrite size:%d\n", pkt->size);
size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
if(size != pkt->size)
{
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
}
if(out_pkt_count >= 2)
{
printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
input_size, out_pkt_count);
}
#else // TS流可以直接写入 st
size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
if(size != pkt->size)
{
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
#endif
}
else
{
if(ret == 0)
av_packet_unref(pkt); // 释放内存
}
}
if(outfp)
fclose(outfp);
if(bsf_ctx)
av_bsf_free(&bsf_ctx);
if(pkt)
av_packet_free(&pkt);
if(ifmt_ctx)
avformat_close_input(&ifmt_ctx);
printf("finish\n");
return 0;
}
发送数据包到过滤器:运用 av_bsf_send_packet 函数把输入的数据包发送给比特流过滤器上下文 bsf_ctx。若发送失败,释放该数据包并继续处理下一个数据包。
接收处理后的数据包:通过 av_bsf_receive_packet 函数从过滤器中接收处理后的数据包,可能会接收到多个输出数据包。
写入文件:将每个处理后的数据包写入输出文件。
调试信息输出:若接收到的输出数据包数量超过预期(这里是大于等于 2 个),输出调试信息。