MKV 文件格式解析

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值