FFMPEG avi录像实现
date:2022.09.27
author:lyn
version:ffmpeg4.1.3
1.avi数据结构
1.1 avi简介
网上太多阐述avi文件格式的文章了,对于初学者而言,这些文章看起来很高深,里面太多的专业词汇和简写,如果没有相关的基础很难理解;
avi的简介参考维基百科,AVI的文件结构分为头部
,主体
和索引
三部分。主体中图像数据和声音数据是交互存放的。从尾部的索引可以索引跳到自己想放的位置。
AVI将视频和音频封装在一个文件里,其顺序是:若干视频帧(Video Frame)之后接着若干音频帧(Audio Frame),再然后是视频帧、音频帧,故名为“音频视频交织”,意即音频和视频按帧交错排列,以此达到音频同步于视频播放的效果。和DVD视频格式一样,AVI文件支持多视频流和音频流,虽然这些功能很少使用。大多数AVI文件还支持由Matrox OpenDML集团于1996年2月开发的格式后缀。这些文件非正式的称为“AVI 2.0”,并得到微软的支持。
AVI本身只是提供了这么一个框架,内部的图像数据和声音顺据格式可以是任意的编码形式。但是由于索引放在了文件尾部,所以在播放internet流媒体时已属力不从心。
理解avi的文件格式,首先需要认识avi文件是怎么组织的,按照一般的理解,应该要有标识封装文件格式的标志,是mov
,avi
,mkv
或者其他的封装格式,然后开始记录音视频流的相关参数,包括图像和音频的参数(宽高、码率、时长、采样率),然后就是具体的编码数据。按照这样的理解,这样就可以入门理解各种视频封装的格式;
1.2 avi名词解释和文件结构
avi比较麻烦的是它的各种char id,他们通常采用简写方式,需要看官方文档才明白具体的简写是什么意思;
avi文件的层级结构如下所示,最外的是一个RIFF标志的根,然后在内部包含其他LIST和Chunk;
RIFF AVI
->LIST hdrl #Header List 信息块,用于描述avi媒体格式
->avih #Main AVI Header avi的头部,主要的参数有图像的宽高参数和数据流个数等全局的参数,是否包含索引块,交叉存储等信息;
->LIST strl #视频流信息 Stream Header List数据流头部list,包含了strlh 和strf;
->strh #Stream Header 数据流的头部,主要的记录音视频流相关的参数,采样率,流标识等相关参数;
->strf #Stream Format 数据流的相关格式,数据流的宽高等参数记录在这里
->JUNK
->LIST strl #音频流信息
->strh
->strf
->JUNK
->LIST strl #字幕等信息
->strh
->JUNK
->LIST INFO #AVSII码标志描述的metadata信息,包含作者,版权名字等相关信息;
->ISFT #描述文件的打包程序
->LIST movi #数据块,音频/视频/字幕数据
->movi
->idx1 #索引块,可选有多种类型的索引dc:视频 wb:音频 tx:字幕
分析avi文件二进制代码可以获得如下的文件结构
2.ffmpeg avi封装实现
视频和音频封装为avi格式调用接口顺序如上所示,avio_open
avio_close
接口取代旧的 url_fopen
和url_fclose
然后直接调用avi_write_header
写avi文件的头部,数据包通过avi_write_packet
一帧一帧的将数据写入,不断调用这个接口,最后调用avi_write_trailer
将文件尾部写入;这样就将编码数据封装为avi格式的文件;
3.avi函数调用关系
avio_open
{
avio_open2
{
ffurl_open_whitelist //初始化URLContext
{
ffurl_alloc
{
url_find_protocol //查找合适的URLProtocol,并创建,file也是一种协议
url_alloc_for_protocol
}
ffurl_connect //打开获得的URLProtocol
}
ffio_fdopen //URLContext初始化AVIOContext
{
avio_alloc_context
{
ffio_init_context
}
}
}
}
avi_write_header
{
list1 = avi_start_new_riff(s, pb, "AVI ", "hdrl"); //avi header list 写头部和 1:hdrl List
ffio_wfourcc(pb, "avih"); //avih
/* stream list 包含音频和视频两个list,循环写入 */
list2 = ff_start_tag(pb, "LIST"); // LIST 这里有音频和视频两个List 2:视频 3:音频
ffio_wfourcc(pb, "strl"); // strl char id
ff_start_tag(pb, "strh"); // strh char id
ffio_wfourcc(pb, "vids"); // 视频char id
ffio_wfourcc(pb, "auds"); // 音频char id
ff_start_tag(pb, "strf"); // strf char id
ff_end_tag(pb, strf); // 结束strf chunk
ff_riff_write_info_tag(s->pb, "strn", t->value); // strn info tag
write_odml_master
{
ff_start_tag(pb, "JUNK");
}
ff_start_tag(pb, "vprp");
ff_end_tag(pb, vprp); // 结束vprp chunk
ff_end_tag(pb, list2); // 结束一个流LIST
ff_end_tag(pb, list1); // 结束hdrl List
ff_riff_write_info(s); // 写INFO List 4:INFO
ff_start_tag(pb, "LIST");
ffio_wfourcc(pb, "movi"); // 写 movi List 5:movi 数据块的List
}
avi_write_packet
{
ff_check_h264_startcode // 检查h264数据startcode
write_skip_frames // 写入跳过的帧,根据dts判断要不要写入空的packet
{
av_init_packet
avi_write_packet_internal
{
avi_write_ix;
avi_write_idx1;
avi_start_new_riff(s, pb, "AVIX", "movi");
avi_stream2fourcc // dc:视频流 wb:音频和其他
avio_write(pb, tag, 4); // 写一帧的数据,前4个字节是类型char id
avio_wl32(pb, size); // 数据长度
avio_write(pb, pkt->data, size); // 写编码数据
}
}
avi_write_packet_internal // 写数据包
{
avi_write_ix;
avi_write_idx1;
avi_start_new_riff(s, pb, "AVIX", "movi");
avi_stream2fourcc // dc:视频流 wb:音频和其他
avio_write(pb, tag, 4); // 写一帧的数据,前4个字节是类型char id
avio_wl32(pb, size); // 数据长度
avio_write(pb, pkt->data, size); // 写编码数据
}
}
avi_write_trailer
{
write_skip_frames // 写入跳过的帧,根据dts判断要不要写入空的packet
ff_end_tag(pb, avi->movi_list); // 结束 movi List
avi_write_idx1(s); // 写入索引块chunk
{
ff_start_tag(pb, "idx1");
/* 轮询写入每一帧的索引信息 */
avi_stream2fourcc(tag, stream_id,
s->streams[stream_id]->codecpar->codec_type);
ffio_wfourcc(pb, tag); // 写入char id 00dc:视频帧 00wb:音频帧
avio_wl32(pb, ie->flags); // ckid chunk的索引id,根据ent_id/AVI_INDEX_CLUSTER_SIZE
avio_wl32(pb, ie->pos); // 帧的位置偏移
avio_wl32(pb, ie->len); // 帧数据长度
}
ff_end_tag(pb, avi->riff_start); // 结束RIFF
}
avio_close // 释放资源
{
avio_flush
av_opt_free
avio_context_free
ffurl_close
}