MKV 文件格式解析
前言
MKV(Matroska Video File)是一种Matroska媒体格式的多媒体封装格式(Multimedia Container Format,简称MCF)。但Matroska媒体格式除了MKV外,常见的还有MKA (Matroska Audio File)单一音频文件,可以有多条及多种类型的音轨,MKS (Matroska Subtitles)字幕文件,Matroska来自于俄语,影射俄罗斯娃娃,就是下面这个啦,表示一层包着另外一层。
MKV采用可扩展二进制元语言EBML(Extensible Binary Meta Language)来描述其文件结构,EBML用元素(Elements)来描述EBML文档,组织结构如下:
Element元素ID用大端方式编码,起始位0的个数代表了ID的长度,ID长度=起始0的个数+1。除起始位0外,其余bit全1的ID为保留 ID,元素ID可以包括其子ID。
Element数据长度也采用大端方式编码,起始位0的个数代表了数据长度(包括头部)占用的字节个数,字节个数=起始0的个数+1。可以是1~8个字节:
EBML组成的文件包含EBML头和EBML体两部分。Matroska 文件可以看作是包含EBML头和Segment两部分的文件。
MKV文件分析
MKV文件分析工具有 EBML Tree Viewer AVI-Mux GUI和MKVToolnix,下面图片是用AVI-Mux解析的某MKV文件
EBML头
Segment
Matroska 文件的segment包含了音视频数据和播放音视频数据所需要的信息。Matroska 文件中可以有多个segment,但不是所有的播放器都支持这样做。Segment的结构示意图如下:
Segment的第一级子元素元素名及ID如下:
1. SeekHead
包含多个seek entry,每一个seek entry是EBML的一种类型的元素,SeekID对应EBML元素ID。SeekID可以是Info,Tracks,Cues,SeekHead,Cluster等等,当SeekID描述的是Cluster时,代表播放过程中可以SEEK对应位置,此时一般会有多个条目,用来描述可以SEEK到的Cluster,这有点像mov的sidx描述的索引,而Cues则可以SEEK到Cluster内,如果Cues中没有CueTrackPosition 元素,那么,他也是对应seek到cluster,FFMPEG是在read过程中,丢掉了cluster和期望seek到的target之间的这段数据。
SeekPosition对应元素在Segment中的位置。这个位置的base是SeekHead的起始位置,SeekPosition+SeekHead的起始位置才是元素在文件中的绝对位置。也就是说定位头Seekhead存储的信息是剪辑Segment里面的其他Master元素的类型和位置,比如剪辑信息SegmentInfo,轨道Tracks,索引表Cues,标签Tags信息所在的位置,这个位置信息也是个相对值,是定位头Seekhead的位置的相对值,实际地址等于定位头所在绝对位置+定位条目里面的位置SeekPosition。
2. Info
包含了Segment的时间戳base和时长。
以下是Info元素的解释:
3. Tracks
音视频,字幕流的描述信息,TrackNumber是流的编号,更像是流的序列号,在Block的头部中使用到的Track Number便是来自于此,TrackUID是Track的唯一标识,具有长达8个字节。它可能会在后面的Tag中用到,用来标识此Tag描述的Track。CodecID以及音视频播放的参数也保存在此元素中。
详细元素解释如下:
4. Chapters
用来预览文件里面的分段信息,它提前定义了一些时间点可供预览时跳到对应时间。
5. Cluster
Cluster包含了音频视频字幕轨道的数据流,Matroska文件至少包含一个Cluster元素,每个必须包含一个时间戳,它也是第一个Block的时间戳,这个时间戳是与Segment相对。每个Block里面包含了相对第一个Block时间戳的时间偏移,因此,Block里面的时间可以看作是Segment的时间偏移+Cluster的时间偏移+Block的时间偏移。
参考帧信息也在Block中描述。如SimpleBlock的前几个字节为:A3 20 4F EC 81 00 00 80
A3表示是SimpleBlock, 20 4F EC表示长度为20460字节,81(8表示长度1字节,1表示Track Number为1) 00 00(两个字节的time code)
80要按位解析,最高位优先:
【 7 】是Keyframe设置为1,
【6-4】保留。
【 3 】此帧解码但不显示置1
【2-1】Lacing 标记。00:没有Lacing。01:Xiph Lacing。11:EBML Lacing。 10:固定长度Lacing。
【 0 】Block可以被丢弃置1.
BlockGroup中的ReferenceBlock用来表示Block类型。Block里面必须包括TrackNumber参数,用来描述当前的Block属于哪个Track,注意这儿用的是TrackNumber而不是TrackUID。所以一个Cluster是可以同时包含Video,Audio,Subtitle数据的,这些数据存在于不同的Block中。
如下是Cluster中元素的详细解释:
看一个BlockGroup中包括Block的例子:
6. Cues
Cues元素用来对播放的音视频数据进行Seek的,它可以定位到特定的时间点进行播放。没有Cues的Seek比较麻烦,需要对Cluster进行parse才行。Cues中应该包含多个CuePoint才是合理的,Cues可以说成就是索引表。CuePoint时间戳放在CueTime Element中的,对应时间在文件中的位置放在CueTrackPositions中。注意CueTrackPositions并不是Cluster的绝对偏移,它是相对于Segment的偏移。也就是说,CueTrackPositions需要加上Segment在文件中的位置和Segment header的长度才是最终要查找的文件位置,Cue索引的Cluster在文件中的位置=CueTrackPosition+Segment Pos + Segment Header size。
元素解释如下:
7. Attachments
给Matroska文件添加一些附加的文件信息,比如图片,网页,程序等等。
8. Tags
Segment的metadata描述。
后记:
在笔者看来,MKV文件格式中如果出现了错误,寻求一个完美的解决方案相当麻烦。MKV中的元素ID不具有唯一性,可能和数据相同,这就造成了在resync的时候,误把数据当成元素ID的可能性。
Gstreamer的MKV parse
static GstFlowReturn
gst_matroska_parse_chain (GstPad * pad, GstObject * parent, GstBuffer * buffer)
{
……
/* 非连续,清除adapter里面的buffer及信息 */
if (G_UNLIKELY (GST_BUFFER_IS_DISCONT (buffer))) {
GST_DEBUG_OBJECT (parse, "got DISCONT");
gst_adapter_clear (parse->common.adapter);
GST_OBJECT_LOCK (parse);
gst_matroska_read_common_reset_streams (&parse->common,
GST_CLOCK_TIME_NONE, FALSE);
GST_OBJECT_UNLOCK (parse);
}
gst_adapter_push (parse->common.adapter, buffer);
buffer = NULL;
next:
available = gst_adapter_available (parse->common.adapter);
/* 获取下一个id及元素的大小length,needed是元素id本身占用的字节数加上元素长度length所占用的字节数(比如:
* 00000000 1a 45 df a3 a3 42 86 81 .E...B..
* 00000008 01 42 f7 81 01 42 f2 81 .B...B..
* 00000010 04 42 f3 81 08 42 82 88 .B...B..
* 00000018 6d 61 74 72 6f 73 6b 61 matroska
* 00000020 42 87 81 04 42 85 81 02 B...B...
* EBML头ID为0x1A45DFA3,占用4字节,它的长度字段0xa3占用一个字节,
* 头里面的内容占用35个字节,0xa开头没有0,故用一字节,则ID=0x1A45DFA3,length=0x23,need=5),
如果长度字段全部是FF的话,则设置成最大长度G_MAXUINT64
*/
ret = gst_matroska_read_common_peek_id_length_push (&parse->common,
GST_ELEMENT_CAST (parse), &id, &length, &needed);
/* 返回值一共三种,OK,EOS或者ERROR,ERROR时代表长度或者ID错误 */
if (G_UNLIKELY (ret != GST_FLOW_OK && ret != GST_FLOW_EOS)) {
/* ebml_segment_length未设置默认值,但遇到ID_SEGMENT时,会配置这个参数,文件中这个参数为0时,会配置成G_MAXUINT64,ebml_segment_start就是segment在文件中开始的位置 */
if (parse->common.ebml_segment_length != G_MAXUINT64
&& parse->common.offset >=
parse->common.ebml_segment_start + parse->common.ebml_segment_length) {
/* 代表处理过的数据已经超过了段segment的长度。 */
return GST_FLOW_EOS;
} else {
/*
* 获取ID或者长度出现错误,设置成SCANNING,事实上gstreamer只有在CLUSTER时,才支持,非CLUSTER相关的ID在scanning的时候会被丢弃,所以如果其他信息错误,是无法播放的。
* parsing error: we need to flush a byte from the adapter if the id is
* not a cluster and so on until we found a new cluser or the
* INVALID_DATA_THRESHOLD is exceeded, we reuse gst_matroska_parse_parse_id
* setting the state to GST_MATROSKA_READ_STATE_SCANNING so the bytes
* are skipped until a new cluster is found
*/
gint64 bytes_scanned;
if (parse->common.start_resync_offset == -1) {
/* 标记开始查找下一个ID的开始位置 */
parse->common.start_resync_offset = parse->common.offset;
parse->common.state_to_restore = parse->common.state;
}
bytes_scanned = parse->common.offset - parse->common.start_resync_offset;
/* 重新同步的数据量不大,还可以继续同步 */
if (bytes_scanned <= INVALID_DATA_THRESHOLD) {
GST_WARNING_OBJECT (parse,
"parse error, looking for next cluster, actual offset %"
G_GUINT64_FORMAT ", start resync offset %" G_GUINT64_FORMAT,
parse->common.offset, parse->common.start_resync_offset);
parse->common.state = GST_MATROSKA_READ_STATE_SCANNING;
ret = GST_FLOW_OK;
} else {
/* 重新同步的数据量太大了,返回错误 */
GST_WARNING_OBJECT (parse,
"unrecoverable parse error, next cluster not found and threshold "
"exceeded, bytes scanned %" G_GINT64_FORMAT, bytes_scanned);
return ret;
}
}
}
GST_LOG_OBJECT (parse, "Offset %" G_GUINT64_FORMAT ", Element id 0x%x, "
"size %" G_GUINT64_FORMAT ", needed %d, available %d",
parse->common.offset, id, length, needed, available);
/* ID长度及长度字段的字节数大于可用数据量,返回 */
if (needed > available)
return GST_FLOW_OK;
ret = gst_matroska_parse_parse_id (parse, id, length, needed);
if (ret == GST_FLOW_EOS) {
/* need more data */
return GST_FLOW_OK;
} else if (ret != GST_FLOW_OK) {
return ret;
} else
goto next;
}
参考网页:
https://www.xuebuyuan.com/zh-hant/1695652.html
https://www.xuebuyuan.com/zh-hant/2111311.html
https://www.matroska.org/technical/diagram.html
https://matroska-org.github.io/libebml/specs.html
https://github.com/ietf-wg-cellar/ebml-specification/blob/master/specification.markdown#ebml-element-types