3.17 stts box*
stts box里面保存了一个压缩格式的表,用来描述音视频帧的解码时间戳。如下图19:
图19
stts box里面保存的这个表c语言表示可以描述为:
struct stts_entry_t
{
uint32_t sampe_count;
uint32_t sample_delta;
}
struct stts_entry_t entry[entry_count];
其记录的含义是连续相邻的sample_count个帧每个帧的持续时长,时长单位由mdhd box中的timescale来设定。通过这个数组结构,我们就可以计算出 每一帧的解码时间戳。譬如,stts的记录为:[{2,1000},{3,1500},{3,1200}......]
那么展开后,帧的时间戳序列为:[0,1000,2000,3500,5000,6500,7700,8900,10100,......]
所以当裁剪的时候,因为用户指定的目标MP4的时间戳范围,所以,需要在展开后的时间戳序列中来匹配目标时间戳范围进行截取。为了让播放的时候能够不产生花屏现象,一般要求首帧是IDR帧,所以,对于用户指定的目标时间戳范围中的起始时间戳,我们需要先找到离起始时间戳最近的关键帧,得到该关键帧的帧序号,然后再直接跳过关键帧之前的帧,进行截取处理。
关于如何找到最接近的关键帧,需要查找stss box中的关键帧记录表,请参考3.18节中的内容描述。
3.18 stss box*
stss box里面保存了一个关键帧帧序号的列表,以便播放器可以快速按照关键帧进行seek操作。如下图20:
图20
stss box里面保存的这个表c语言表示可以描述为:
struct stss_entry_t
{
uint32_t sample_number;
}
struct stss_entry_t entry[entry_count];
通过以上可以看到,这个表没有经过压缩处理,非常直白,就是依次罗列了所有关键帧的ID,标识哪些帧是关键帧。
对于音频trak,由于没有关键帧的概念,所以stts box是没有的,那么也就不存在裁剪的时候要进行按照关键帧进行对齐的处理动作。
与stts box一样,本表也需要进行裁剪处理,在进行stts box处理的时候,我们已经可以知道最终裁剪完成后包括的帧的起始和结束ID,然后对照stss表中的记录,保留裁剪范围内的关键帧ID记录就可以了。
本box中用压缩形式记录了音视频帧解码和展现的时间偏移。如下图21:
图21
对于version=0, 那么规定解码时间必须小于展现时间,并且时间偏移偏移是无符号整数:CT(n) = DT(n) + CTTS(n),其中DT(n)为中的帧id为n的帧的解码时间,CT(n)为帧id为n的帧的展现时间, CTTS(n)则为两者之间的差值。
对于version=1,那么解码时间和展现时间的偏移量是带符号整数。 如果该trak的所有帧解码时间和展现时间都是相同的(譬如H264编码没有B帧的情况),那么它们之间的偏移量是0,那么这个表只需要记录一条sample_count等于总帧数,并且sample_offset为0的记录即可。
stss box里面保存的这个表c语言表示可以描述为:
struct ctts_entry_t
{
uint32_t sample_count;
uint32_t sample_offset;
}
struct ctts_entry_t entry[entry_count];
举个例子,假设ctts中的表的记录如下:
Sample count | Sample_offset |
---|---|
1 | 10 |
1 | 30 |
2 | 0 |
1 | 30 |
2 | 0 |
1 | 10 |
1 | 30 |
2 | 0 |
1 | 30 |
2 | 0 |
而stts中的记录为:
sample count | sample delta |
---|---|
2 | 1000 |
3 | 1500 |
3 | 1200 |
2 | 1000 |
4 | 1200 |
根据ctts和stts两个表,进行解压后为,可以得到每个帧的dts和pts值,如下:
sample id | decode timestamp | sample offset | composition timestamp |
---|---|---|---|
0 | 0 | 10 | 10 |
1 | 1000 | 30 | 1030 |
2 | 2000 | 0 | 2000 |
3 | 3500 | 0 | 3500 |
4 | 5000 | 30 | 5030 |
5 | 6500 | 0 | 6500 |
6 | 7700 | 0 | 7700 |
7 | 8900 | 10 | 8910 |
8 | 10100 | 30 | 10130 |
9 | 11100 | 0 | 11100 |
10 | 12100 | 0 | 12100 |
11 | 13300 | 30 | 13330 |
12 | 14500 | 0 | 14500 |
13 | 15700 | 0 | 15700 |
所以在裁剪的时间也就是根据裁剪的起始帧的sample id和结束帧的sample id进行处理即可,因为ctts本身对应的压缩表,所以可能需要裁剪的sample正好位于头部和尾部的记录的中间,需要特别处理一下。
3.20 stsc box*
stsc box描述了如何将各个sample划分为不同的chunk,它也是采用压缩方式进行存储。格式如下图22:
图22
stss box里面保存的这个表c语言表示可以描述为:
struct stsc_entry_t
{
uint32_t first_chunk;
uint32_t samples_per_chunk;
uint32_t sample_description_index;
}
struct stsc_entry_t entry[entry_count];
其中first_chunk是一个整数,指定了第1个块的块序号,从该序号起到下一条记录的first_chunk之间的每个chunk都包含sample_per_chunk条记录。如果当前记录是最后一条记录,那么这里first_chunk指定的chunk开始到最后一个chunk,每个chunk都是samples_per_chunk数量的sample数。那么如何知道最后一个chunk的id呢,那么就需要根据stco或者co64 box中的记录,来得到总共有多少chunk了,通过chunk的数量就可以推知最后一个chunk的id。
其中sample_description_index是一个整数,表示描述此块中样本的样本条目的索引。索引范围从1到stsd box中的样本条目数。在裁剪处理的时候是不需要关心的。
对stsc表举例如下:
first chunk | samples_per_chunk | sample_description_index |
---|---|---|
0 | 10 | 1 |
2 | 15 | 1 |
5 | 12 | 1 |
14 | 10 | 1 |
假设当前mp4总共有15个chunk,那么对stsc表解压后得到:
chunk id | samples_per_chunk | sample_description_index |
---|---|---|
0 | 10 | 1 |
1 | 10 | 1 |
2 | 15 | 1 |
3 | 15 | 1 |
4 | 15 | 1 |
5 | 12 | 1 |
6 | 12 | 1 |
7 | 12 | 1 |
8 | 12 | 1 |
9 | 12 | 1 |
10 | 12 | 1 |
11 | 12 | 1 |
12 | 12 | 1 |
13 | 12 | 1 |
14 | 10 | 1 |
15 | 10 | 1 |
理解了以上逻辑,那么在裁剪的时候就可以知道如何对stsc表进行裁剪了。需要注意的是,首先需要遍历这个表,找到起始sample对应的是哪个chunk,因为一般来说起始sample是位于chunk的中间的,所以需要对该chunk进行裁剪,对于包含结尾sample的chunk来说,也需要做类似的操作,最终保留的是这两个chunk及其之间的所有chunk。
3.21 stsz box*
stsz box记录了每个sample的大小。其格式如下图23:
图23
如果当前trak的所有sample的大小是一样的,那么sample_size就可以设置为sample的大小,否则sample_size将被设置为0,sample_size = 0,会要求每一个sample的大小在下面的表中进行列出,sample的总个数为sample_count。
根据以上结构,进行mp4裁剪的时候,首先要调整sample_count的值,如果不是所有的sample大小都一样的情况,则要接着裁剪entry_size表,去掉不在范围内的记录。
stco box记录了每个chunk在MP4文件中的偏移量。如下图24:
图24
这里的offset指的是chunk的起始位置相对于整个MP4文件的偏移量,而不是相对于mdat box的偏移量。
在进行MP4裁剪操作的时候,需要根据裁剪的sample id的范围,得到chunk的范围,然后截取对应的chunk记录,因为这里记录的是chunk偏移量的绝对值,对于moov在mdat前面的情况,由于moov box的大小缩小了,因此mdat会整体往上移动,包括开头的部分音视频帧也会被删除,所以chunk的偏移量自然就变化了;对于mdat在moov前面的情况,也会因为mdat中开头部分的音视频帧会被删除,所以chunk的偏移量一样需要跟着变化,所以在所有box都调整完毕后,需要重新统一在原来的偏移量上面加上一个delta值。
co64 box记录了每个chunk在MP4文件中的偏移量。结构和stco一致,只不过偏移量是64位版本的,如下图25:
图25
co64 box和stco box的含义是一样的,关于如何裁剪不再赘述。
之前有提到,在指定裁剪范围的时候,有时候起始帧并不一定是关键帧,所以需要往前找到最近的一个关键帧然后再进行裁剪,但是这样子就可能导致多了一部分视频帧,通过edts和elst可以让我们告诉播放器跳过关键帧到真实请求的起始帧之间的内容再开始播放。
edts box的格式如下图26:
图26
在edts box中,可以包含0个或者一个及以上的elst。
elst box存储了若干个视频编辑列表。每个编辑条目定义了一个时间范围内的媒体进行视频播放的偏移量和持续时间。这些编辑条目可以用于对媒体进行裁剪、剪辑或时间轴调整。下面是几个字段的含义说明:
下面来举两个例子说明:
例子1: 譬如需要对某个trak进行延时播放的处理,那么可以在elst中插入一个空的entry,segment_duration设置为需要延迟播放的时间,
media_time设置为-1,然后再插入一个entry,segment_duration
设置为正常播放时间,media_time
也就是起始时间设置为0。
例子2: 譬如需要对某个trak进行跳过前面若干时间进行播放,那么可以在elst中插入一个entry,segment_duration
设置为trak的最终时长,media_time
设置为要跳过的片头时间(当然时间需要折算成trak中定义的timescale单位),media_rate_integer设置为1, media_rate_factor设置为0。在nginx的mp4模块中就通过这个功能来跳过最近的关键帧到真正想要播放的帧之间的内容。
所在在其他moov中的box处理完毕后,我们可以通过定义一个edts和elst来实现跳过片头
的功能。不过遗憾的是,并不是所有的播放器都能够支持edts,譬如windows自带的播放器就不能支持。
elst box的结构如下图27:
图27
以上即是MP4进行裁剪所需处理的box的详细说明,并且对如何进行裁剪的方法进行了详细说明。