oxideav-mkv 0.0.9

Pure-Rust Matroska (MKV/WebM) container for oxideav
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
# oxideav-mkv

Pure-Rust **Matroska (MKV)** and **WebM** container — demuxer + muxer
built on the EBML primitives from RFC 8794. Zero C dependencies.

Part of the [oxideav](https://github.com/OxideAV/oxideav-workspace)
framework but usable standalone.

## Installation

```toml
[dependencies]
oxideav-core = "0.1"
oxideav-codec = "0.1"
oxideav-container = "0.1"
oxideav-mkv = "0.0"
```

## Quick use

Register both containers (`"matroska"` and `"webm"`) and let the probe
pick which DocType the file carries:

```rust
use oxideav_container::ContainerRegistry;

let mut containers = ContainerRegistry::new();
oxideav_mkv::register(&mut containers);

let input: Box<dyn oxideav_container::ReadSeek> = Box::new(
    std::fs::File::open("movie.mkv")?,
);
let mut dmx = containers.open_demuxer("matroska", input)?;
for s in dmx.streams() {
    println!("track {}: {}", s.index, s.params.codec_id.as_str());
}
loop {
    match dmx.next_packet() {
        Ok(p) => { /* feed p into a decoder from oxideav-codec */ }
        Err(oxideav_core::Error::Eof) => break,
        Err(e) => return Err(e.into()),
    }
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

The demuxer returns raw `Packet` bytes — pair it with a decoder crate
(e.g. [`oxideav-opus`](https://crates.io/crates/oxideav-opus),
[`oxideav-flac`](https://crates.io/crates/oxideav-flac),
[`oxideav-vp9`](https://crates.io/crates/oxideav-vp9)) or go through
the unified `oxideav` aggregator to wire decoding automatically.

## What's implemented

### Demuxer (`demux::open`)

- EBML header parse, DocType validation (`matroska` / `webm`).
- Segment walk: `Info`, `Tracks`, `Tags`, `Cues`, `Cluster`. Known- and
  unknown-size Segment/Cluster both supported.
- Clusters: `SimpleBlock` and `BlockGroup -> Block`, all three lacing
  modes (Xiph, fixed, EBML-signed-delta).
- Metadata lift: title, muxer, encoder, date (Matroska `DateUTC` ->
  ISO-8601), Tags `SimpleTag` name/value pairs with **target-scope
  resolution** (`Tags.Targets.TagTrackUID` -> `tag:track:N:<name>`,
  `TagChapterUID` -> `tag:chapter:N:<name>`, `TagAttachmentUID` ->
  `tag:attachment:N:<name>`, `TagEditionUID` -> `tag:edition:N:<name>`;
  all-zero UIDs -> bare `<name>` global key; unresolved non-zero UIDs
  are dropped per RFC 9559 §5.1.8.1.1.x "MUST match"), `Chapters`
  (`chapter:N:start_ms` / `:end_ms` / `:title`, ns→ms), and
  `Attachments` (`attachment:N:filename` / `:mime_type` / `:size_bytes`;
  payload is skipped, only the index surfaces).
- **Typed `Tag` accessor**: `demux::open_typed` returns the concrete
  `MkvDemuxer`, whose `.tags() -> &[Tag]` exposes RFC 9559 §5.1.8.1
  fields the flat metadata view drops — `TargetType` /
  `TargetTypeValue` informational hints, multi-UID `Targets` masters
  (one `Tag` can scope to several tracks/chapters at once), per-
  `SimpleTag` `TagLanguage` / `TagLanguageBCP47` / `TagDefault`,
  and binary `TagBinary` payloads (e.g. embedded cover-art bytes).
  Tags with only dangling non-zero UIDs are filtered out per
  §5.1.8.1.1.3..§5.1.8.1.1.6; mixed Targets keep their resolvable UIDs.
- **`Targets::target_level()` typed hierarchy** (RFC 9559 §5.1.8.1.1.1,
  Table 33): `Targets::target_level() -> Option<TargetLevel>` resolves
  the raw `target_type_value` integer into the typed `TargetLevel`
  enum (`Shot=10` / `Subtrack=20` / `Track=30` / `Part=40` /
  `Album=50` / `Edition=60` / `Collection=70`, plus `Other(u64)` for
  values registered under the §27.13 "Matroska Tags Target Types"
  registry after RFC 9559). The enum derives `Ord` in spec-containment
  order so a player can walk the album → track → subtrack hierarchy
  without re-comparing raw integers — the §5.1.8.1.1.1 usage note
  ("Higher values MUST correspond to a logical level that contains
  the lower logical level TargetTypeValue values") falls straight out
  of `Ord`. `Other(_)` sorts after every named level so a future entry
  doesn't break the comparison rule for the named ones. Returns `None`
  when the `TargetTypeValue` element was absent on disk —
  distinguishable from `Some(TargetLevel::Album)` (the spec default
  `50` materialised by a writer). Inverse `TargetLevel::to_raw()`
  round-trips every named variant + the `Other(u64)` forward-compat
  passthrough. Companion `TargetLevel::canonical_label()` returns the
  leftmost / most common Table 33 label for the level (e.g. `ALBUM`
  for value `50`, not the alternate `OPERA` / `CONCERT` / `MOVIE` /
  `EPISODE` labels); the file's own `TargetType` informational string
  stays on the existing `Targets::target_type` field — the typed level
  helper doesn't overwrite it.
- **Typed `TrackAudienceFlags` accessor** (RFC 9559 §5.1.4.1.6..§5.1.4.1.11):
  `MkvDemuxer::track_audience_flags(stream_index) -> Option<&TrackAudienceFlags>`
  (and the per-stream `all_track_audience_flags()` slice) folds the six
  per-`TrackEntry` audience hints — `FlagForced` (id `0x55AA`),
  `FlagHearingImpaired` (id `0x55AB`), `FlagVisualImpaired` (id `0x55AC`),
  `FlagTextDescriptions` (id `0x55AD`), `FlagOriginal` (id `0x55AE`),
  `FlagCommentary` (id `0x55AF`) — into one typed record per stream. Spec
  defaults are materialised asymmetrically: `forced()` returns a bare `bool`
  with the §5.1.4.1.6 default `0` always reflected (a `TrackEntry` with no
  `FlagForced` child decodes `false`); the five `minver: 4` flags carry no
  spec default and surface as `Option<bool>` so callers can distinguish
  "writer was silent" (`None`) from "writer explicitly cleared the flag"
  (`Some(false)`) — the §5.1.4.1.7..§5.1.4.1.11 wording ("Set to 1 *if and
  only if* …") makes that distinction load-bearing. Convenience predicates
  `is_default_presentation()` (no flag is `Some(true)`) and
  `is_accessibility()` (any of `hearing_impaired` / `visual_impaired` /
  `text_descriptions` is `Some(true)`) cover the common filter cases. Every
  track surfaces a record — `FlagForced`'s spec wording "applies only to
  subtitles" does not suppress the surface on audio / video tracks because
  the spec puts the elements on `TrackEntry` itself with `minOccurs: 1` for
  `FlagForced`; the typed surface trusts the caller to apply each flag
  where it makes sense for the track's `TrackType` / `CodecID`.
- **Typed `TrackAudio` accessor** (RFC 9559 §5.1.4.1.29.1..§5.1.4.1.29.4):
  `MkvDemuxer::track_audio(stream_index) -> Option<&TrackAudio>` (and the
  per-stream `all_track_audio()` slice) folds the four `Audio` sub-master
  children — `SamplingFrequency` (id `0xB5`, §5.1.4.1.29.1),
  `OutputSamplingFrequency` (id `0x78B5`, §5.1.4.1.29.2), `Channels`
  (id `0x9F`, §5.1.4.1.29.3), `BitDepth` (id `0x6264`, §5.1.4.1.29.4) —
  into one typed record. Spec defaults are materialised asymmetrically:
  `sampling_frequency()` returns a bare `f64` with the §5.1.4.1.29.1
  default `0x1.f4p+12` = `8000.0` always reflected (an `Audio` master with
  no explicit child still surfaces 8000.0 Hz, never `0.0`); `channels()`
  returns a bare `u64` with the §5.1.4.1.29.3 default `1` (mono) always
  reflected; `output_sampling_frequency()` folds Table 19's derived default
  (= `sampling_frequency()` when the element was absent) but
  `output_sampling_frequency_explicit()` preserves the on-disk presence as
  `Option<f64>` so a re-muxer doesn't materialise an element that wasn't
  in the source. `bit_depth()` stays `Option<u64>` — §5.1.4.1.29.4 defines
  no default, so absence is observable. Convenience predicate `is_sbr()`
  returns `true` exactly when the writer emitted an explicit
  `OutputSamplingFrequency` strictly greater than `SamplingFrequency` (the
  canonical SBR-doubling signal for HE-AAC and similar tracks). Records
  surface only for `TrackEntry`s that carried an `Audio` master at all:
  video / subtitle / button tracks (where the master is `maxOccurs: 1` but
  carries no `minOccurs` at the `TrackEntry` level) return `None`, as does
  a malformed audio track that emitted no `Audio` child — the typed
  surface never synthesises a record from the spec defaults alone.
- **Typed `TrackTiming` accessor** (RFC 9559 §5.1.4.1.13..§5.1.4.1.15):
  `MkvDemuxer::track_timing(stream_index) -> Option<&TrackTiming>` (and the
  per-stream `all_track_timing()` slice) folds the three `TrackEntry`-level
  timing elements — `DefaultDuration` (id `0x23E383`, §5.1.4.1.13),
  `DefaultDecodedFieldDuration` (id `0x234E7A`, §5.1.4.1.14), and
  `TrackTimestampScale` (id `0x23314F`, §5.1.4.1.15) — into one record per
  track. The elements sit directly on `TrackEntry` (no gating master), so
  every valid track surfaces a record; `track_timing` returns `None` only
  for an out-of-range stream index. `default_duration()` is the container's
  nominal nanoseconds-per-frame source — `TrackTiming::nominal_frame_rate()`
  derives fps (`1e9 / ns`), so e.g. a `41708333` ns track yields `~23.976`.
  Both nanosecond durations carry a "not 0" range and no spec default, so
  they stay `Option<u64>` and a spec-illegal explicit `0` is dropped at parse
  time. `track_timestamp_scale()` materialises the §5.1.4.1.15 default `1.0`
  while `track_timestamp_scale_explicit()` preserves the on-disk presence
  (a non-finite / non-positive payload is dropped, since the spec range is
  `> 0x0p+0`). `TrackTiming::is_empty()` reports the all-absent state — a
  track that carried none of the three elements.
- **Typed `TrackCodecTiming` accessor** (RFC 9559 §5.1.4.1.25 + §5.1.4.1.26):
  `MkvDemuxer::track_codec_timing(stream_index) -> Option<&TrackCodecTiming>`
  (and the per-stream `all_track_codec_timing()` slice) folds the two
  `TrackEntry`-level codec-timing elements — `CodecDelay` (id `0x56AA`,
  §5.1.4.1.25) and `SeekPreRoll` (id `0x56BB`, §5.1.4.1.26), both nanosecond
  (Matroska Tick) `uinteger`s — into one record per track. The elements sit
  directly on `TrackEntry` (no gating master), so every valid track surfaces a
  record; `track_codec_timing` returns `None` only for an out-of-range stream
  index. `codec_delay()` is the encoder's built-in delay (Opus pre-skip) the
  player MUST subtract from each frame timestamp; `seek_pre_roll()` is the
  audio the decoder MUST decode after a seek before its output is valid (Opus
  convention 80 ms). Unlike the `TrackTiming` durations, both elements carry
  spec default `0` and **no** "not 0" range, so an explicit on-disk `0` is a
  legal value distinct from "absent": the plain accessors materialise the `0`
  default while `codec_delay_explicit()` / `seek_pre_roll_explicit()` preserve
  the on-disk presence (a re-muxer can avoid emitting an element the source
  omitted). `TrackCodecTiming::is_empty()` reports the both-absent state — a
  track that emitted an explicit `0` for either element is *not* empty. The
  mux side already writes both on the Opus path (`CodecDelay` = `OpusHead`
  pre-skip in ns, `SeekPreRoll` = 80 ms).
- **Typed per-Cluster `Position` / `PrevSize` records** (RFC 9559
  §5.1.3.2 / §5.1.3.3): `MkvDemuxer::cluster_records() ->
  &[ClusterRecord]` surfaces each Cluster's optional `Position`
  (id `0xA7`, `uinteger`) and `PrevSize` (id `0xAB`, `uinteger`)
  children as they're walked. Records are appended in first-encounter
  order through `next_packet` / `seek_to`, with `body_offset` (the
  absolute file offset of the byte right after the Cluster's id+size
  header) as the dedup key — a back-then-forward seek that revisits
  the same Cluster doesn't push a duplicate row. Both typed fields
  are `Option<u64>`: `None` when the on-disk child was absent (common
  for `PrevSize` on the first Cluster of a Segment, and for both
  fields when a writer omitted them entirely), `Some(v)` when present.
  The `Some(0)` `Position` case is the §5.1.3.2 spec convention for
  live streams (Cluster offset not determined ahead of time) and is
  distinct from `None`. Consumers can verify a recorded `Position`
  matches the actual on-disk offset by subtracting `segment_data_start`
  + the Cluster's header length from `body_offset` (the §16
  Segment-Position definition), build a reverse walker on top of
  `PrevSize` without re-scanning the SeekHead, or detect a live stream
  by seeing `Some(0)` `Position` values. The slice grows incrementally
  as the demuxer walks the Segment — callers wanting the full
  per-Cluster set should drain the file via `next_packet` first.
- **Typed `Attachments` accessor** (RFC 9559 §5.1.6):
  `MkvDemuxer::attachments() -> &[Attachment]` returns one
  [`Attachment`] per `AttachedFile` parsed from the Segment, in document
  order. Each entry carries the 1-based `index` (matching the
  `attachment:N:*` flat metadata keys and any `tag:attachment:N:<name>`
  Tag scope), `filename` (`FileName`, §5.1.6.2), `mime_type`
  (`FileMimeType`, §5.1.6.3), `description` (`FileDescription`,
  §5.1.6.1), `uid` (`FileUID`, §5.1.6.5), and the on-disk byte range
  (`data_offset` + `data_size`) of the `FileData` payload. The payload
  bytes are **not** read up front — a multi-megabyte embedded font
  stays on disk until `MkvDemuxer::attachment_data(index)` is called,
  at which point exactly `data_size` bytes are read from `data_offset`
  and returned; the demuxer's reader position is preserved across the
  fetch so calling it between `next_packet` calls is safe. The flat
  `metadata()` view also gains an `attachment:N:description` key when
  the source element was present.
- **Typed `Chapters` accessor** (RFC 9559 §5.1.7):
  `MkvDemuxer::chapters() -> &[Edition]` exposes the structured chapter
  tree the flat `chapter:N:*` metadata view collapses — every
  `EditionEntry` keeps its `EditionUID`, `EditionFlagDefault` and
  `EditionFlagOrdered` flags; every `ChapterAtom` keeps its
  `ChapterUID`, `ChapterStringUID` (e.g. WebVTT cue id), full-precision
  `ChapterTimeStart` / `ChapterTimeEnd` nanoseconds, `ChapterFlagHidden`,
  `ChapterFlagEnabled` (spec default `1` materialised as `true`),
  Medium-Linking fields `ChapterSegmentUUID` (raw 16 B) +
  `ChapterSegmentEditionUID` (zero suppressed per spec "range: not 0"),
  `ChapterPhysicalEquiv` (DVD/SIDE physical mapping per §20.4),
  **all** multilingual `ChapterDisplay` rows (each with `ChapString`,
  `ChapLanguage` + `ChapLanguageBCP47`, `ChapCountry`), the `ChapProcess`
  sub-tree (RFC 9559 §5.1.7.1.4.14–19 — `ChapProcessCodecID`,
  `ChapProcessPrivate`, and zero or more `ChapProcessCommand` rows each
  with `ChapProcessTime` + raw `ChapProcessData`; payloads surfaced
  verbatim, never executed), and any nested
  child atoms (the spec marks `ChapterAtom` as recursive). Atoms are
  1-indexed depth-first in document order — the same index the flat
  `chapter:N:*` keys and `TagChapterUID`-resolved tags use, now extended
  to nested chapters. Returns an empty slice when the file has no
  `Chapters` element.
- Duration: `Segment\Info\Duration` translated to microseconds.
- Seek: `seek_to(stream, pts)` uses the Cues index. Handles Cues at
  either end of the Segment, and walks an unknown-size final Cluster to
  find Cues that sit past it.
- **`CueRelativePosition` honoured on seek** (RFC 9559 §5.1.5.1.2.3): when
  a Cues entry carries the `CueRelativePosition` element, `seek_to` opens
  the target Cluster, captures its `Timestamp` (RFC 9559 §5.1.3.1 — SHOULD
  be the first child), and then repositions the reader directly at the
  byte offset of the referenced `SimpleBlock` / `BlockGroup` (`0` being
  the first possible element position inside that Cluster). The next
  packet emitted is the cue's exact block, not the first block in the
  Cluster — finer seek granularity than the legacy "scan from cluster
  start" path, which is preserved as a fallback when the cue has no
  `CueRelativePosition` or the encoded position is out of range.
- **Typed `Cues` accessor** (RFC 9559 §5.1.5.1, including
  §5.1.5.1.1..§5.1.5.1.2.8 and the reclaimed Appendix A.37..A.39
  `CueReference` children): `MkvDemuxer::cue_points() -> &[CuePoint]`
  surfaces the full on-disk seek-index tree in document order. The
  `seek_to` path consumes a denormalised, sorted projection internally
  (track, time, cluster offset, relative position); `cue_points` instead
  preserves everything that projection collapses, so callers can read
  per-cue `CueDuration` (§5.1.5.1.2.4) and `CueBlockNumber`
  (§5.1.5.1.2.5), the `CueCodecState` (§5.1.5.1.2.6, spec default `0`
  materialised — `0` meaning "taken from the initial `TrackEntry`"), and
  walk the nested `CueReference` rows (§5.1.5.1.2.7 — each carrying
  `CueRefTime` plus the reclaimed `CueRefCluster` / `CueRefNumber` /
  `CueRefCodecState`), or re-mux the `Cues` element sub-element-for-sub-
  element. Each `CuePoint` pairs an absolute `CueTime` (in Segment Ticks
  — the file's `TimestampScale`, not microseconds) with one or more
  `CueTrackPositions` (the spec gives the latter `minOccurs: 1` with no
  `maxOccurs`, so a single timestamp can index blocks on several tracks).
  Populated whether `Cues` sits before the first Cluster or after the
  last (the late best-effort rescan feeds the same typed collector);
  optional children surface as `Option<u64>` (absent vs present), `0`-but-
  present and `0`-by-default `CueCodecState` are observationally identical
  per the spec default. Unknown children inside `CueTrackPositions` are
  skipped (forward-compat). Returns an empty slice when the file has no
  `Cues` element.
- An unknown-size Cluster is terminated cleanly when a sibling Segment-
  child element follows it (no more "Cues silently eaten as payload").
- **CRC-32 validation** (RFC 8794 §11.3.1, RFC 9559 §6.2): when a Top-Level
  master element (`Info`, `Tracks`, `Tags`, `Cues`, `Chapters`,
  `Attachments`, `SeekHead`) **or a `Cluster`** carries a leading `CRC-32`
  child, the demuxer recomputes the IEEE CRC-32 (reflected poly
  `0xEDB88320`, init `0xFFFFFFFF`, final XOR, little-endian storage) over
  the rest of the element and records the result.
  `MkvDemuxer::crc_status() -> &[CrcStatus]` exposes each
  `{element_id, stored, computed}` triple with an `is_valid()` helper.
  Up-front masters are checked at open time in segment order; Cluster
  checks land lazily on the first `next_packet` / `seek_to` that opens
  each Cluster (the element id on a Cluster status is `ids::CLUSTER`),
  with a body-offset dedup so a back-then-forward seek revisiting the
  same Cluster never produces two statuses for it. The late best-effort
  Cues rescan (the path the demuxer uses when `Cues` sits after the
  final `Cluster` — the common single-pass-mux layout, and the one our
  own muxer emits) also validates a leading `CRC-32` on the rediscovered
  `Cues` element and pushes its status, so a Cues CRC mismatch surfaces
  regardless of whether the `Cues` was placed before or after Clusters.
  A Cluster declared with the unknown-size VINT can't be CRC-checked
  (the spec requires a bounded body) and produces no status. Validation
  is informational — a mismatch does **not** abort the open (RFC 8794
  §12: a reader MAY ignore the data); strict callers reject any non-
  valid status. Elements with no `CRC-32` child produce no status
  (omission is spec-legal).
- **`TrackOperation` typed decode** (RFC 9559 §5.1.4.1.30): a *virtual*
  track assembled from other tracks. `MkvDemuxer::track_operation(stream_index)`
  (and the per-stream `track_operations()` slice) returns a typed
  `TrackOperation` for any `TrackEntry` carrying the element, `None` for an
  ordinary track. `TrackCombinePlanes` (§5.1.4.1.30.1) surfaces as a
  `Vec<TrackPlane>` — each pairs a referenced track with its
  `TrackPlaneType` (`LeftEye` / `RightEye` / `Background`, with `Other(u64)`
  preserving FCFS-registry values per §27.17) — and `TrackJoinBlocks`
  (§5.1.4.1.30.5) surfaces as a `Vec<TrackRef>`. Every `TrackPlaneUID` /
  `TrackJoinUID` is resolved back to a `TrackRef` carrying both the on-disk
  `TrackUID` and the matching 0-indexed stream index (`None` for a dangling
  reference, kept rather than dropped). A `TrackPlane` missing its mandatory
  `TrackPlaneUID` and a zero `TrackJoinUID` ("not 0" per spec) are dropped.
- **`BlockAdditionMapping` typed decode** (RFC 9559 §5.1.4.1.17):
  `MkvDemuxer::block_addition_mappings(stream_index)` (and the per-stream
  `all_block_addition_mappings()` slice) returns each
  `Tracks > TrackEntry > BlockAdditionMapping` master the file carries,
  in on-disk order, as a typed `BlockAdditionMapping` record exposing
  `value` (`BlockAddIDValue`, §5.1.4.1.17.1, `Option<u64>` — spec range
  `>=2`, no default), `name` (`BlockAddIDName`, §5.1.4.1.17.2,
  `Option<String>`), `addid_type` (`BlockAddIDType`, §5.1.4.1.17.3,
  `u64` — spec default `0` (codec-defined) materialised), and
  `extra_data` (`BlockAddIDExtraData`, §5.1.4.1.17.4, `Option<Vec<u8>>`
  — opaque per-track binary state the type interpreter consults). The
  helper `is_codec_defined()` reports whether `addid_type == 0` (the
  §5.1.4.1.17.3 usage-note case in which the matching `BlockAddID` must
  be `1`). Unknown child elements inside the master are skipped — the
  spec allows additions to the registry. Tracks with no
  `BlockAdditionMapping` child surface as an empty slice (the common
  case — the element only appears on tracks that use `BlockAdditional`
  to extend their on-disk format). The typed view declares the *shape*
  of the side channel; the per-frame `BlockAdditional` payload bytes
  themselves surface through the per-packet `block_additions()`
  accessor below, and payload semantics stay with the codec /
  track-format extension that owns each `BlockAddIDType` value.
- **Per-Block `BlockAdditions` typed decode** (RFC 9559 §5.1.3.5.2,
  including §5.1.3.5.2.1..§5.1.3.5.2.3) **+ `MaxBlockAdditionID`**
  (§5.1.4.1.16): `MkvDemuxer::block_additions() -> &[BlockAddition]`
  surfaces the side-channel payloads attached to the most recently
  returned packet — one typed `BlockAddition` per `BlockMore` in
  on-disk order, each pairing `block_add_id()` (`BlockAddID`,
  §5.1.3.5.2.3, spec default `1` = codec-defined materialised on
  omission) with the verbatim `data()` bytes (`BlockAdditional`,
  §5.1.3.5.2.2, never interpreted by the container — id `1` is e.g.
  the WebM alpha plane when the track's `AlphaMode` is `Present`; ids
  `>= 2` are described by the track's `BlockAdditionMapping`). The
  slice is empty for `SimpleBlock` packets (the element only exists on
  `BlockGroup`), for `BlockGroup`s without the master (the common
  case), before the first `next_packet`, and after a seek; every frame
  de-laced from one laced Block shares the Block's additions (the spec
  attaches the master to the Block as a whole). Malformed `BlockMore`s
  are dropped: a missing mandatory `BlockAdditional`, a `BlockAddID`
  of `0` (range "not 0"), and a duplicate `BlockAddID` (uniqueness
  MUST — first occurrence kept). The per-track declaration surfaces
  through `MkvDemuxer::max_block_addition_id(stream_index)` with the
  §5.1.4.1.16 spec default `0` ("there is no BlockAdditions for this
  track") materialised on absence.
- **`ContentEncodings` typed decode** (RFC 9559 §5.1.4.1.31):
  `MkvDemuxer::content_encodings(stream_index)` (and the per-stream
  `all_content_encodings()` slice) returns the track's transformation chain
  — compression and/or encryption applied to frame data / `CodecPrivate`
  before the bytes hit Blocks — as typed `ContentEncodings`, `None` for an
  ordinary track. Each `ContentEncoding` carries its `ContentEncodingOrder`,
  `ContentEncodingScope` bit field (`block()` / `private()` / `next()`
  accessors), and a `ContentEncodingTransform` enum: `Compression`
  (`ContentCompAlgo``Zlib` / `Bzlib` / `Lzo1x` / `HeaderStripping` /
  `Other(u64)`, plus the `ContentCompSettings` stripped bytes) or
  `Encryption` (`ContentEncAlgo``None` / `Des` / `TripleDes` / `Twofish`
  / `Blowfish` / `Aes` / `Other(u64)`, the `ContentEncKeyID`, and the
  nested `ContentEncAESSettings``AESSettingsCipherMode` as
  `Ctr` / `Cbc` / `Other(u64)`). The list is pre-sorted into *decode* order
  (highest `ContentEncodingOrder` first, per §5.1.4.1.31.2). Element
  defaults are honoured (order 0, scope 0x1 Block, type 0 compression,
  comp-algo 0 zlib). The headers are surfaced; zlib/bzlib/lzo1x and
  encryption are never decompressed or decrypted (out of container scope).
- **Header-Stripping applied on read** (RFC 9559 §5.1.4.1.31.6 algo 3,
  §5.1.4.1.31.7): Header Stripping is the one `ContentEncoding` transform
  the container can reverse without a codec — the `ContentCompSettings`
  bytes were removed from the front of each frame on write, so the demuxer
  prepends them back to every de-laced frame, and `next_packet` returns the
  original (un-stripped) frame data. Block scope (§5.1.4.1.31.3 bit 0x1) is
  honoured per-frame (the prefix lands on each laced sub-frame, not the
  Block once); a chain of several Header-Stripping steps is combined in
  decode order. If the Block-scoped chain contains any step the container
  can't undo (zlib/bzlib/lzo1x compression or encryption), packets pass
  through encoded — the demuxer never *partially* strips. Private-scope
  (`CodecPrivate`-only) Header Stripping leaves frame data untouched.
- **`Video` geometry quartet typed decode** (RFC 9559
  §5.1.4.1.28.8..§5.1.4.1.28.14):
  `MkvDemuxer::video_geometry(stream_index)` (and the per-stream
  `video_geometries()` slice) folds the `PixelCrop{Top,Bottom,Left,Right}`
  hide-window plus the `DisplayWidth` / `DisplayHeight` / `DisplayUnit`
  render-size triple into a single typed `VideoGeometry`. `DisplayUnit`
  surfaces as the `DisplayUnit` enum (`Pixels` / `Centimeters` / `Inches` /
  `DisplayAspectRatio` / `Unknown` / `Other(u64)` for forward-compat with
  the §27.9 "Matroska Display Units" registry). `display_width()` /
  `display_height()` return `Option<u64>`: the explicit element when the
  file carries it, otherwise the §5.1.4.1.28.12 / §5.1.4.1.28.13 derived
  default (`PixelWidth - PixelCropLeft - PixelCropRight` / `PixelHeight -
  PixelCropTop - PixelCropBottom`) — but only when `DisplayUnit == 0`
  (pixels), since the spec explicitly states "If the DisplayUnit of the
  same TrackEntry is 0, then the default value for DisplayWidth is ...;
  else, there is no default value". For any other `DisplayUnit` an absent
  element resolves to `None`. The PixelCrop defaults (`0`, §5.1.4.1.28.8..11)
  and DisplayUnit default (`0`, §5.1.4.1.28.14) are always materialised.
  Non-video tracks (and video tracks with no `Video` master) return `None`;
  a derivation that would underflow (malformed file with crops larger than
  the encoded width or height on the same axis) returns `None` on that
  axis rather than wrapping.
- **`Video > Colour` typed decode** (RFC 9559 §5.1.4.1.28.16, including
  §5.1.4.1.28.17..§5.1.4.1.28.40 sub-elements and the SMPTE 2086 /
  CTA-861.3 HDR `MasteringMetadata`):
  `MkvDemuxer::video_colour(stream_index)` (and the per-stream
  `video_colours()` slice) folds the `Colour` master's children into a
  single typed `VideoColour`. Each of `MatrixCoefficients`,
  `TransferCharacteristics`, `Primaries`, `ColourRange`,
  `ChromaSitingHorz` and `ChromaSitingVert` surfaces as a typed enum;
  forward-compat values outside the registered tables pass through
  via an `Other(u64)` variant (§27 leaves registries open for future
  additions). `BitsPerChannel`, `ChromaSubsampling{Horz,Vert}`,
  `CbSubsampling{Horz,Vert}`, `MaxCLL` / `MaxFALL` surface as the raw
  unsigned integer (Optional when the spec doesn't define a default).
  The nested `MasteringMetadata` (§5.1.4.1.28.30..§5.1.4.1.28.40)
  surfaces as `Option<&MasteringMetadata>` with the six
  `Primary{R,G,B}Chromaticity{X,Y}` floats, the two
  `WhitePointChromaticity{X,Y}` floats and the
  `Luminance{Max,Min}` cd/m² pair — each independently optional, since
  the spec does not require all-or-nothing. Spec defaults are
  materialised on the typed surface so an empty `Colour` master decodes
  as fully-typed *unspecified* (§5.1.4.1.28.17 / .26 / .27 default `2`;
  §5.1.4.1.28.23..25 default `0`). Non-video tracks (and video tracks
  with no `Colour` child) return `None`.
- **`Video > StereoMode` typed decode** (RFC 9559 §5.1.4.1.28.3):
  `MkvDemuxer::video_stereo_mode(stream_index) -> Option<StereoMode>`
  (and the per-stream `video_stereo_modes()` slice) returns the
  single-track stereo-3D packing — `Mono` / `SideBySide{Left,Right}First`
  / `TopBottom{Left,Right}First` / `Checkboard{Left,Right}First` /
  `RowInterleaved{Left,Right}First` /
  `ColumnInterleaved{Left,Right}First` / `Anaglyph{CyanRed,GreenMagenta}`
  / `BothEyesLaced{Left,Right}First` (the full §5.1.4.1.28.3 Table 5
  set) plus `Other(u64)` for values registered after RFC 9559 (§27.7
  leaves the registry open). The §5.1.4.1.28.3 default `0` (`Mono`) is
  materialised: a `Video` master with no explicit `StereoMode` decodes
  as `Some(StereoMode::Mono)`, distinguishable from `None` (which means
  "no `Video` master at all"). Multi-track stereo (`TrackOperation >
  TrackCombinePlanes`, §5.1.4.1.30.1) is independent and surfaces
  through `track_operation`; a single track MAY carry both. A convenience
  `StereoMode::is_stereo()` returns `true` for any non-`Mono` packing.
- **`Video > Projection` typed decode** (RFC 9559 §5.1.4.1.28.41,
  including §5.1.4.1.28.42..§5.1.4.1.28.46):
  `MkvDemuxer::video_projection(stream_index)` (and the per-stream
  `video_projections()` slice) folds the `Projection` master's children
  into a single typed `Projection`. `ProjectionType` surfaces as a typed
  enum (`Rectangular` / `Equirectangular` / `Cubemap` / `Mesh` /
  `Other(u64)` for values registered after RFC 9559 — §27.15 leaves the
  registry open). `ProjectionPrivate` (the verbatim ISOBMFF box body —
  `equi` / `cbmp` / `mshp` — that pairs with the projection type)
  surfaces verbatim as `Option<&[u8]>` and is never parsed or validated
  by the container; that's a renderer concern. The yaw / pitch / roll
  pose triple (degrees, ranges `±180 / ±90 / ±180` per §5.1.4.1.28.44..46)
  surfaces as three `f64`s with the spec default `0.0` materialised. An
  empty `Projection` master decodes as a fully-typed identity projection
  (rectangular + zero pose), distinguishable from `None` (which means
  "no `Projection` master at all" — the common case for ordinary 2D
  video). The §5.1.4.1.28.46 worked example
  `<Projection><ProjectionPoseRoll>90</ProjectionPoseRoll></Projection>`
  (signalling a 90° counter-clockwise rotation) round-trips with
  `projection_type == Rectangular`, `pose_roll == 90.0`, and the other
  pose components at their defaults. Convenience helpers
  `ProjectionType::is_spherical()` and `Projection::is_rotated()` provide
  the headline yes/no answers. Non-video tracks (and video tracks with no
  `Projection` child) return `None`.
- **`Video > AlphaMode` typed decode** (RFC 9559 §5.1.4.1.28.4):
  `MkvDemuxer::video_alpha_mode(stream_index) -> Option<AlphaMode>`
  (and the per-stream `video_alpha_modes()` slice) folds the per-track
  WebM-alpha hint into a typed enum (`None` / `Present` / `Other(u64)`
  for values registered after RFC 9559 — §27.8 leaves the registry
  open). The §5.1.4.1.28.4 default `0` (`None`) is materialised: a
  `Video` master with no explicit `AlphaMode` decodes as
  `Some(AlphaMode::None)`, distinguishable from `None` (which means "no
  `Video` master at all"). `AlphaMode::Present` (value `1`) signals
  that the track's `BlockAdditional` element with `BlockAddID=1` carries
  alpha-channel data per the codec mapping for `CodecID` (the WebM
  VP8/VP9 alpha extension is the canonical user). A convenience
  `AlphaMode::has_alpha()` returns `true` exactly for the `Present`
  variant — values outside Table 6 are conservatively treated as "no
  alpha" because the spec leaves their semantics implementation-defined.
- **`Video > AspectRatioType` typed decode** (RFC 9559 Appendix A.24,
  reclaimed): `MkvDemuxer::video_aspect_ratio_type(stream_index) ->
  Option<u64>` (and the per-stream `video_aspect_ratio_types()` slice)
  surfaces the raw `u64` value rather than synthesising an enum — the
  reclaimed appendix says only "Specifies the possible modifications to
  the aspect ratio" and enumerates no values. Returns `None` whenever
  the file did not carry the element (the appendix specifies no
  default, so absence is *not* materialised).
- **`Video > UncompressedFourCC` typed decode** (RFC 9559
  §5.1.4.1.28.15): `MkvDemuxer::video_uncompressed_fourcc(stream_index)
  -> Option<&UncompressedFourCC>` (and the per-stream
  `video_uncompressed_fourccs()` slice) surfaces the 4-byte FourCC that
  identifies the uncompressed pixel layout. Spec-mandatory only when
  `CodecID == "V_UNCOMPRESSED"` (Table 11); the typed surface carries
  the verbatim on-disk bytes via `as_bytes()`, plus convenience
  `fourcc() -> Option<[u8; 4]>` and `as_str() -> Option<String>` (UTF-8
  lossy) accessors that return `None` whenever the on-disk payload
  isn't exactly 4 bytes. A malformed non-4-byte payload is preserved
  verbatim rather than being dropped, so callers debugging a malformed
  file can still see what the writer emitted. Absence on any track is
  legal — the spec specifies no default — and returns `None`.
- **`Video > FlagInterlaced` + `FieldOrder` typed decode** (RFC 9559
  §5.1.4.1.28.1 + §5.1.4.1.28.2):
  `MkvDemuxer::video_interlacing(stream_index)` (and the per-stream
  `video_interlacings()` slice) folds both elements into a typed
  `VideoInterlacing``flag()` returns a `FlagInterlaced` enum
  (`Undetermined` / `Interlaced` / `Progressive` / `Other(u64)`) and
  `field_order()` returns `Some(FieldOrder)`
  (`Progressive` / `Tff` / `Undetermined` / `Bff` / `TffInterleaved` /
  `BffInterleaved` / `Other(u64)`) only when the track is actually
  interlaced. §5.1.4.1.28.2's "If FlagInterlaced is not set to 1, this
  element MUST be ignored" is honoured by the typed surface: a stray
  `FieldOrder` on a progressive / undetermined track silently resolves to
  `None`. Spec defaults materialised — bare `Video` master with no
  `FlagInterlaced` child decodes as `Undetermined` (default `0`); an
  interlaced track with no explicit `FieldOrder` decodes as
  `Some(FieldOrder::Undetermined)` (default `2`). Non-video tracks (and
  video tracks with no `Video` master) return `None`.

### Muxer (`mux::open` and `mux::open_webm`)

- EBML header + Segment (unknown size) for a streaming-friendly layout.
- Fixed-size `SeekHead` at the start of the Segment with Seek entries
  for `Info`, `Tracks`, and `Cues` - so players that pre-walk the
  SeekHead (mpv, Chromium) jump straight to Cues without scanning. The
  Cues `SeekPosition` is patched in `write_trailer`; if no packets were
  written, the Cues entry is rewritten as a Void filler.
- `Info` (1 ms `TimecodeScale`), `Tracks`, rolling ~5 s `Cluster`s with
  `SimpleBlock` payload.
- `Cues` element emitted in `write_trailer` - index entries for every
  video keyframe and every audio cluster-start, so the resulting file
  is seekable without a second pass. Each entry carries
  `CueRelativePosition` (RFC 9559 §5.1.5.1.2.3, recommended by §22.1)
  so seek-aware readers jump straight to the indexed `SimpleBlock`
  inside the Cluster instead of scanning from the cluster header.
- Codec-specific fields: `CodecPrivate` normalisation for FLAC (`fLaC`
  magic prepended), Opus `CodecDelay` derived from the `OpusHead`
  pre-skip plus an 80 ms `SeekPreRoll` per the WebM spec.
- `Chapters` (RFC 9559 §5.1.7): `MkvMuxer::add_chapter(start_ns,
  end_ns, title)` queues a single English-language `ChapterAtom`;
  `add_chapter_full(MkvChapter)` takes a fully-specified record with
  multilingual `ChapterDisplay` rows (`ChapString` + `ChapLanguage`
  + optional `ChapCountry`). Chapters must be added before
  `write_header`; the muxer emits a single `EditionEntry` between
  Tracks and the first Cluster and patches the SeekHead `Chapters`
  slot to point at it (slot is voided if no chapters were queued).
- `Attachments` (RFC 9559 §5.1.6): `MkvMuxer::add_attachment(MkvAttachment
  { filename, mime_type, data, uid, description })` queues one
  `AttachedFile`. Attachments must be added before `write_header`; the
  muxer emits the `Attachments` master right after `Chapters` (or
  directly after `Tracks` when no chapters are queued) and patches the
  SeekHead `Attachments` slot to point at it (slot is voided if no
  attachments were queued). Field handling matches the demux side
  field-for-field so an end-to-end demux→mux pipeline preserves
  attachments: `FileName` (§5.1.6.1.2) + `FileMediaType` (§5.1.6.1.3)
  are mandatory and rejected up front when empty; `FileUID` (§5.1.6.1.5,
  `range: not 0`) auto-derives from the 1-based attachment index when
  the caller passes `None`, and an explicit `Some(0)` is rejected;
  `FileDescription` (§5.1.6.1.1) is omitted on disk when `None` or
  empty. `MkvAttachment::new(filename, mime_type, data)` is a
  convenience constructor mirroring the demux-side typed surface.
- WebM profile: `mux::open_webm` pins `DocType="webm"` and rejects any
  stream whose codec isn't VP8/VP9/AV1 video or Vorbis/Opus audio with
  `Error::Unsupported`.
- **CRC-32 on Top-Level masters** (RFC 8794 §11.3.1, RFC 9559 §6.2):
  the muxer prepends a 6-byte `CRC-32` child (id `0xBF`, fixed size 4,
  little-endian IEEE CRC-32 of the rest of the element's data) to every
  Top-Level master it buffers end-to-end before flushing — `Info`,
  `Tracks`, `Cues`, plus `Chapters` and `Attachments` when those are
  queued. RFC 9559 §6.2 says "all Top-Level Elements of an EBML Document
  SHOULD include a CRC-32 element as their first Child Element," and the
  in-tree demuxer's `validate_top_level_crc` peel-off-leading-CRC rule
  verifies every emitted master round-trips to a matching stored /
  computed pair. `SeekHead` is deliberately not CRC'd — its Cues entry
  is patched in `write_trailer`, which would invalidate any CRC computed
  up front. `Cluster` is not CRC'd because the muxer streams Clusters
  with the unknown-size VINT and RFC 8794 §11.3.1 requires a bounded
  body for CRC.
- **`Video > FlagInterlaced` + `FieldOrder` on write** (RFC 9559
  §5.1.4.1.28.1 + §5.1.4.1.28.2): `MkvMuxer::set_video_interlacing(
  stream_index, FlagInterlaced, Option<FieldOrder>)` queues a per-track
  interlacing hint that lands inside the track's `Video` master at
  `write_header` time, alongside the existing `PixelWidth` /
  `PixelHeight`. The demux-side `FlagInterlaced` / `FieldOrder` enums
  gained `to_raw()` inverses so every Table 3 / Table 4 value
  round-trips, including the `Other(u64)` forward-compat variant on
  both. Spec rules enforced at queue time: the call rejects
  post-`write_header` use, out-of-range `stream_index`, non-video
  tracks, and `FieldOrder` paired with anything other than
  `FlagInterlaced::Interlaced` (the §5.1.4.1.28.2 "If FlagInterlaced is
  not set to 1, this element MUST be ignored" rule applied
  symmetrically on write). Omitting the call leaves both elements
  off-disk so the demuxer materialises the §5.1.4.1.28.1 default `0` /
  §5.1.4.1.28.2 default `2` (Undetermined). Pairs symmetrically with
  the existing `MkvDemuxer::video_interlacing` typed accessor — a
  mux→demux pipeline preserves the interlacing pair bit-exactly.
- **`Video` geometry quartet on write** (RFC 9559
  §5.1.4.1.28.8..§5.1.4.1.28.14):
  `MkvMuxer::set_video_geometry(stream_index, MkvVideoGeometry)` queues a
  per-track hint that lands inside the track's `Video` master at
  `write_header` time, alongside `PixelWidth` / `PixelHeight`. The hint
  carries `PixelCrop{Top,Bottom,Left,Right}` (§5.1.4.1.28.8..11),
  `DisplayWidth` / `DisplayHeight` (§5.1.4.1.28.12 / .13), and
  `DisplayUnit` (§5.1.4.1.28.14). The demux-side `DisplayUnit` enum
  gained a `to_raw()` inverse so every Table 10 value round-trips,
  including the `Other(u64)` forward-compat variant (§27.9 leaves the
  "Matroska Display Units" registry open). Per-element omission rules:
  zero crops stay off-disk (spec default `0`); `DisplayWidth` /
  `DisplayHeight` are written when `Some` and skipped when `None`;
  `DisplayUnit` is written explicitly only for non-`Pixels` values
  (omitting it lets the demuxer materialise the §5.1.4.1.28.14 spec
  default). Spec rules enforced at queue time: rejects post-`write_header`
  use, out-of-range `stream_index`, calls on non-video tracks, and
  `Some(0)` on either `display_width` / `display_height` per the
  §5.1.4.1.28.12 / .13 `range: not 0` pin. Convenience constructors
  `MkvVideoGeometry::cropped(top, bottom, left, right)` (RFC 9559 §11.1
  pillar-box / letterbox shape, no display-size override, `Pixels` unit)
  and `MkvVideoGeometry::aspect_ratio(num, den)`
  (`DisplayUnit::DisplayAspectRatio` + the ratio encoded as
  `DisplayWidth` / `DisplayHeight`) cover the two common shapes. Pairs
  symmetrically with the existing `MkvDemuxer::video_geometry` typed
  accessor — a mux→demux pipeline preserves the quartet bit-exactly,
  including the §5.1.4.1.28.12 / .13 derived-default behaviour when
  display dimensions were omitted on write and `DisplayUnit == Pixels`.
- **`Video > StereoMode` + `AlphaMode` on write** (RFC 9559
  §5.1.4.1.28.3 + §5.1.4.1.28.4):
  `MkvMuxer::set_video_stereo_mode(stream_index, StereoMode)` and
  `MkvMuxer::set_video_alpha_mode(stream_index, AlphaMode)` queue
  per-track hints that land inside the track's `Video` master at
  `write_header` time. The demux-side `StereoMode` and `AlphaMode`
  enums gained `to_raw()` inverses so every Table 5 / Table 6 value
  round-trips, including the `Other(u64)` forward-compat variant on
  both (§27.7 / §27.8 leave the "Matroska Stereo Modes" / "Matroska
  Alpha Modes" registries open). Spec rules enforced at queue time:
  both setters reject post-`write_header` use, out-of-range
  `stream_index`, and calls on non-video tracks. The two settings are
  independent — setting one does not affect the other. Omitting the
  call leaves the element off-disk so the demuxer materialises the
  §5.1.4.1.28.3 default `0` (`Mono`) / §5.1.4.1.28.4 default `0`
  (`None`). Calling `set_video_stereo_mode(_, StereoMode::Mono)` /
  `set_video_alpha_mode(_, AlphaMode::None)` explicitly still writes
  the element on disk — that is the way for a producer to override a
  downstream tool that might infer something else. Pairs symmetrically
  with the existing `MkvDemuxer::video_stereo_mode` /
  `MkvDemuxer::video_alpha_mode` typed accessors.
- **`Video > UncompressedFourCC` on write** (RFC 9559 §5.1.4.1.28.15):
  `MkvMuxer::set_video_uncompressed_fourcc(stream_index, [u8; 4])`
  queues a per-track FourCC hint that lands inside the track's
  `Video` master at `write_header` time (id `0x2EB524`, `binary`
  type, schema-fixed `length: 4`). The setter takes a `[u8; 4]` array
  directly, so the schema's fixed length is enforced at the type
  system; every byte (including high bytes and `0x00`) is written
  verbatim — the element is `binary`, not `string`, and the muxer
  never interprets the payload as text. Spec rules enforced at queue
  time: the setter rejects post-`write_header` use, out-of-range
  `stream_index`, and calls on non-video tracks. Omitting the call
  leaves the element off-disk so the demuxer's
  `MkvDemuxer::video_uncompressed_fourcc` surfaces `None`  §5.1.4.1.28.15 defines no default, and Table 11's `minOccurs=1`
  only fires for `CodecID == "V_UNCOMPRESSED"`, which the muxer does
  not presently emit. Pairs symmetrically with the existing
  `MkvDemuxer::video_uncompressed_fourcc` typed accessor — a
  mux→demux pipeline preserves the four-byte FourCC bit-exactly.
- **`Video > AspectRatioType` on write** (RFC 9559 Appendix A.24,
  reclaimed, id `0x54B3`): `MkvMuxer::set_video_aspect_ratio_type(
  stream_index, u64)` queues a per-track hint that lands inside the
  track's `Video` master at `write_header` time as a plain `uinteger`
  element. The reclaimed appendix documents the element only as
  "Specifies the possible modifications to the aspect ratio" and
  enumerates no values and no default, so the setter takes the raw
  `u64` verbatim — mirroring the demux side, which deliberately
  surfaces it as a raw `Option<u64>` rather than a synthesised enum.
  Per-element omission rule: the element is written only when the
  caller opts in; an explicit `0` is written and round-trips as
  `Some(0)` (distinct from absence, since the appendix defines no
  default). Spec rules enforced at queue time: the setter rejects
  post-`write_header` use, out-of-range `stream_index`, and calls on
  non-video tracks. Omitting the call leaves the element off-disk so
  the demuxer's `MkvDemuxer::video_aspect_ratio_type` surfaces `None`.
  Pairs symmetrically with the existing
  `MkvDemuxer::video_aspect_ratio_type` typed accessor — a mux→demux
  pipeline preserves the raw value bit-exactly. This closes the last
  remaining `Video` sub-element that the demux side read but the mux
  side could not write.
- **`Video > Colour` scalar children on write** (RFC 9559
  §5.1.4.1.28.16, §5.1.4.1.28.17..§5.1.4.1.28.29):
  `MkvMuxer::set_video_colour(stream_index, MkvVideoColour)` queues a
  per-track colour-description hint that lands inside the track's
  `Video` master at `write_header` time as a `Colour` master (id
  `0x55B0`) carrying the eleven scalar children: `MatrixCoefficients`
  / `BitsPerChannel` / `ChromaSubsampling{Horz,Vert}` /
  `CbSubsampling{Horz,Vert}` / `ChromaSiting{Horz,Vert}` / `Range` /
  `TransferCharacteristics` / `Primaries` / `MaxCLL` / `MaxFALL`.
  Convenience constructors `MkvVideoColour::bt709()` (matrix `1` /
  transfer `1` / primaries `1` / broadcast range — the canonical SDR
  HD shape) and `MkvVideoColour::bt2020_pq()` (matrix `9` / transfer
  `16` / primaries `9` / full range / 10 bpc — the canonical HDR10
  shape) cover the two everyday cases; every field can be overridden
  on the returned value for one-off departures. Per-element omission
  rules apply at write time: every scalar that equals its
  §5.1.4.1.28 spec default is left off-disk so the demuxer
  materialises the spec default; every `Option<u64>` (the four
  chroma-subsampling integers + `MaxCLL` / `MaxFALL`) is written
  when `Some(v)` and skipped when `None`. As a result, queueing
  `MkvVideoColour::default()` writes an empty 3-byte `Colour` master
  (id `0x55B0` + size VINT `0x80`), which the demuxer parses into
  `Some(VideoColour::default())` with every getter returning the
  materialised spec default — distinguishable on disk from the
  call-was-omitted case, which keeps the `Colour` master off-disk
  entirely so the demuxer surfaces `None` from `video_colour`. Spec
  rules enforced at queue time: the setter rejects post-`write_header`
  use, out-of-range `stream_index`, and calls on non-video tracks.
  The `Colour > MasteringMetadata` sub-master
  (§5.1.4.1.28.30..§5.1.4.1.28.40, id `0x55D0`) is emitted whenever
  the queued hint carries `mastering_metadata: Some(MkvMasteringMetadata)`;
  inside that master each chromaticity / luminance child
  (`PrimaryRChromaticityX/Y` / `PrimaryGChromaticityX/Y` /
  `PrimaryBChromaticityX/Y` / `WhitePointChromaticityX/Y` /
  `LuminanceMax` / `LuminanceMin`, ids `0x55D1`..`0x55DA`) is written
  as an 8-byte big-endian `f64` only when its own `Option<f64>` slot is
  `Some(v)` — mirroring the per-child omission rules above. A
  `Some(MkvMasteringMetadata::default())` (every slot `None`)
  serialises as an empty 3-byte `MasteringMetadata` master that the
  demuxer parses into `Some(MasteringMetadata::default())`; setting
  `mastering_metadata: None` keeps the entire sub-master off-disk so
  the demuxer surfaces `None` from `mastering_metadata()`. The
  convenience `MkvMasteringMetadata::bt2020_d65_hdr10()` populates the
  ten-child set with BT.2020 primaries + D65 white point + 1000 cd/m²
  peak / 0.005 cd/m² floor — the canonical HDR10 mastering display.
  Pairs symmetrically with the existing `MkvDemuxer::video_colour`
  typed accessor — a mux→demux pipeline preserves every scalar child
  verbatim, including the `Other(u64)` forward-compat variants on each
  of the six enum-typed children, plus every populated
  `MasteringMetadata` chromaticity / luminance child.
- **`Video > Projection` master on write** (RFC 9559 §5.1.4.1.28.41,
  including §5.1.4.1.28.42..§5.1.4.1.28.46):
  `MkvMuxer::set_video_projection(stream_index, MkvProjection)` queues a
  per-track hint that lands inside the track's `Video` master at
  `write_header` time, after the `Colour` master, as a `Projection`
  master (id `0x7670`). The demux-side `ProjectionType` enum gained a
  `to_raw()` inverse so every Table 18 value round-trips, including the
  `Other(u64)` forward-compat variant (§27.15 leaves the registry open).
  Per-element omission rules: `ProjectionType` is written only for
  non-`Rectangular` types (the §5.1.4.1.28.42 default `0` stays off-disk);
  each `ProjectionPose{Yaw,Pitch,Roll}` child is written as an 8-byte
  big-endian `f64` only when non-zero (the §5.1.4.1.28.44..46 default
  `0.0` stays off-disk); `ProjectionPrivate` (the verbatim ISOBMFF box
  body — `equi` / `cbmp` / `mshp`) is written only when `Some(_)` and is
  never interpreted by the muxer. Queueing `MkvProjection::default()`
  writes an empty `Projection` master that the demuxer parses into
  `Some(Projection::default())`; omitting the call keeps the master
  off-disk so the demuxer surfaces `None`. Convenience constructors
  `MkvProjection::equirectangular(private)` (the 360°-VR shape) and
  `MkvProjection::rotated(roll_degrees)` (the §5.1.4.1.28.46 worked
  example) cover the two common shapes. Spec rules enforced at queue
  time: rejects post-`write_header` use, out-of-range `stream_index`, and
  calls on non-video tracks. Pairs symmetrically with the existing
  `MkvDemuxer::video_projection` typed accessor — a mux→demux pipeline
  preserves the projection record (type, pose, and verbatim
  `ProjectionPrivate` payload) bit-exactly.
- **TrackEntry audience flags on write** (RFC 9559
  §5.1.4.1.6..§5.1.4.1.11):
  `MkvMuxer::set_track_audience_flags(stream_index, MkvTrackAudienceFlags)`
  queues a per-track hint whose six `Option<bool>` slots — `forced`
  (`FlagForced`, id `0x55AA`), `hearing_impaired` (`FlagHearingImpaired`,
  id `0x55AB`), `visual_impaired` (`FlagVisualImpaired`, id `0x55AC`),
  `text_descriptions` (`FlagTextDescriptions`, id `0x55AD`), `original`
  (`FlagOriginal`, id `0x55AE`), `commentary` (`FlagCommentary`, id
  `0x55AF`) — land directly inside the `TrackEntry` (the elements sit on
  `TrackEntry` itself, not in a sub-master) at `write_header` time, after
  `FlagLacing`, in numerical-id order. Per-element omission rule: each
  `Some(v)` slot writes the element explicitly as `0` / `1`; each `None`
  slot stays off-disk. For `FlagForced` (the only one with a spec
  default), omission and `Some(false)` decode identically (`false`) but
  differ on disk — the explicit write is the way to override a
  downstream tool. For the five default-less `minver: 4` flags the
  distinction is semantic: omission decodes as `None` while `Some(false)`
  round-trips as `Some(false)`, preserving the §5.1.4.1.7..§5.1.4.1.11
  "set to 1 *if and only if* …" explicit-zero signal. Unlike the
  `set_video_*` family there is **no track-type restriction** — the spec
  carries all six elements on every `TrackEntry`, so audio / video /
  subtitle tracks all accept the call (mirroring the demux side, which
  surfaces a record for every track). The muxer already pins
  `DocTypeVersion` to `4`, so emitting the `minver: 4` elements never
  violates the declared document version. Convenience constructors
  `MkvTrackAudienceFlags::forced_subtitle()` /
  `hearing_impaired_track()` / `visual_impaired_track()` /
  `commentary_track()` cover the common single-flag shapes. Rejects
  post-`write_header` use and out-of-range `stream_index`. Pairs
  symmetrically with the existing `MkvDemuxer::track_audience_flags`
  typed accessor — a mux→demux pipeline preserves every explicit flag,
  including the `Some(false)`-vs-absent distinction.
- **Per-Block `BlockAdditions` on write** (RFC 9559 §5.1.3.5.2 +
  §5.1.4.1.16): `MkvMuxer::write_packet_with_additions(&packet,
  &[MkvBlockAddition])` emits the packet as a `BlockGroup` (§5.1.3.5)
  instead of a `SimpleBlock``Block` (frame bytes, unlaced; any
  pending same-track lace is flushed first so Block order is
  preserved), `BlockAdditions` with one `BlockMore` per addition in
  slice order (each writing `BlockAdditional` verbatim and `BlockAddID`
  only when it differs from the §5.1.3.5.2.3 default `1`),
  `BlockDuration` (§5.1.3.5.3) when the packet carries a duration (a
  `SimpleBlock` could not have carried it), and `ReferenceBlock`
  (§5.1.3.5.5) when the packet is not a keyframe (a plain `Block` has
  no KEY flag bit; keyframe-ness is the element's absence — the
  relative value points at the track's most recently written Block,
  falling back to the spec-sanctioned `0` "reference unknown" when
  there is none). Prerequisite: declare the track's maximum id via
  `MkvMuxer::set_max_block_addition_id(stream_index, max)` before
  `write_header` — it lands as the `MaxBlockAdditionID` TrackEntry
  element, and `write_packet_with_additions` rejects an undeclared
  stream (§5.1.4.1.16's default `0` means "no BlockAdditions for this
  track"), a `BlockAddID` of `0` (range "not 0"), an id above the
  declared maximum, and duplicate ids within one call (§5.1.3.5.2.3
  uniqueness MUST) — all before any byte is written. An empty
  additions slice degrades to plain `write_packet` behaviour
  (`BlockMore` is mandatory inside the master, so an empty
  `BlockAdditions` would be malformed). The convenience constructor
  `MkvBlockAddition::codec_defined(data)` covers the `BlockAddID = 1`
  shape (e.g. WebM alpha — pair with `set_video_alpha_mode`). Pairs
  symmetrically with the new `MkvDemuxer::block_additions` /
  `max_block_addition_id` typed accessors — a mux→demux pipeline
  preserves every addition byte-for-byte, plus the packet's keyframe
  flag and duration.
- Opt-in **block lacing** on write (RFC 9559 §5.1.4.5.5, §10.3):
  `MkvMuxer::with_block_lacing(LacingMode::{Xiph,Ebml,FixedSize})`
  before `write_header` aggregates same-track, same-keyframe-status
  consecutive frames (up to 8 per Block, never crossing a cluster
  boundary) into a single laced `SimpleBlock`. Default stays
  `LacingMode::None` (one frame per Block, `FlagLacing = 0`) for
  byte-identical back-compat. When lacing is on, the muxer writes
  `TrackEntry.FlagLacing = 1`, sets the LACING bits in the
  SimpleBlock flags byte to the requested mode, and encodes the
  per-frame size header (Xiph 255-additive octets,
  EBML signed-VINT deltas, or no header for fixed-size). For
  fixed-size mode, a frame whose size differs from the buffered run
  flushes the lace and starts a new one. Demuxer side already
  handles all three modes — the new write path completes the
  round-trip in-tree.
- **`Audio` master children on write** (RFC 9559 §5.1.4.1.29,
  §5.1.4.1.29.1..§5.1.4.1.29.4):
  `MkvMuxer::set_track_audio(stream_index, MkvTrackAudio)` queues a
  per-track hint that lands inside the track's `Audio` master (id
  `0xE1`) at `write_header` time. The muxer already derives a minimal
  `Audio` master from the stream's `StreamInfo` (`sample_rate`  `SamplingFrequency`, `channels``Channels`, sample-format bit width
  `BitDepth`); this hint lets a caller override those derived children
  **and** supply the one child the `StreamInfo`-derived path cannot
  express: `OutputSamplingFrequency` (id `0x78B5`, §5.1.4.1.29.2), the
  Spectral Band Replication (SBR) output rate the demux-side
  `track_audio` / `TrackAudio::is_sbr()` accessor already reads back.
  Per-field rule: a `Some(v)` overrides the `StreamInfo`-derived child;
  a `None` defers to the `StreamInfo` value (and for
  `output_sampling_frequency`, simply omits the element). Children that
  resolve to nothing stay off-disk so the demuxer materialises the
  §5.1.4.1.29.1 default `8000.0` / §5.1.4.1.29.3 default `1` (mono);
  `BitDepth` has no spec default, so its absence surfaces as `None`. The
  convenience constructor `MkvTrackAudio::sbr(core)` produces the
  canonical HE-AAC pair (`core`, `2*core`). Spec range checks enforced
  at queue time: `SamplingFrequency` / `OutputSamplingFrequency` ranged
  `> 0x0p+0` (a `Some(v)` `<= 0.0` / non-finite is rejected),
  `Channels` / `BitDepth` ranged `not 0` (a `Some(0)` is rejected).
  Track-type restriction mirrors the demux side (which returns `None`
  for non-audio tracks): the setter rejects non-`Audio` streams plus
  post-`write_header` use and out-of-range `stream_index`; repeated
  calls are last-write-wins; the read-back
  `MkvMuxer::track_audio(stream_index)` accessor returns the queued hint
  pre-`write_header`. Pairs symmetrically with the existing
  `MkvDemuxer::track_audio` typed accessor — a mux→demux pipeline
  preserves every supplied child bit-exactly, including the
  `OutputSamplingFrequency` SBR signal.
- **`TrackEntry` timing trio on write** (RFC 9559
  §5.1.4.1.13..§5.1.4.1.15): `MkvMuxer::set_track_timing(stream_index,
  MkvTrackTiming)` queues a per-track hint whose three `Option` slots —
  `default_duration` (`DefaultDuration`, id `0x23E383`),
  `default_decoded_field_duration` (`DefaultDecodedFieldDuration`, id
  `0x234E7A`), and `track_timestamp_scale` (`TrackTimestampScale`, id
  `0x23314F`) — land directly inside the `TrackEntry` (no gating master) at
  `write_header` time, after `MaxBlockAdditionID`. Per-field omission rule:
  each `Some(v)` writes the element explicitly, each `None` stays off-disk
  (the demuxer surfaces `None` for the two durations and materialises the
  §5.1.4.1.15 `TrackTimestampScale` default `1.0`). There is no track-type
  restriction — the spec carries all three on every `TrackEntry`. Spec
  range checks enforced at queue time: the two durations are ranged `not 0`
  (a `Some(0)` is rejected) and `TrackTimestampScale` is ranged `> 0x0p+0`
  (a non-finite / non-positive `Some(v)` is rejected); the setter also
  rejects post-`write_header` use and out-of-range `stream_index`. The
  convenience constructor `MkvTrackTiming::from_frame_rate(fps)` rounds
  `1e9 / fps` to the nanosecond `DefaultDuration` interval (rejecting
  non-finite / non-positive fps). Repeated calls are last-write-wins; the
  read-back `MkvMuxer::track_timing(stream_index)` accessor returns the
  queued hint pre-`write_header`. Pairs symmetrically with the new
  `MkvDemuxer::track_timing` typed accessor — a mux→demux pipeline
  preserves every supplied child bit-exactly, including the
  `DefaultDuration`-derived nominal frame rate.

### Codec ID mapping (`codec_id` module)

Matroska `CodecID` string <-> oxideav `CodecId`. Both directions are
implemented for roundtrip:

- Audio: `A_FLAC`, `A_OPUS`, `A_VORBIS`, `A_PCM/INT/LIT`,
  `A_PCM/INT/BIG`, `A_PCM/FLOAT/IEEE`, `A_AAC` (+ `MPEG4/LC` /
  `MPEG2/LC` aliases), `A_MPEG/L3`, `A_AC3`, `A_EAC3`.
- Video: `V_VP8`, `V_VP9`, `V_AV1`, `V_MPEG4/ISO/AVC`,
  `V_MPEGH/ISO/HEVC`, `V_FFV1`, `V_THEORA`, plus `V_MS/VFW/FOURCC` with
  BITMAPINFOHEADER fourcc extraction (e.g. `FFV1`).
- Subtitle: `S_TEXT/UTF8` (subrip), `S_TEXT/SSA`, `S_TEXT/ASS`,
  `S_TEXT/WEBVTT`, `S_TEXT/USF`, `S_VOBSUB` (DVD), `S_HDMV/PGS` /
  `S_HDMV/TEXTST` (Blu-ray), `S_DVBSUB`, `S_KATE`. Subtitle tracks
  surface with `MediaType::Subtitle`; their payload bytes pass through
  unchanged.

Unknown MKV codec IDs fall back to a pass-through `mkv:<raw-id>` form
so the demuxer never hides an unrecognised track.

### Probes + registration

- Registers both `"matroska"` and `"webm"` with the container registry.
- Extensions: `.mkv`, `.mka`, `.mks` -> `matroska`; `.webm` -> `webm`.
- Probe scoring: DocType=webm scores 100 on `probe_webm` and 0 on
  `probe_matroska` (so `.mkv` never masquerades as `webm`). DocType=
  matroska scores 100 on `probe_matroska` and 0 on `probe_webm`. Files
  with an ambiguous DocType fall through to the matroska entry.

## What's NOT implemented

- CRC-32 validation covers Top-Level master elements parsed up front and
  every `Cluster` the demuxer opens through `next_packet` / `seek_to`; the
  late best-effort Cues rescan (when Cues sit after the final Cluster) is
  now checksummed too — a leading `CRC-32` child on the late-Cues `Cues`
  element validates and surfaces through `crc_status()` exactly the same
  way the up-front masters do. A `Cluster` declared with the unknown-size
  VINT still produces no status (RFC 8794 §11.3.1 needs a bounded body).
  The muxer writes a leading `CRC-32` child on every Top-Level master it
  buffers end-to-end before flushing — `Info`, `Tracks`, `Cues`, plus
  `Chapters` and `Attachments` when those are queued. `SeekHead` and
  `Cluster` are deliberately not CRC'd on the mux side: the `SeekHead`
  Cues entry is patched in `write_trailer` (which would invalidate any
  CRC computed up front), and `Cluster` is streamed with the unknown-size
  VINT (RFC 8794 §11.3.1's bounded-body requirement).
- ContentSignature (RFC 9559 §A.33 reclaimed `0x47E3`) is parsed by neither
  side. The element is reserved for a future per-segment signature scheme.
- `TrackOperation` is decoded and surfaced (left/right-eye plane combining,
  block joining) but the demuxer does not yet *apply* it — virtual tracks
  are reported alongside their source tracks rather than being synthesised
  into a single combined output stream. `TrackOperation` is never written
  on the mux side.
- `ContentEncodings` is decoded and surfaced (compression / encryption
  headers). The demuxer *undoes* a Block-scoped Header-Stripping chain
  (algo 3) on read — packets carry the original frame bytes — but the
  generic compression algorithms (zlib / bzlib / lzo1x) and encryption are
  not reversed: for those a caller that wants raw codec bytes must apply the
  reported encoding chain itself. zlib/bzlib/lzo1x decompression and
  decryption are out of container scope; `ContentEncodings` is never written
  on the mux side.
- `Video` sub-element coverage is now complete on the demux side:
  `PixelWidth` / `PixelHeight` (§5.1.4.1.28.6 / §5.1.4.1.28.7) feed the
  `StreamInfo` dimensions; `FlagInterlaced` / `FieldOrder`
  (§5.1.4.1.28.1 / §5.1.4.1.28.2) surface through `video_interlacing`;
  the `PixelCrop{Top,Bottom,Left,Right}` + `DisplayWidth` /
  `DisplayHeight` / `DisplayUnit` quartet
  (§5.1.4.1.28.8..§5.1.4.1.28.14) surfaces through `video_geometry`;
  the full `Colour` master (§5.1.4.1.28.16) — including HDR metadata
  (`MaxCLL` / `MaxFALL` / `MasteringMetadata`) — surfaces through
  `video_colour`; `StereoMode` (§5.1.4.1.28.3) surfaces through
  `video_stereo_mode`; the `Projection` master (§5.1.4.1.28.41) —
  including `ProjectionType`, the verbatim ISOBMFF-mirrored
  `ProjectionPrivate` payload, and the yaw / pitch / roll pose triple —
  surfaces through `video_projection`; `AlphaMode` (§5.1.4.1.28.4)
  surfaces through `video_alpha_mode`; the reclaimed Appendix-A
  `AspectRatioType` element surfaces through
  `video_aspect_ratio_type`; and `UncompressedFourCC`
  (§5.1.4.1.28.15) surfaces through `video_uncompressed_fourcc`. On the
  mux side, `PixelWidth` / `PixelHeight`, the `FlagInterlaced` /
  `FieldOrder` pair (`MkvMuxer::set_video_interlacing`,
  §5.1.4.1.28.1 + §5.1.4.1.28.2), the `StereoMode` / `AlphaMode`
  pair (`MkvMuxer::set_video_stereo_mode` /
  `MkvMuxer::set_video_alpha_mode`, §5.1.4.1.28.3 + §5.1.4.1.28.4),
  the `PixelCrop{Top,Bottom,Left,Right}` + `DisplayWidth` /
  `DisplayHeight` / `DisplayUnit` quartet
  (`MkvMuxer::set_video_geometry`, §5.1.4.1.28.8..§5.1.4.1.28.14),
  `UncompressedFourCC`
  (`MkvMuxer::set_video_uncompressed_fourcc`, §5.1.4.1.28.15), the
  eleven scalar children of the `Colour` master
  (`MkvMuxer::set_video_colour`, §5.1.4.1.28.16,
  §5.1.4.1.28.17..§5.1.4.1.28.29 — `MatrixCoefficients`,
  `BitsPerChannel`, `ChromaSubsampling{Horz,Vert}`,
  `CbSubsampling{Horz,Vert}`, `ChromaSiting{Horz,Vert}`, `Range`,
  `TransferCharacteristics`, `Primaries`, `MaxCLL`, `MaxFALL`; the
  convenience constructors `MkvVideoColour::bt709()` and
  `MkvVideoColour::bt2020_pq()` cover the SDR HD and HDR10 PQ
  shapes), and the ten chromaticity / luminance children of the
  `Colour > MasteringMetadata` sub-master
  (`MkvVideoColour::mastering_metadata = Some(MkvMasteringMetadata)`,
  §5.1.4.1.28.30..§5.1.4.1.28.40 — `Primary{R,G,B}Chromaticity{X,Y}`,
  `WhitePointChromaticity{X,Y}`, `Luminance{Max,Min}`; the convenience
  constructor `MkvMasteringMetadata::bt2020_d65_hdr10()` covers the
  canonical HDR10 shape), and the `Projection` master
  (`MkvMuxer::set_video_projection`, §5.1.4.1.28.41 — `ProjectionType`,
  the verbatim `ProjectionPrivate` payload, and the yaw / pitch / roll
  pose triple; the convenience constructors
  `MkvProjection::equirectangular()` and `MkvProjection::rotated()` cover
  the 360°-VR and roll-only shapes), and the reclaimed Appendix-A
  `AspectRatioType` element (`MkvMuxer::set_video_aspect_ratio_type`,
  Appendix A.24, id `0x54B3`) are written. The `Video` sub-element set
  is now fully symmetric — every element the demux side reads, the mux
  side can write.

## Robustness

`tests/injection_robustness.rs` pins sixteen attacker-shaped byte
patterns against the open / `next_packet` / `seek_to` / `attachment_data`
surface: a `skip` helper that previously cast `u64 as i64` and could
seek the reader *backwards* on a forged `Size` field; demux-open
rejection of an empty input, an EBML-magic with a truncated header, an
oversize EBML-header `Size`, oversize `DocType` / `CodecID` / `TagString`
strings, and a `Segment` declared size that runs past EoF; cluster-time
handling of an oversize `SimpleBlock`, a Xiph-laced `SimpleBlock` whose
declared sub-frame sizes overrun the body, and a fixed-laced
`SimpleBlock` with `n_frames = 5` over an empty payload; on-demand
`attachment_data` short-read on a forged 4 GiB `FileData` size and a
forged 2 GiB `FileName`; an out-of-range `CueRelativePosition` in
`seek_to`; and an inline fuzz-corpus replay of five malformed seed
shapes. All checks land as standard `cargo test` targets so a regression
on any one surfaces in CI without waiting for a fuzz cycle.

## Fuzzing

A cargo-fuzz harness for the demuxer lives in `fuzz/`. It drives
`demux::open`, drains up to 256 packets via `next_packet`, and exercises
the `seek_to` cluster pre-open path — over arbitrary bytes — against the
contract that no call panics, aborts, integer-overflows (in a debug
build), or attempts an attacker-controlled allocation that exceeds what
the input can back. The seed corpus in `fuzz/corpus/demux/` covers a
minimal valid Matroska file, a minimal valid WebM file, an EBML-header-
only stream, and two regression inputs (one for an EBML size-overflow,
one for a zero-frame-size fixed-lacing `SimpleBlock`).

Run locally with a nightly toolchain:

```sh
cd fuzz
cargo +nightly fuzz run demux            # libFuzzer drives indefinitely
cargo +nightly fuzz run demux -- -max_total_time=60   # bounded
```

CI runs a 30-minute fuzz cycle daily via
`.github/workflows/fuzz.yml` (the OxideAV org-level reusable
`crate-fuzz.yml`).

## License

MIT - see [LICENSE](LICENSE).