MKV Demux 插件知识文档
MKV Demux 插件知识文档
基于 GStreamer gst-plugins-good/gst/matroska 插件源码整理,涵盖 Matroska/WebM 容器格式关键信息及 matroskademux 元素调用流程。
零、MKV 是什么,为什么要有它
定位
Matroska 是一种开放、免费的容器格式,不包含任何编解码算法,只负责把视频、音频、字幕、附件等多路数据打包到一个文件中。类似一个信封——不管信纸用什么语言写的,信封只管投递。
文件扩展名约定:.mkv(视频)、.mka(音频)、.mks(字幕)。
与 WebM 的关系
WebM 是 Google 定义的 Matroska 受限子集:仅允许 VP8/VP9/AV1 视频 + Vorbis/Opus 音频,去掉 Chapters/Attachments 等元素。文件仍用 EBML 结构,DocType = “webm”。可理解为:WebM ⊂ MKV。
为什么选 MKV 而不是 MP4
| 场景 | 选 MKV | 选 MP4 |
|---|---|---|
| 多字幕轨(中/英/日) | 原生支持,无限制 | tx3g 有限,常用外挂 |
| 多音轨(5.1/立体声/解说) | 原生支持 | 支持,但兼容性参差 |
| 开源编码器(VP9/AV1/Opus) | 原生 CodecID | 需注册 FourCC,部分播放器不认 |
| 流式写入(边录边存) | Cues 放尾部,天然支持 | moov 放尾部需移动 atom |
| 硬件播放器兼容性 | 一般(智能电视/盒子需适配) | 好(几乎所有设备原生支持) |
| 在线流媒体(DASH/HLS) | 不适用 | ISO BMFF 是标准基础 |
| DRM 版权保护 | WebM 有 AES-CTR,生态弱 | Widevine/PlayReady/FairPlay 成熟 |
一句话总结:MKV 胜在开放灵活(任意编码+多轨道+附件),MP4 胜在生态成熟(设备兼容+DRM+流媒体)。
一、Matroska 容器格式概览
1.1 基本概念
Matroska 文件基于 EBML (Extensible Binary Meta Language) 格式。EBML 采用 ID + Size + Data 的元素结构,类似 XML 但为二进制编码。
术语说明: Matroska 是容器格式名,文件扩展名通常为
.mkv(视频)/.mka(音频)/.mks(字幕);WebM 是其受限子集,仅允许 VP8/VP9/AV1 + Vorbis/Opus 编码。
字节序: EBML 所有整数字段均为大端序 (Big-Endian)。
VINT: Element ID 和 Data Size 均使用 EBML 变长整数编码,首字节的高位前导零决定宽度。
EBML 元素结构:
┌──────────┬───────────┬────────────────────────┐
│ ID(VINT) │ Size(VINT)│ Data / child elements │ ← Size 不包含 ID+Size 自身
└──────────┴───────────┴────────────────────────┘
VINT 编码 (以 Element ID 为例):
首字节 宽度 可表示范围
1xxxxxxx 1B 0x80 ~ 0xFF (7 bit 有效)
01xxxxxx 2B 0x4000 ~ (14 bit 有效)
001xxxxx 3B 0x200000 ~ (21 bit 有效)
0001xxxx 4B 0x10000000 ~ (28 bit 有效)
- Data Size 的 VINT 宽度可达 8 字节(56 bit 有效)
- 当
Size == 0x01FFFFFFFFFFFFFF时表示"未知大小"(GST_EBML_SIZE_UNKNOWN)
1.2 EBML 文件头 (Header)
EBML Header (ID = 0x1A45DFA3)
├── EBMLVersion (0x4286) 值=1
├── EBMLReadVersion (0x42F7) 值=1
├── EBMLMaxIDLength (0x42F2) 值=4
├── EBMLMaxSizeLength (0x42F3) 值=8
├── DocType (0x4282) "matroska" 或 "webm"
├── DocTypeVersion (0x4287) 如 4
└── DocTypeReadVersion (0x4285) 如 2
代码中 gst_ebml_read_header() 负责解析,返回 doctype 和 version。结果决定 common->is_webm 标志。
1.3 Matroska 文件顶层结构
EBML Header (0x1A45DFA3)
├── DocType "matroska" | "webm" ← 决定 is_webm 标志
├── DocTypeVersion 通常 4 ← 影响 WritableApp 行为
├── DocTypeReadVersion 通常 2
├── EBMLMaxIDLength 4 (字节)
└── EBMLMaxSizeLength 8 (字节, VINT 最大宽度)
Segment (0x18538067) ← 顶层容器,Size 可能为 0x01FF..(未知)
│
├── SeekHead (0x114D9B74) — 段目录,可出现1~2次(头+尾)
│ └── SeekEntry (0x4DBB)
│ ├── SeekID (0x53AB) 目标元素 ID,如 0x1C53BB6B=Cues
│ └── SeekPosition (0x53AC) 相对 Segment 起始的偏移(byte)
│
├── SegmentInfo (0x1549A966) — 全局段信息 ★
│ ├── TimecodeScale (0x2AD7B1) 默认1000000 → 1ms/刻度 ★ 全局时间基准
│ ├── Duration (0x4489) Float,总时长(ns)=值×TimecodeScale ★
│ ├── MuxingApp (0x4D80) 混流应用标识
│ ├── WritingApp (0x5741) 写入应用标识
│ ├── Title (0x7BA9) 段标题
│ └── DateUTC (0x4461) 从2001-01-01起的纳秒偏移
│
├── Tracks (0x1654AE6B) — 轨道定义 ★★
│ └── TrackEntry (0xAE) ← 每轨道一个,可能有1~N个
│ ├── TrackNumber (0xD7) 1-based 编号
│ ├── TrackUID (0x73C5) 唯一ID,Cues/Tags 引用依据
│ ├── TrackType (0x83) ★ 1=Video, 2=Audio, 0x11=Subtitle
│ ├── CodecID (0x86) ★ 编码标识,如 "V_MPEG4/ISO/AVC"
│ ├── CodecPrivate (0x63A2) ★ 编码器私有数据(AVCC/HEVC等)
│ ├── DefaultDuration (0x23E383) 默认帧时长(ns),可算帧率
│ ├── TrackLanguage (0x22B59C) ISO 639-2,如 "und"/"eng"/"chi"
│ ├── SeekPreRoll (0x56BB) 预卷时长(ns),Opus/CodecDelay相关
│ ├── CodecDelay (0x56AA) 编码器初始化延迟(ns)
│ ├── FlagLacing (0x9C) 是否允许Lacing(多帧打包)
│ ├── TrackVideo (0xE0) ← Video 轨道特有
│ │ ├── PixelWidth (0xB0) ★ 像素宽,如 1920
│ │ ├── PixelHeight (0xBA) ★ 像素高,如 1080
│ │ ├── DisplayWidth (0x54B0) 显示宽(DAR≠SAR时需)
│ │ ├── DisplayHeight (0x54BA) 显示高
│ │ ├── FlagInterlaced (0x9A) 0=逐行, 1=隔行
│ │ ├── StereoMode (0x53B8) 3D 模式
│ │ ├── Colour (0x55B0) ← HDR/色彩信息
│ │ │ ├── Primaries (0x55BB) 色域,如 9=BT.709
│ │ │ ├── TransferCharacteristics (0x55BA) 传输函数,如 16=PQ
│ │ │ ├── MatrixCoefficients (0x55B1) 矩阵,如 9=BT.709
│ │ │ ├── Range (0x55B9) 0=limited, 1=full
│ │ │ ├── MaxCLL (0x55BC) 最大内容亮度(cd/m²)
│ │ │ ├── MaxFALL (0x55BD) 最大帧平均亮度
│ │ │ └── MasteringMetadata (0x55D0) 母版元数据
│ │ │ ├── Primary R/G/B 色度坐标
│ │ │ ├── WhitePoint 白点色度
│ │ │ └── LuminanceMax/Min 亮度范围
│ │ └── AlphaMode (0x53C0) Alpha 通道标志
│ ├── TrackAudio (0xE1) ← Audio 轨道特有
│ │ ├── SamplingFrequency (0xB5) ★ 采样率,默认8000
│ │ ├── Channels (0x9F) ★ 声道数,默认1
│ │ ├── BitDepth (0x6264) 位深,如 16/24
│ │ └── OutputSamplingFrequency (0x78B5) 输出采样率(重采样后)
│ └── ContentEncodings (0x6D80) ← 加密/压缩轨道特有
│ └── ContentEncoding (0x6240)
│ ├── ContentEncodingType (0x5033) 0=压缩, 1=加密
│ ├── ContentCompression (0x5034)
│ │ └── ContentCompAlgo (0x4254) 0=zlib, 3=header-strip
│ └── ContentEncryption (0x5035) WebM: AES-CTR
│
├── Cues (0x1C53BB6B) — 随机访问索引 ★★ Seek 依赖
│ └── CuePoint (0xBB)
│ ├── CueTime (0xB3) 时间戳(TimecodeScale单位)
│ └── CueTrackPositions (0xB7)
│ ├── CueTrack (0xF7) 轨道编号
│ ├── CueClusterPosition (0xF1) ★ 簇相对Segment起始的偏移
│ └── CueBlockNumber (0x5378) 簇内块编号(默认1)
│
├── Attachments (0x1941A469) — 附件(封面图等)
│ └── AttachedFile (0x61A7)
│ ├── FileDescription (0x467E)
│ ├── FileName (0x466E) 如 "cover.jpg"
│ ├── FileMimeType (0x4660) 如 "image/jpeg"
│ └── FileData (0x465C) 二进制数据
│
├── Chapters (0x1043A770) — 章节导航
│ └── EditionEntry (0x45B9)
│ └── ChapterAtom (0xB6)
│ ├── ChapterUID (0x73C4)
│ ├── ChapterTimeStart (0x91) ★ 章节起始时间(ns)
│ ├── ChapterTimeEnd (0x92) 章节结束时间(ns)
│ ├── ChapterFlagHidden (0x98) 是否隐藏
│ ├── ChapterFlagEnabled (0x4598) 是否启用
│ └── ChapterDisplay (0x80) 章节标题(可多语言)
│ ├── ChapString (0x85) 标题文本
│ └── ChapLanguage (0x437C) 语言代码
│
├── Tags (0x1254C367) — 元数据标签
│ └── Tag (0x7373)
│ ├── Targets (0x63C0)
│ │ ├── TargetTypeValue (0x68CA) 50=专辑, 30=轨道, 20=章节
│ │ └── TargetTrackUID (0x63C5) 关联轨道 UID
│ └── SimpleTag (0x67C8) — 可递归嵌套
│ ├── TagName (0x45A3) 如 "TITLE"/"ARTIST"
│ ├── TagString (0x4487) 文本值
│ └── TagLanguage (0x447A) 如 "eng"
│
└── Cluster (0x1F43B675) — 媒体数据 ★★★ 文件主体,可有数千个
├── Timecode (0xE7) ★ 簇基准时间戳(TimecodeScale单位)
├── Position (0xA7) 簇在段中的位置(调试用)
├── PrevSize (0xAB) 前一簇字节大小 ★ 反向遍历/Seek关键
├── SimpleBlock (0xA3) ★ 简单块(含keyframe标志,无ReferenceBlock)
│ └── 数据: TrackNum(VINT) + Timecode(int16) + Flags(1B) + Frames
│ Flags: bit7=keyframe, bit5=invisible, bit3=discardable, bit[1:0]=lacing
└── BlockGroup (0xA0) ★ 块组(含参考帧/附加信息)
├── Block (0xA1) 块数据(格式同SimpleBlock但无keyframe位)
├── BlockDuration (0x9B) 块时长(TimecodeScale单位)
├── ReferenceBlock (0xFB) ★ 非关键帧标记(值为参考帧时间偏移)
├── DiscardPadding (0x75A2) 丢弃填充(ns,有符号)
├── CodecState (0xA4) 编码器状态切换
└── BlockAdditions (0x75A1) ★ 附加数据(DolbyVision元数据等)
└── BlockMore (0xA6)
├── BlockAddID (0xEE) 附加数据ID, 1=默认
└── BlockAdditional (0xA5) 附加数据内容
关键区别: MP4 中 moov 可在文件头或尾;Matroska 中 SeekHead 也在头或尾,Cues 通常在尾部以支持流式写入。
二、关键技术要点
完整的元素 ID、子元素结构见上方 1.3 节的树形图。本节仅补充开发调试中需特别关注的技术细节。
2.1 时间计算 ★★★
这是 A/V 同步和 Seek 的根基,所有时间公式汇总:
帧时间戳(ns) = (cluster_timecode + block_timecode) × time_scale × track_timecodescale
总时长(ns) = Duration × TimecodeScale
帧率(Hz) = 1,000,000,000 / DefaultDuration (当 DefaultDuration 存在时)
簇起始时间 = Cluster.Timecode × TimecodeScale
cluster_timecode: Cluster 元素中的 Timecode 字段,int16,相对 Segment 起始block_timecode: Block/SimpleBlock 中的 Timecode 字段,int16,相对簇时间time_scale: SegmentInfo.TimecodeScale,默认 1,000,000 (= 1ms/刻度)track_timecodescale: TrackTimecodeScale,默认 1.0,几乎不被使用- 注意: block_timecode 是有符号 int16,可为负数(B 帧重排场景)
代码对应: lace_time = cluster_time + block_time + lace_offset
2.2 关键帧判定 ★★
| 块类型 | 关键帧判定方式 |
|---|---|
| SimpleBlock | Flags bit 7 = 1 |
| BlockGroup | 无 ReferenceBlock 元素 = 关键帧 |
- SimpleBlock 的 keyframe 位由 muxer 设置,不可靠时应结合编码器判断
- BlockGroup 中 ReferenceBlock 的值是参考帧的时间偏移量,非帧序号
- Seek 后必须从关键帧开始解码,否则花屏
2.3 Lacing 解码要点 ★
Lacing 将多帧打包到一个 Block,减少容器开销。解码流程:
1. 读取 Lace Count (1字节, 值 = 实际帧数 - 1)
2. 根据 Lacing 类型读取每帧大小:
Xiph: 逐字节读,0xFF 表示继续累加;最后一帧 = 剩余字节
EBML: 第一帧 = VINT 无符号; 后续帧 = 前帧大小 + 有符号增量
Fixed: 每帧 = Block总数据大小 / 帧数
3. 依次读出每帧数据
- Lacing 仅用于音频和低码率视频,H.264/HEVC 等通常不用
- Xiph Lacing 编码与 Ogg/Xiph 容器一致
- 常见坑: EBML Lacing 后续帧大小是有符号增量,可能为负数
2.4 Cues 与 Seek ★★
CueClusterPosition = 簇相对 Segment 数据起始的偏移(字节)
查找流程: 二分搜索 common->index → 找到 ≤ 目标时间的条目 → seek 到簇偏移
- Cues 是簇级别索引,不是帧级别——seek 后需线性扫描到目标帧
- 无 Cues 时: Pull 模式可用
search_pos()二分搜索文件;Push 模式不可 seek scan_back_for_keyframe_cluster()用 PrevSize 字段回溯前一簇,确保从关键帧开始
2.5 内容编码解码 ★
ContentEncodings 可对帧数据做压缩或加密,解码顺序由 ContentEncodingOrder 决定:
| 类型 | 算法 | 说明 |
|---|---|---|
| 压缩 | header-strip (3) | 最常用:剥离固定前缀,解码时补回 comp_settings |
| 压缩 | zlib (0) / bzlib (1) | 通用压缩,开销大,罕见 |
| 压缩 | lzo1x (2) | 快速压缩,代码中有 lzo.c 实现 |
| 加密 | AES-CTR (5) | WebM 加密标准,signal byte + IV + 分区 |
信号字节(signal byte)格式:
bit 7: E=1 加密, E=0 明文
bit 6: P=1 分区加密(subsample), P=0 整帧
E=1 时: 紧跟 8字节 IV → 解密 → 去padding
P=1 时: IV 后读 subsample count + 各 subsample 的 clear/encrypted 长度
2.6 CodecPrivate 常见格式 ★★
CodecPrivate 是编码器私有数据,不同 CodecID 格式各异:
| CodecID | CodecPrivate 内容 |
|---|---|
| V_MPEG4/ISO/AVC | AVCC box (SPS/PPS) |
| V_MPEGH/ISO/HEVC | HEVCConfig (VPS/SPS/PPS) |
| V_VP9 | 可空 / VP9CodecPrivate |
| V_AV1 | AV1CodecConfigurationRecord (OBU) |
| A_AAC | AudioSpecificConfig (2-5字节) |
| A_VORBIS | Xiph 编码的 3 个 header packets |
| A_OPUS | OpusHead (19字节) |
| A_FLAC | FLAC METADATA_BLOCK_HEADER + 数据 |
三、端到端播放流程
从打开文件到出画面的完整链路,将零散知识串成全局视角:
1. 打开文件
└── 读 EBML Header → 得到 DocType(matroska/webm) + 版本号
2. 定位元数据
└── 读 SeekHead → 知道 Tracks/Info/Cues 各自在文件中的位置
├── Pull 模式: 直接 seek 到各位置解析
└── Push 模式: 等上游推送,记录 offset 供后续使用
3. 解析全局信息
└── SegmentInfo → time_scale(默认1ms) + Duration(总时长)
└── Tracks → 每个轨道的 CodecID/CodecPrivate/分辨率/采样率/语言
├── 根据 TrackType 创建对应上下文 (Video/Audio/Subtitle)
├── 根据 CodecID 映射 GStreamer Caps
├── 创建 Pad (video_%u / audio_%u / subtitle_%u)
└── 发送 STREAM_START + caps + tags
4. 解析索引(Pull 模式优先)
└── Cues → 簇级索引表 (时间戳 → 簇偏移)
├── 有 Cues: 二分搜索定位 Seek 目标
└── 无 Cues: Pull 可二分搜索文件; Push 不可 Seek
5. 进入数据循环
└── 逐 Cluster 读取:
├── Timecode → 簇基准时间
├── SimpleBlock / BlockGroup → 逐帧提取
│ ├── 读取 TrackNum + Timecode + Flags
│ ├── Lacing 解包 (如有) → 得到各帧数据
│ ├── 关键帧判定 → SimpleBlock 看 bit7, BlockGroup 看有无 ReferenceBlock
│ ├── 内容编码解码 → header-strip 补前缀 / AES-CTR 解密
│ └── 计算帧时间戳: (cluster_tc + block_tc) × time_scale
└── gst_pad_push() → 下游解码器
6. Seek 处理
└── 收到 seek event:
├── Pull: 二分搜索 index → seek 到簇 → 用 PrevSize 回溯找关键帧 → 从关键帧开始推送
└── Push: 无索引则先进入 SEEK 状态解析 Cues → BYTE seek 上游 → 重新进入数据循环
7. 结束
└── 所有轨道 EOS → 发送 EOS event → 暂停 task
关键路径上的性能瓶颈:
- 元数据解析(步骤2-4): Cues 在文件尾部时需额外 seek,Push 模式需等上游推送
- 帧时间戳计算(步骤5): 每帧都要算,是热路径
- Seek 定位(步骤6): 簇级索引 → 线性扫描到目标帧,大簇时延迟高
三、CodecID 映射
Matroska 使用字符串形式的 CodecID 标识编码格式。
3.1 视频 CodecID
| CodecID | GStreamer Caps | 说明 |
|---|---|---|
| V_MS/VFW/FOURCC | video/x-msvideocodec | VFW 兼容模式 |
| V_UNCOMPRESSED | video/x-raw | 未压缩 YUV |
| V_MPEG4/ISO/SP | video/mpeg mpegversion=4 | MPEG-4 SP |
| V_MPEG4/ISO/ASP | video/mpeg mpegversion=4 | MPEG-4 ASP |
| V_MPEG4/ISO/AP | video/mpeg mpegversion=4 | MPEG-4 AP |
| V_MPEG4/ISO/AVC | video/x-h264 | H.264/AVC |
| V_MPEGH/ISO/HEVC | video/x-h265 | H.265/HEVC |
| V_MPEG1 | video/mpeg mpegversion=1 | MPEG-1 |
| V_MPEG2 | video/mpeg mpegversion=2 | MPEG-2 |
| V_VP8 | video/x-vp8 | VP8 |
| V_VP9 | video/x-vp9 | VP9 |
| V_AV1 | video/x-av1 | AV1 |
| V_THEORA | video/x-theora | Theora |
| V_DIRAC | video/x-dirac | Dirac |
| V_PRORES | video/x-prores | ProRes |
| V_FFV1 | video/x-ffv | FFV1 |
| V_MJPEG | video/x-jpeg | MJPEG |
| V_QUICKTIME | video/x-quicktime | QuickTime 编码 |
| V_REAL/RV* | video/x-pn-realvideo | RealVideo |
| V_SNOW | (不常用) | Snow |
3.2 音频 CodecID
| CodecID | GStreamer Caps | 说明 |
|---|---|---|
| A_MPEG/L1/L2/L3 | audio/mpeg mpegversion=1 | MP1/MP2/MP3 |
| A_PCM/INT/BIG | audio/x-raw format=SxxBE | PCM 大端 |
| A_PCM/INT/LIT | audio/x-raw format=SxxLE | PCM 小端 |
| A_PCM/FLOAT/IEEE | audio/x-raw format=F32LE/F64LE | PCM 浮点 |
| A_AC3 | audio/x-ac3 | AC-3 |
| A_EAC3 | audio/x-eac3 | E-AC-3 |
| A_DTS | audio/x-dts | DTS |
| A_AAC | audio/mpeg mpegversion=4 | AAC |
| A_AAC/MPEG2/ | audio/mpeg mpegversion=2 | AAC MPEG-2 |
| A_AAC/MPEG4/ | audio/mpeg mpegversion=4 | AAC MPEG-4 |
| A_VORBIS | audio/x-vorbis | Vorbis |
| A_FLAC | audio/x-flac | FLAC |
| A_OPUS | audio/x-opus | Opus |
| A_SPEEX | audio/x-speex | Speex |
| A_TRUEHD | audio/x-truehd | TrueHD |
| A_TTA1 | audio/x-tta | TTA |
| A_WAVPACK4 | audio/x-wavpack | WavPack |
| A_MS/ACM | audio/x-ms-codec | ACM 编码 |
| A_REAL/* | audio/x-pn-realaudio | RealAudio |
| A_QUICKTIME/QDM* | audio/x-quicktime | QuickTime 音频 |
3.3 字幕 CodecID
| CodecID | GStreamer Caps | 说明 |
|---|---|---|
| S_TEXT/UTF8 | text/x-raw format=utf8 | UTF-8 文本 |
| S_TEXT/ASCII | text/x-raw format=utf8 | ASCII (当UTF8处理) |
| S_TEXT/SSA | text/x-ssa | SSA |
| S_TEXT/ASS | text/x-ass | ASS |
| S_TEXT/USF | text/x-usf | USF |
| S_VOBSUB | video/x-dvd-subpicture | VobSub |
| S_HDMV/PGS | video/x-hdmv-presentation-graphic-stream | PGS |
| S_IMAGE/BMP | application/x-subtitle-unknown | BMP 图片字幕 |
| S_KATE | subtitle/x-kate | Kate |
四、代码架构
4.1 状态机
┌──────────────────────┐
│ START │
│ 等待 EBML Header │
└──────┬───────────────┘
│ EBML Header
▼
┌──────────────────────┐
│ SEGMENT │
│ 等待 Segment 元素 │
└──────┬───────────────┘
│ Segment ID
▼
┌──────────────────────┐
┌───────│ HEADER │
│ │ 解析头元素 │
│ │ (Info/Tracks/Cues) │
│ └──────┬───────────────┘
│ │ Cluster 出现
│ ▼
│ ┌──────────────────────┐
│ ┌───▶│ DATA │
│ │ │ 处理 Cluster/Block │◀─── Seek 完成
│ │ └──────┬───────────────┘
│ │ │ Push模式需要Cues
│ │ ▼
│ │ ┌──────────────────────┐
│ │ │ SEEK │
│ │ │ 等待 Cues 解析完成 │──────┘
│ │ └──────────────────────┘
│ │
│ │ ┌──────────────────────┐
│ └────│ SCANNING │
│ │ 错误恢复,寻找Cluster│
└───────└──────────────────────┘
状态转换在 gst_matroska_demux_parse_id() (~line 6170) 中实现:
| 当前状态 | 事件 | 目标状态 |
|---|---|---|
| START | EBML Header | SEGMENT |
| SEGMENT | Segment 元素 | HEADER |
| HEADER | 遇到 Cluster | DATA |
| DATA | Push seek 无索引 | SEEK |
| SEEK | Cues 解析完成 | DATA |
| DATA/HEADER | 解析错误 | SCANNING |
| SCANNING | 找到 Cluster | DATA |
| 任何 | 遇到 EBML Header | SEGMENT |
五、核心流程
5.1 初始化与模式选择
gst_matroska_demux_init()
├── gst_matroska_read_common_init() — 初始化共享上下文
├── 创建 sink pad (chain/activatemode/event/query)
├── gst_flow_combiner_new()
└── gst_matroska_demux_reset() — 初始化所有状态字段
gst_matroska_demux_sink_activate()
└── gst_pad_check_pull_range()
├── 支持 → 激活 PULL 模式 ( GstTask 驱动 gst_matroska_demux_loop )
└── 不支持 → 激活 PUSH 模式 ( gst_matroska_demux_chain 回调 )
5.2 Pull 模式主循环
gst_matroska_demux_loop() (~line 6571)
│
├── while (1):
│ ├── 如果 state==DATA 且 pending new_segment → 发送
│ ├── 读取下一个 EBML 元素 ID + Size
│ ├── gst_matroska_demux_parse_id(demux, id, length, ...)
│ │ ├── START: 仅接受 EBML Header
│ │ ├── SEGMENT: 仅接受 Segment
│ │ ├── HEADER: 解析 SeekHead/Info/Tracks/Cues
│ │ ├── DATA: 处理 Cluster/Block
│ │ └── SEEK/SCANNING: 特殊处理
│ ├── 所有 pad EOS → 发 EOS, 暂停 task
│ └── 反向播放处理
5.3 Push 模式数据流
gst_matroska_demux_chain(pad, buffer) (~line 6757)
│
├── DISCONT 标记 → adapter 标记 discont
├── gst_adapter_push(adapter, buffer)
│
└── while (有足够数据):
├── state==DATA: 从 adapter 中解析 Cluster 级元素
├── 其他状态: peek EBML id+length,不够则 break
├── gst_matroska_demux_parse_id(...)
└── 解析错误 → 进入 SCANNING 状态
返回 gst_flow_combiner_update() 聚合结果
5.4 轨道解析流程
gst_matroska_demux_parse_tracks() (~line 3811)
│
└── 对每个 TrackEntry:
gst_matroska_demux_parse_stream() (~line 881)
│
├── 分配 GstMatroskaTrackContext (基类)
├── 遍历 TrackEntry 子元素:
│ ├── TrackType → 重分配为 Video/Audio/Subtitle 上下文
│ ├── Video 子元素 → 像素/显示尺寸、隔行、色彩
│ ├── Audio 子元素 → 采样率、声道、位深
│ ├── ContentEncodings → 压缩/加密信息
│ ├── CodecPrivate → 编码器私有数据
│ └── BlockAdditionMapping → DolbyVision 元数据
│
├── 根据 TrackType 调用 caps 函数:
│ ├── Video → gst_matroska_demux_video_caps()
│ ├── Audio → gst_matroska_demux_audio_caps()
│ └── Subtitle → gst_matroska_demux_subtitle_caps()
│
└── gst_matroska_demux_add_stream()
├── 创建 GstPad (video_%u / audio_%u / subtitle_%u)
├── 设置 event/query 处理函数
├── 发送 STREAM_START 事件
├── gst_pad_set_caps()
├── 发送 tags (language, title 等)
└── gst_element_add_pad() + gst_flow_combiner_add_pad()
5.5 Block 解析与帧推送
gst_matroska_demux_parse_blockgroup_or_simpleblock() (~line 4792)
│
├── 读取 TrackNumber (VINT)
├── 读取 Timecode (int16, 相对簇时间)
├── 读取 Flags (keyframe/invisible/lacing/discardable)
│
├── Lacing 处理:
│ ├── 读取 lace count
│ ├── Xiph: 逐字节读取,0xFF 表示继续累加
│ ├── EBML: 第一帧大小为 VINT, 后续为有符号增量
│ └── Fixed: 均分
│
├── BlockGroup 子元素:
│ ├── BlockAdditions → parse_blockadditions()
│ ├── BlockDuration
│ ├── DiscardPadding (纳秒)
│ └── ReferenceBlock (非关键帧)
│
└── 逐帧处理:
├── 计算 lace_time = cluster_time + block_time + lace_offset
├── 关键帧判断:
│ ├── SimpleBlock: flags bit 7
│ └── BlockGroup: 无 ReferenceBlock = 关键帧
├── gst_matroska_decode_buffer() — 解密/解压
├── 设置 buffer timestamps/duration/flags
└── gst_pad_push() → gst_flow_combiner_update()
5.6 Seek 流程
Pull 模式 Seek
gst_matroska_demux_handle_seek_event() (~line 3174)
│
├── 解析 seek event (format, flags, start/stop)
├── gst_segment_do_seek() 计算新 segment
│
├── 索引查找:
│ gst_matroska_read_common_do_index_seek()
│ → 二分搜索 common->index,找到 <= 目标时间的条目
│
├── 无索引时:
│ gst_matroska_demux_search_pos() — 二分搜索
│ ├── first_cluster ... last_cluster
│ ├── 中点读 Cluster 时间戳
│ └── 缩小范围直到收敛
│
├── scan_back_for_keyframe_cluster() — 用 PrevSize 回溯
│
├── FLUSH seek: 发送 flush_start/flush_end
├── 更新 segment,设置 new_segment_pending
└── perform_seek_to_offset() → BYTE seek 上游
Push 模式 Seek
gst_matroska_demux_handle_seek_push() (~line 3494)
│
├── 索引未解析:
│ ├── 进入 SEEK 状态
│ ├── 保存 seek_event
│ └── seek 到 CUES 偏移处解析索引
│
└── 索引已解析:
├── 查找索引条目
├── 创建 BYTE seek event 推向上游
└── 更新 segment
5.7 内容编码解码
gst_matroska_decode_data(encodings, data, size, scope)
│
└── 对每个匹配 scope 的编码:
├── 压缩类型 (type=0):
│ gst_matroska_decompress_data()
│ ├── ZLIB: inflate()
│ ├── BZLIB: BZ2_bzDecompress()
│ ├── LZO1X: lzo1x_decode()
│ └── HEADERSTRIP: 前缀 comp_settings + 原始数据
│
└── 加密类型 (type=1):
gst_matroska_parse_protection_meta()
├── 读 signal byte: E(encrypted) P(partitioned)
├── E=0: 未加密,直接返回
├── E=1: 读 8-byte IV
└── P=1: 读分区信息 → subsample 格式
5.8 Cues 解析
gst_matroska_read_common_parse_index() (~line 1857)
│
├── 分配 common->index (GArray, 初始128条)
│
└── 对每个 CuePoint:
parse_index_pointentry()
├── 读 CueTime → 转换: time * time_scale
└── parse_index_cuetrack()
├── CueTrack (轨道编号)
├── CueClusterPosition (簇偏移)
└── CueBlockNumber (块编号)
→ 去重后追加到 index
│
├── 排序 index (按时间)
├── 分发到各轨道的 index_table
└── common->index_parsed = TRUE
六、Pull 模式 vs Push 模式对比
| 特性 | Pull 模式 | Push 模式 |
|---|---|---|
| 入口函数 | gst_matroska_demux_loop (GstTask) |
gst_matroska_demux_chain (回调) |
| 数据源 | gst_pad_pull_range — 随机访问 |
GstAdapter — 顺序推送 |
| SeekHead | 立即 seek 到 Cues 位置解析 | 仅记录 index_offset 供后续使用 |
| 无索引Seek | search_pos() 二分搜索 |
不支持 |
| 大数据块 | 可 skip (seek 跳过) | 致命错误 |
| 错误恢复 | 可 seek 回退 | 进入 SCANNING 状态重新同步 |
| 模式选择 | sink_activate 检测 gst_pad_check_pull_range |
Pull 不可用时的回退 |
| 读取缓存 | cached_buffer (64KB 最小粒度) |
GstAdapter (上游推送) |
| seek 延迟 | 可直接处理 | 可能需先进入 SEEK 状态解析 Cues |
七、性能优化
7.1 I/O 层优化
64KB 预读缓存 (Pull 模式)
peek_bytes() 维护 cached_buffer,命中时直接返回子缓冲区引用,不触发 I/O。未命中时一次拉取 MAX(请求大小, 64KB),避免逐字节读取。
零拷贝子缓冲区
gst_ebml_read_buffer() 用 gst_buffer_copy_region() 创建共享父缓冲区内存的子缓冲区,不拷贝数据。
文件长度缓存
cached_length 只查询一次上游,后续复用,避免重复 GST_QUERY_DURATION。
Adapter 及时释放 (Push 模式)
gst_matroska_demux_flush() 每次处理后调用 gst_adapter_flush() 释放已消费字节,防止内存持续增长。
7.2 查找优化
索引二分搜索 O(log N)
gst_util_array_binary_search() 在排序后的 common->index 中查找 seek 目标,替代线性扫描。优先使用 per-track index_table,回退到全局 index。
簇偏移二分搜索
gst_matroska_demux_search_cluster() 对 demux->clusters 数组做二分查找,快速定位目标簇。
无索引时二分搜索文件
gst_matroska_demux_search_pos() 在无 Cues 时用插值+二分法定位目标簇:
- 用
gst_util_uint64_scale()在已知簇之间做线性插值估算位置 - 5 秒内即视为"足够近",停止搜索
- 簇大小已知时直接跳过整个簇,不逐元素解析
Lace 级帧跳过
解析 Block 时,如果帧时间戳早于当前需要的最早时间,用 per-track index 找到下一个关键帧,直接跳过该帧(goto next_lace),避免无谓解码。
7.3 解析优化
已解析元素跳过
segmentinfo_parsed / tracks_parsed / index_parsed 标志位确保 SegmentInfo、Tracks、Cues 等只解析一次,再次遇到直接 flush 跳过。
未知元素快速跳过
gst_ebml_read_skip() 只读 ID+Size,不读 Data,直接推进偏移。
Seek 块跳过
gst_matroska_demux_seek_block() 在 seek 到目标簇后,逐块递减计数直到目标块,非目标块走 goto skip 路径。
簇 ID 快速扫描
错误恢复时用 gst_byte_reader_masked_scan_uint32() 扫描 4 字节 Cluster ID (0x1F43B675),比逐元素解析快得多。
7.4 内存保护
MAX_BLOCK_SIZE (15MB)
gst_matroska_demux_check_read_size() 拒绝超过 15MB 的单次读取,防止损坏文件导致内存溢出。Pull 模式可跳过,Push 模式为致命错误。
MAX_DECOMPRESS_SIZE
zlib/bzlib 解压时,输出超过阈值则中止,防止解压炸弹。
INVALID_DATA_THRESHOLD (2MB)
Push 模式错误恢复时最多扫描 2MB 寻找下一个 Cluster,超过则放弃,防止死循环。
max_backtrack_distance (30秒)
Seek 回溯找关键帧时,最多往回搜 30 秒,防止从文件头开始扫描。
GArray 预分配
簇偏移数组 g_array_sized_new(100) 预分配 100 条空间,EBML reader 栈预分配 10 层,减少 realloc。
7.5 分支预测提示
热路径用 G_LIKELY / G_UNLIKELY 标注:
G_LIKELY(state == DATA)— 大部分时间在数据状态G_UNLIKELY(seek_block)— seek 是低频操作G_UNLIKELY(bytes > MAX_BLOCK_SIZE)— 超大块极少出现
7.6 性能优化总结
| 优化类型 | 关键机制 | 效果 |
|---|---|---|
| I/O 减少次数 | 64KB 预读缓存 + 文件长度缓存 | 减少 pull_range 调用 |
| 查找加速 | 二分搜索(索引/簇/文件) | O(N) → O(log N) |
| 解析减少 | 已解析跳过 + 未知元素 skip + 帧级跳过 | 避免重复/无用解析 |
| 内存安全 | 15MB 块上限 + 解压上限 + 2MB 扫描上限 + 30s 回溯上限 | 防止损坏文件导致 OOM |
| 拷贝消除 | 零拷贝子缓冲区 + Adapter 及时释放 | 减少内存分配和拷贝 |
| 分支优化 | G_LIKELY/G_UNLIKELY | CPU 分支预测命中率提升 |
八、与 MP4 的设计取舍对比
不只是格式差异,更关键的是理解为什么这样设计:
| 维度 | MKV | MP4 | 设计意图 |
|---|---|---|---|
| 索引粒度 | 簇级(Cues → Cluster) | 帧级(stbl 表 → 每帧) | MKV 为流式写入优化(边录边写不需回填);MP4 为随机访问优化(精确 seek) |
| 时间基准 | 全局 time_scale + 簇 + 块三层 | 每轨独立 timescale | MKV 简单(一个刻度管全局);MP4 精确(每轨可独立调精度) |
| 编码标识 | 字符串 CodecID(如 “V_MPEG4/ISO/AVC”) | 4字节 FourCC(如 “avc1”) | MKV 可扩展性好(新编码直接写字符串);MP4 需注册,但解析快 |
| 元数据位置 | SeekHead 头+尾均可,Cues 通常在尾 | moov 头或尾均可 | 都支持流式写入,但 MP4 尾部 moov 播放前需移动 atom(faststart) |
| 帧压缩 | 内置 header-strip/zlib/lzo | 无 | MKV 面向低带宽场景(字幕/低码率音频)减少容器开销 |
| 加密 | WebM AES-CTR(单一方案) | cenc/sinf(多种方案,Widevine/PlayReady 等) | MKV 简单但生态弱;MP4 复杂但 DRM 生态成熟 |
| 多轨道复用 | 同一 Cluster 可交错多轨道 | 每个 trak 独立存储 | MKV 交错利于流式播放(音视频就近);MP4 分离利于独立处理 |
| 无索引 Seek | Pull 模式可二分搜索文件 | 需遍历 stbl | MKV 容错性好(即使 Cues 缺失仍可播放);MP4 更依赖索引完整性 |
| 变长 vs 定长 | VINT 变长 ID + Size | 固定 4 字节 Box | MKV 灵活(小元素省空间);MP4 解析快(偏移可计算) |
一句话总结设计哲学:MKV 用灵活性和简洁性换取了设备兼容性和 DRM 生态——这在开源社区场景下是合理的取舍。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)