Asterisk 现有版本不支持播放视频文件(支持视频通话),无法满足发送视频通知、视频 IVR 等场景。本系列文章,通过学习音视频的相关知识和工具,尝试实现一个通过 Asterisk 播放 mp4 视频文件的应用。
- Asterisk播放mp4(1)——音频和PCM编码
- Asterisk播放mp4(2)——音频封装
- Asterisk播放mp4(3)——搭建开发环境
- Asterisk播放mp4(4)——H264&AAC
- Asterisk播放mp4(5)——MP4文件解析
- Asterisk播放mp4(6)——音视频同步
- Asterisk播放mp4(7)——DTMF
本文关注在MP4文件中,h264
和aac
的媒体数据是如何存放的,又如何进行访问?为后续解析文件后,通过RTP进行发送进行准备。
MP4文件格式
MP4文件封装格式,对应的标准为ISO/IEC 14496-12
,即:信息技术视听对象编码的第12部分:ISO 基本媒体文件格式(Information technology Coding of audio-visual objects Part 12: ISO base media file format)。MP4文件可以嵌入任何形式的数据,不过通常存放的是AVC(H.264)编码的视频和AAC/MPEG-4(Part 2)编码的音频。
MP4文件由许多box
组成,box
可以嵌套。box
分为header
和data
两部分。header
包括size
(4字节),type
(4字节)和largesize
(8字节,可选)。其中,size
指明了整个box
的大小,包括header
部分。如果box
很大(例如存放具体视频数据的mdat box),超过了uint32的最大数值,size
就被设置为1,并用接下来的8位uint64来存放大小。box
中的字节序为网络字节序,也就是大端字节序(Big-Endian),就是一个32位的4字节整数存储方式为高位字节在内存的低端。
下面看看几个主要的box
。
box类型 | 层级 | 名称 | 说明 |
---|---|---|---|
ftyp | 1 | File Type Box | 有且只有1个,只能被包含在文件层,而不能被其他 box 包含。该 box 应该被放在文件的最开始,指示该 MP4 文件应用的相关信息。 |
moov | 1 | Movie Box | 有且只有1个,只能被包含在文件层。moov是一个container box ,包含若干子box,例如:mvhd,trak等,子box中存放媒体的元数据(metadata)信息。 |
mvhd | 2 | Movie Header Box | 描述了媒体一些基本信息,例如:timescale,duration等。 |
trak | 2 | Track Box |
trak 也是一个container box ,其子box包含了该track的媒体数据引用和描述(hint track除外)。一个MP4文件中的媒体可以包含1个或多个track,它们之间彼此独立,有自己的时间和空间信息。trak至少包含tkhd和mdia这两个box,此外还有很多可选的box。其中tkhd 为track header box ,mdia 为media box ,该box是一个包含一些track媒体数据信息box的container box。 |
mdat | 1 | Media Data | 实际媒体数据,最终解码播放的音视频数据都在这里面。对于h264 和aac 编码的媒体来说,其视频mdat 中内容是nal ,对于音频来说,其内容为aac 的一帧。mdat中的帧依次存放,每个帧的位置、时间、长度都由moov中的信息指定。 |
trak
是mp4中最复杂的部分,包含了读取媒体数据所需的各种信息。要理解trak
下的box
首先要掌握几个基本概念,包括:track,sample,trunk。
track:表示
sample
的集合,对于媒体数据来说,track表示一个视频或音频序列。sample:video sample即为一帧视频,或一组连续视频帧,audio sample即为一段连续的压缩音频。
chunk:一个
track
中的几个sample
组成的单元。
trak
下有很多box
,最重要也是最复杂的是trak/mdia/minf/stbl
,它下面又包含了多个box
。sample table
指明sampe
时序和存储地址的表。利用这个表,可以解析sample
的时序、类型、大小以及在文件中的位置。下面对这些box
做个简单的说明:
box类型 | 名称 | 说明 |
---|---|---|
stsd | Sample Description Box | 包含h264 编码的sps 和pps 。可以有一个到多个sample description 。 |
stsc | Sample To Chunk Box | 指定了chunk 和sample 的对应关系。从first_chunk 这个chunk 序号开始,每个chunk 都有samples_per_chunk 个sample ,每个sample 都可以通过sample_description_index 这个索引,在stsd 中找到描述信息。 |
stsz | Sample Size Box | 指定了每个sample 的大小。 |
stco | Sample Size Box | 指定了每个chunk 的在整个文件中的起始地址。 |
stss | Sync Sample Box | 关键帧。 |
stts | DecodingTime to Sample | 用于计算sample 的dts ,其中sample_counts 定义连续多少个sample的dts 具有相同的差值,sample_delta 为dts 的差值。 |
ctts | Composition Time to Sample | 每个sample 的构成时间(Composition Time)和解码时间(DT)之间的差值。如果不存在ctts,则代表该流不存在B帧,那么CT就直接等于DT。 |
上图是ISO/IEC 14496-12
规范中给出的示例。第2行代表了视频帧的存储序列,帧后面的编号代表了显示顺序。视频流编码时,如果支持B帧,P帧会先于B帧编码,因此帧编码顺序(存储顺序)和帧播放顺序不一致。通过这个图我们就更容理解上面两个和时间相关的box
的含义。stts
定义的是表格中的第3行,DT,用于计算出每个sample
的dts
;ctts
对应的是表格中的第6行,Composition offset,用于计算出每个sample
的pts(Compostion Time)
DTS(Decode Time Stamp):标识读入内存中的视频帧什么时候开始送入解码器中进行解码。PTS(Presentation Time Stamp):用于度量解码后的视频帧什么时候被显示出来 。这两个值对应的并不是秒,毫秒这些时间单位,而是时间刻度,它们的值除以mdhd
中的timescale
(每秒钟有多少个时间刻度)转换为时间。
制作样本
为了控制篇幅我们忽略音频流,只分析视频流。
ffmpeg -t 10 -lavfi sine -t 10 -lavfi color=red sine-red-10s.mp4
mdat
中存放的就是媒体数据,视频就是h264
的NALU
,我们可以将mp4中的数据和h264裸流中的数据进行对比。
ffmpeg -t 10 -lavfi color=red red-default.h264
我们直接生成h264的裸流,其中I/P/B帧的数据和生成mp4文件中的h264
流的数量是一致的。
通过ffmpeg生成mp4文件时有很多参数可以指定,faststart
是比较常用的一个,其作用是将moov
挪到mdat
前面,这样就可以实现边下载边播放。
ffmpeg -t 10 -lavfi sine -t 10 -lavfi color=red -movflags faststart sine-red-10s-faststart.mp4
mp4文件的结构太复杂了,直接看二进制数据很费劲,下面我们通过一个在线工具解析mp4数据。
后面计算时序时要用到timescale
和duration
这两个参数,duration / timescale = 10秒。
从stsd/avc1/avcC
中可以获得h264
的sps
和pps
。另外,需要特别注意lengthSizeMinusOne
这个参数,其含义是用几个字节表示nalu
的长度,实际的长度是值加1,这里就是3+1=4
。h264
裸流中,分割nalu通常采用的是Annex B
这种用起始码的方式(用3字节或4字节的起始码),但是在MP4中使用的是AVCC
方式(填加字节指定nalu的大小)。
stsc
中指明track共有250个采样,以及每个采样的大小。这里需要注意两点:1、采样的大小是NALU
的实际大小加4,因为前面有4字节记录用来记录尺寸;2、采样不一定是一个NALU
,实际上第一个采样(759)就包括了SEI
和IDR
两个NALU
。
stsc
中定义了chunk
和sample
的对应关系,以及sample的描述信息(stsd)。这里第一个chunk
包含了两个sample
,后面每个chunk
包含一个sample
。
stco
中定义了chunk
在文件中的地址。通过stsz
,stsc
和stco
就可以在文件中获得任意一个sample
。9792
是第一个chunk
的偏移量,第一个chunk
包含2个采样,前2个采样的大小分别为759
和17
,可以计算出下一个chunk
的偏移量是10568
,它不等于下一个视频chunk
的偏移量10991
,通过查看音频的stco
,可知音频流的第一个chunk
的偏移量是10568
,这说明视频和音频的chunk
是交错存储的。
stts
中记录了每个sample
间相差的时间刻度512。前面的timescale
的值为12800,512/12800=0.04秒,说明每帧之间的解码时间间隔为0.04秒。
ctts
提供的是composition time
和decoding time
的差值,它们的和就是composition time
。
通过解析red.h264
文件的slice
可以知道每帧的大小和类型,这样方便我们理解decode time
:
1 | 视频帧 | I | P | B | B | B | P | B | B | B |
---|---|---|---|---|---|---|---|---|---|---|
2 | DT(stts) | 0 | 512 | 1024 | 1536 | 2048 | 2560 | 3072 | 3584 | 4096 |
3 | Composition offset(ctts) | 1024 | 2560 | 1024 | 0 | 512 | 2560 | 1024 | 0 | 512 |
4 | CT | 1024 | 3072 | 2048 | 1536 | 2560 | 5120 | 4096 | 3584 | 4608 |
5 | seq | 1 | 5 | 3 | 2 | 4 | 9 | 7 | 6 | 8 |
6 | size | 65 | 13 | 10 | 10 | 10 | 19 | 12 | 10 | 10 |
上表中的第1行(DT)代表的是文件中视频帧的存储顺序,第4行(CT)代表的是视频帧的播放时间(单位是时间刻度),第5行(seq)代表的是视频帧的播放顺序。可以看到存储顺序和播放顺序是不一致的,这是因为视频帧中包含B帧,它是双向依赖帧,解析是可能要依赖P(或B帧),所以虽然B帧的播放顺序靠前,但是解码的时候必须要先解码P帧,才能解码B帧。另外,在avcc
中一个sample
的前4个字节代表这个包的大小,因此stsz
中记录的采样大小比NALU
的大小多4。
通过对比h264裸流和mp4中的视频数据,我们可以得到一致的结果,就是说mp4中的stts
和ctts
记录h264的解析结果,方便了数据的直接访问。