【免费下载链接】filmcraft
An open-source, clean-room reimplementation of Adobe Premiere Pro built in pure Rust.
filmcraft-matroska是 FilmCraft(一个用纯 Rust 重写的开源 NLE)中的 Matroska(MKV)与 WebM 容器层:它依据公开规范 RFC 8794(EBML)、RFC 9559(Matroska)以及 Matroska 编解码映射与 WebM 容器指南净室实现了一个完整的解复用器和一个最小封装器。读完本文,你将掌握该 crate 的两套 API(索引式open/MkvFile与流式Demuxer)、时间戳与 lacing 的底层规则、CodecID 到解码器配置的映射方式,以及它对未知尺寸元素、CRC 校验和文件损坏的处理策略。
定位:一个"Layer L0"的净室容器实现
该 crate 的边界在 crates/matroska/src/lib.rs 顶部有明确声明,并在 crates/matroska/Cargo.toml 中得到印证:
- 零依赖(Layer L0):Cargo.toml 中
[dependencies]为空,只依赖std;开发期依赖仅filmcraft-testkit和serde_json(oracle 测试解析ffprobe输出用); - 无
unsafe:lib.rs 还在非测试构建下deny了unwrap_used、expect_used、panic等 clippy 规则; - 可编译到
wasm32-unknown-unknown,因此 Web 版应用(apps/filmcraft-web)同样能跑在这套容器层上; - 净室声明:实现未参考 GPL/LGPL 代码(FFmpeg、libmatroska、mkvtoolnix);FFmpeg 只作为外部测试 oracle 使用(对比
ffprobe的逐 packet 输出)。
crate 的模块划分为:ebml.rs(VINT、元素头、类型化值、CRC-32、lacing)、demux.rs(打开文件、Cluster 扫描、packet 读取与 seek)、track.rs(轨道/采样表)、codec.rs(CodecID 映射)、meta.rs(Info/Cues/Chapters/Tags/Attachments)、mux.rs(封装器)、source.rs(ByteSource抽象)、ids.rs(元素 ID)。对外的公共 API 集中在 lib.rs,包括Codec、Demuxer、MkvFile、Packet、open/open_with、MkvWriter、Track/Sample以及element_ids(供调用方检查原始元素)。
解复用入口:open/open_with与 OpenOptions
MkvFile是"一次性打开、索引优先"的读取模型,README 给出的示例:
use filmcraft_matroska::{Demuxer, TrackKind, open}; // Sample tables, same shape as filmcraft-isobmff (any ByteSource: &[u8], Vec<u8>, Arc<[u8]>, File…) let file = std::fs::File::open("clip.mkv")?; let mkv = open(&file)?; let v = mkv.track_of_kind(TrackKind::Video).unwrap(); let k = mkv.keyframe_before(v, 2_000_000_000).unwrap(); // sample index let frame = mkv.read_sample(&file, v, k)?; // Streaming packets + seeking (from a ByteSource, any Read + Seek, or a byte slice) let mut d = Demuxer::from_reader(std::io::BufReader::new(std::fs::File::open("clip.webm")?))?; let sp = d.seek(v, 2_000_000_000)?; // lands on the keyframe with pts ≤ 2 s while let Some(p) = d.next_packet()? { // file order, all tracks // p.track, p.pts (ticks), p.pts_ns, p.duration, p.keyframe, p.data }OpenOptions:两个开关控制打开行为
OpenOptions定义在 demux.rs,默认index: true, verify_crc: false:
| 选项 | 默认 | 行为 |
|---|---|---|
index | true | 打开时扫描每一个Cluster,但只读元素头与块头、从不读帧负载,从而为每条轨道建立Sample表(offset、size、pts、duration、keyframe、cluster、block offset、lace index)和关键帧列表。设为false时打开到第一个 Cluster 即停,后续 seek 走 Cues 或在首次使用时补建索引 |
verify_crc | false | 校验 level-1 元素(以及索引过程中 Cluster)的 CRC-32,失配不致命,记录到MkvFile::crc_errors |
从源码看(Walker::walk,demux.rs),打开过程的行为比文档更具体:
- EBML 头定位容忍前导垃圾:在文件前 64 KiB 内滑动搜索魔数
[0x1A, 0x45, 0xDF, 0xA3](demux.rs); - 校验
DocType必须为matroska或webm,拒绝EBMLMaxIDLength > 4或EBMLMaxSizeLength > 8; - Segment 之后的 level-1 元素按文件顺序行走;遇到第二个
EBML/SEGMENT(链式 Segment)即停止——只读第一个 Segment; - 第一个 Cluster 出现时会主动
follow_seek_head去取INFO/TRACKS(保证块解释前元数据已知),index: false时随即停止(demux.rs); - 行走结束后还会按 SeekHead 补齐位于 Cluster 之后的
CUES/CHAPTERS/TAGS/ATTACHMENTS,甚至支持"二级 SeekHead 指向再一个 SeekHead"(demux.rs); - 单元素读入内存有 256 MiB 上限(
MAX_META),超限元素记录警告并跳过。
MkvFile结构(demux.rs)暴露的字段包括:doc_type/版本、segment_data_start(SeekHead 与 Cues 位置的基准)、segment_end、live(Segment 尺寸字段为 unknown,即直播/流式输出)、info、tracks、seek_head、cues(按时间排序)、clusters、chapters、tags、attachments、first_cluster、indexed、crc_checked、crc_errors和warnings(所有非致命问题)。
采样表、按需读帧与关键帧定位
索引式 API 的核心是每轨道的Sample表(track.rs),其形状刻意与filmcraft-isobmff保持一致,使调用方能用同一套代码处理 MP4 与 MKV。常用辅助方法:
track_of_kind(kind)(demux.rs):返回第一条指定类型轨道,优先default标志位;keyframe_before(track, time_ns)(demux.rs):返回 pts 最大且不超过time_ns的关键帧采样下标(仅对已索引文件);read_sample(src, track, index)(demux.rs):按采样表定位(offset, size),读出帧数据并在前面拼回被剥离的头部(见"内容编码"一节);轨道带压缩/加密编码时直接返回Unsupported;duration_ns()(demux.rs):Info/Duration优先,否则用最后一个索引采样的结束时间。
流式解复用:Demuxer
Demuxer(demux.rs)在文件顺序上迭代Packet,支持三种输入:任意ByteSource(字节切片、Vec<u8>、File等)、任意Read + Seek流(from_reader)、内存切片(from_slice)。它内部维护一个 64 KiB 读窗口(WIN),通过peek/read_vec小窗口式地前进,避免把大文件载入内存。
Packet 的字段语义
Packet(demux.rs)的每个字段都有明确约定:
track:在MkvFile::tracks中的下标;track_number:MatroskaTrackNumber;pts:以 tick 为单位(时间基为Track::timebase);pts_ns:精确纳秒;duration/duration_ns:未知时为 0;keyframe、discardable、invisible:来自块标志;discard_padding_ns:来自外层 BlockGroup 的DiscardPadding;offset:存储帧数据的绝对偏移;lace:laced 块内的帧序号(非 laced 为 0);data:已还原 header stripping 的帧字节;带其他内容编码(zlib/加密)的轨道按原样返回(见下文)。
next_packet()(demux.rs)内部循环:先吐 pending 队列,队列空则fill()前进元素直到产生一帧;Segment 末尾返回None。Demuxer还实现了Iterator<Item = Result<Packet>>(demux.rs),出错后停止迭代。此外提供rewind()(回到第一个 Cluster)、build_index()(未索引时全量扫描,已索引则无操作)、keyframe_index(track)(必要时补建索引,返回Keyframe { sample, pts, pts_ns, block_offset }列表)和read_sample()。
Seek 的三级策略
Demuxer::seek(track, time_ns)(demux.rs)的语义是:把 packet 流定位到该轨道中 pts ≤ time_ns 的最后一个关键帧(若更早无关键帧则用轨道第一个),之后next_packet()从该关键帧所在块开始按文件顺序吐所有轨道的 packet。实现按可用性分三级:
- 采样索引:若文件已索引,直接查
keyframe_before_pts,得到采样下标、Cluster 偏移与块偏移,position_at就位; - Cues:未索引但文件有 Cues 时,取 pts ≤ 目标时间的最晚 Cue(找不到则取第一个),跳到 Cue 指向的 Cluster 并扫描该 Cluster找出确切的块级关键帧位置(
seek_via_cue,demux.rs);若 Cue 指向的不是 Cluster(如位置偏移错),记录警告并降级; - 现建索引:无 Cues(典型场景:管道写入的直播文件)则
build_index()全量扫描后再定位。
时间戳体系:ticks、timebase 与 CodecDelay
- Matroska 的 tick 时长为
Info/TimestampScale纳秒(惯例为 1 ms);Track::timebase就是该值化简后的秒分数(gcd化简见 demux.rs)。pts以 tick 计,pts_ns为精确纳秒,换算函数为 track.rs 的ticks_to_ns/ns_to_ticks; - CodecDelay 一律从时间戳中扣除(RFC 9559 语义,与 FFmpeg 一致):Opus/Vorbis 的 priming 帧因此会得到负 pts;
- laced 帧的时间戳:第 i 帧 =
block ts + i × (BlockDuration 或 DefaultDuration)/n;当两者都未知时,后续 lace 重复块时间戳且duration == 0,由Packet::lace告知帧序号。这一逻辑在expand()中实现(demux.rs):总时长按比例切分到各帧,tick 侧用"半出"舍入(远离零),并且 tick 时间戳保持在块网格上(CodecDelay 只舍入一次)。
Lacing:三种模式的解析
块级 lacing 模式取自标志字节的 bit 1–2(ebml.rs 的Lacing::from_flags),parse_lacing(ebml.rs)把 laced 块载荷切分为各帧的(offset, len):
- Xiph:每帧前一个变长尺寸,字节累加直到小于 255(恰为 255 倍数的尺寸需要一个终结 0,单测
lacing_xiph专门覆盖了这条边界); - FixedSize:载荷除首字节外必须被帧数整除,否则报错;
- EBML:首帧为普通 VINT,其余为有符号 VINT 增量(
read_svint,ebml.rs,值域−(2^(7·len−1)−1) .. +(2^(7·len−1)−1)),出现负尺寸即判为损坏; - 尾部帧尺寸自动补为"剩余全部";任何尺寸总和超出块大小都返回错误而不是越界读。
关键帧判定
两种块类型规则不同:
- SimpleBlock:标志位
0x80为关键帧(解析于 demux.rs,同字节还有0x01discardable、0x08invisible); - BlockGroup:当且仅当不含
ReferenceBlock子元素时是关键帧(demux.rs)。BlockGroup 的BlockDuration会传给 laced 帧做时间切分,DiscardPadding记入 packet。
未知尺寸、损坏处理与 CRC-32
- 未知尺寸:Segment(直播/流式输出)和 Cluster 允许尺寸字段为"全 1"(unknown),此时元素在下一个顶层元素处结束;
MkvFile::live标记 Segment 是否为这种文件。unknown_end(demux.rs)沿子元素前进,直到遇到顶层 ID 或不可解码处为止; - 损坏重同步:遇到不可解码的元素头或声明尺寸越界的元素时,从损坏位置起做一次字节扫描,寻找"看起来合理"的 level-1 元素——判据是:ID 属于顶层集合、头可解码且不越界、Cluster 的首个有效子元素必须是
Timestamp(跳过CRC-32/Void)、其他 metadata 元素首个子元素可解码即可(plausible,demux.rs)。截断文件末尾的不完整块被直接丢弃并记录警告;所有问题都进入MkvFile::warnings,解析继续; - CRC-32:ebml.rs 实现标准 IEEE 多项式(0xEDB88320)的查表 CRC-32,
verify_crc检查主元素的第一个子元素是否为 4 字节CRC-32(比较时覆盖其后全部数据)。校验只对verify_crc: true时执行,失配记入crc_errors与非致命警告,不打断解析。
轨道元数据
Track解析覆盖(README 列表,实现见 meta.rs 与 track.rs):
- 通用:number/UID/type/flags、
DefaultDuration、名称、语言(BCP 47)、CodecID、CodecPrivate、CodecName、CodecDelay、SeekPreRoll、ContentEncodings; VideoInfo:像素/显示尺寸、裁剪、隔行、立体模式、alpha 模式,以及pixel_aspect()(宽高比);Colour:矩阵、范围(含full_range()便捷判断)、传递特性、 primaries、子采样/位置、MaxCLL/MaxFALL,以及 SMPTE 2086 HDR mastering 元数据(MasteringMetadata);AudioInfo:采样率、输出采样率、声道数、位深;- 附加元素:
Chapters(Edition)、Tags(SimpleTag 树)、Attachments——附件只登记并记录FileData的偏移/大小,从不读入载荷(demux.rs)。
Codec 映射:从 CodecID 到解码器配置
map_codec(codec.rs)把CodecID+CodecPrivate+ 音频参数映射为Codec枚举;ISO 系 codec 的配置记录就是 ISOBMFF 配置框的载荷(avcC/hvcC/av1C/ASC),可直接喂给仓库内对应的解码 crate。完整映射表:
| CodecID | Codec | 载荷 |
|---|---|---|
V_MPEG4/ISO/AVC | Avc | avcC(CodecPrivate) |
V_MPEGH/ISO/HEVC | Hevc | hvcC |
V_VP8,V_VP9 | Vp8,Vp9 | VP9 CodecPrivate 特性 |
V_AV1 | Av1 | av1C |
V_PRORES | ProRes | FourCC;帧数据补回 8 字节size+icpf头 |
V_MJPEG,V_MS/VFW/FOURCC(MJPG) | Mjpeg | |
V_MS/VFW/FOURCC(其他) | VfwFourcc | BITMAPINFOHEADER |
A_AAC,legacyA_AAC/MPEG{2,4}/… | Aac | AudioSpecificConfig(legacy ID 无 CodecPrivate 时合成,含 SBR) |
A_OPUS | Opus | OpusHead |
A_VORBIS | Vorbis | 从 Xiph-laced CodecPrivate 拆出的三个头 |
A_FLAC | Flac | fLaC+ 元数据块 |
A_PCM/INT/LIT,A_PCM/INT/BIG,A_PCM/FLOAT/IEEE | Pcm | 位深取自 Audio 元素 |
A_AC3,A_EAC3,A_MPEG/L3,A_MPEG/L2 | Ac3,Eac3,Mp3,Mp2 | |
S_TEXT/UTF8 | SubRip | |
S_TEXT/WEBVTT,D_WEBVTT/*(WebM) | WebVtt | WebM 块保留id\nsettings\npayload结构 |
S_TEXT/ASS,S_TEXT/SSA | Ass | 脚本头 |
| 其他 | Other(id) | 保留Track::codec_private |
源码比表格更细的几处:
- VFW FOURCC 特判:读 BITMAPINFOHEADER 的
biCompression字段(FourCC),MJPG/mjpg/AVRn/AVDJ归为Mjpeg;H264/avc1/X264归为Avc;apch/apcn/apcs/apco/ap4h/ap4x归为ProRes;apv1归为Apv;其余落VfwFourcc(codec.rs); - legacy AAC 合成 AudioSpecificConfig:
A_AAC/MPEG{2,4}/…且无 CodecPrivate 时,按 profile(MAIN/LC/SSR/LTP)取 object type,用采样率/声道数合成 2 字节 ASC;/SBR后缀追加向后兼容的显式 SBR 扩展(0x56, 0xE5, ext rate 字节,synth_asc,codec.rs); - Vorbis 头拆分:CodecPrivate 本身是 Xiph-laced,
split_xiph拆成 identification/comment/setup 三个头; - ProRes 帧补头:ProRes 在 MKV 中存裸压缩数据,读帧时通过
Track::frame_prefix拼回size+icpf8 字节头(track.rs); Codec::name()(codec.rs)对这些 codec 返回与 FFmpegcodec_name一致的名字(pcm_s24le、subrip等),便于日志与 UI。
内容编码
- Header stripping(
ContentEncoding中Compression = none+Sliced的头部剥离):在所有 packet 与read_sample中自动还原,对上层透明; - zlib/bzlib/lzo 压缩与加密:只检测并报告——
Track::frames_readable()返回false,packet 按存储原样返回,read_sample报Unsupported,不做解码。
封装:最小 Muxer
MkvWriter(mux.rs)的调用链是:
MkvWriter::new(w: Write + Seek, Vec<TrackSpec>, MuxOptions) → write_frame(track, pts_ns, keyframe, data, duration_ns) → finish()TrackSpec由TrackSpec::new(kind, codec_id)构造,再填充 CodecPrivate 等描述信息(mux.rs);- 写入结构:EBML 头、Segment、SeekHead、带
Duration的 Info、Tracks,随后是 SimpleBlock 组成的 Cluster——每经过MuxOptions.cluster_duration_ns时长后,在下一个视频关键帧处开新 Cluster;带显式 duration 的帧写进 BlockGroup;支持三种音频 lacing(LacingMode::Xiph/Ebml/FixedSize); finish()补写Cues(视频关键帧,或每条轨道在每个 Cluster 的首帧);- 明确不写:CRC-32、Chapters、Tags、Attachments、内容编码。
测试:单元测试 + FFmpeg oracle
单元层(ebml.rs、codec.rs、synthetic_tests.rs):VINT 已知/未知尺寸与 ID、有符号 lace 增量、类型化值(uint/int/float/string)、CRC-32 已知向量(crc32(b"123456789") == 0xCBF43926)、三种 lacing 模式全部边界(255 的整数倍、越界、负尺寸)、codec 映射(含 legacy AAC 44.1k stereo →0x12 0x10)、以及一份手工构造的文件——覆盖未知尺寸 Segment/Cluster、header stripping、BlockGroup、Colour + mastering 元数据、Chapters/Tags/Attachments、CRC-32 通过/失败、无 Cue 的 seek;另有每种 lacing 模式的 mux → demux 往返。
Oracle 层(tests/oracle.rs,无 ffmpeg/ffprobe 时自动跳过;夹具位于target/fixtures/matroska):
- 覆盖样本:H.264+AAC、VP9+Opus WebM、FLAC、Vorbis、SRT+ASS 字幕、WebVTT WebM、Cue 在文件前部、经管道写入(无 Cue)的流式文件、带 HDR 颜色的 HEVC、ProRes+PCM、MJPEG+AC-3、AV1;
- 逐 packet 对比
ffprobe -show_packets(流号、大小、关键帧标志、pts、duration),再加 codec 名、timebase、尺寸、音频参数、总时长;采样索引必须与 packet 流严格一致; - seek 三种路径(索引、仅 Cues、现建索引)都必须落在"≤ 目标时间的最新关键帧"上;
- 本 crate 自己的 muxer 产出的 laced 文件(Xiph/EBML/fixed)由 ffprobe 回读并做同样对比;
- 损坏文件必须能重同步并在损坏点之后继续出包;截断文件不得报错。
已知限制
README 明确列出的边界(与源码行为一致):
- 只读第一个 Segment:不支持链接/链式 Segment,也不支持 ordered-chapter editions 播放;
- zlib/bzlib/lzo 压缩与加密轨道能检测,但帧不解码;
BlockAdditions(如 VP9 alpha、Matroska 里的 WebVTT settings)被跳过;EncryptedBlock被忽略;- 已弃用的
TrackTimestampScale被忽略; - 完全无 duration 信息时,laced 帧时间戳重复块时间戳(FFmpeg 是靠各 codec 解析器推导的,这里不做 codec 级推导)。
综合来看,这个 crate 的价值在于:以零依赖、无 unsafe 的纯 Rust 代码,把 EBML 解析(VINT/lacing/CRC)、MKV 元数据、laced 时间戳、损坏恢复与最小封装收敛成两层清晰 API——索引式MkvFile(随机访问、与 MP4 层同形)和流式Demuxer(文件顺序出包 + 关键帧 seek),并用 FFmpeg oracle 把"逐 packet 与 ffprobe 一致"作为可执行的正确性标准。
【免费下载链接】filmcraft
An open-source, clean-room reimplementation of Adobe Premiere Pro built in pure Rust.
相关推荐
filmcraft-h264:FilmCraft 纯 Rust 干净室 H.264/AVC 解码器的架构与实现解析
filmcraft h264:FilmCraft 纯 Rust 干净室 H.264/AVC 解码器的架构与实现解析 本篇以 crates/h264/README
FilmCraft 的 HEVC 解码器(filmcraft-hevc):纯 Rust 零 unsafe 的 H.265 清洁室实现
FilmCraft 的 HEVC 解码器(filmcraft hevc):纯 Rust 零 unsafe 的 H.265 清洁室实现 filmcraft hev
filmcraft-isobmff:用纯 Rust 从零实现 MP4/MOV 解复用与复用(ISO/IEC 14496 家族)
filmcraft isobmff:用纯 Rust 从零实现 MP4/MOV 解复用与复用(ISO/IEC 14496 家族) filmcraft isobmf
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考