fix(mp4): refuse a video track with no resolved dimensions

Resolution::pixels() returned (0, 0) for Unknown, and the MP4 sink wrote
it verbatim into tkhd (ISO/IEC 14496-12 8.3.2) and VisualSampleEntry
(12.1.3). Both fields are MANDATORY there, so unlike Matroska — which
omits the optional PixelWidth/PixelHeight elements — MP4 has nothing to
leave out. The result was a structurally complete file that passes every
container check, declares a 0x0 video track, cannot be rendered, and is
written with no error anywhere.

WHY IT WAS POSSIBLE, which is the part worth keeping:

pixels() previously fabricated 1920x1080 for Unknown. That was wrong but
playable, so this sink never needed a guard and the absence of one was
invisible. Changing the sentinel to (0, 0) moved the defect instead of
removing it — a zero PAIR still reads as a usable value, so the sink
stored it and serialised it.

The accessor's doc comment then ENUMERATED the callers it believed were
safe: "the Matroska sink omits the optional elements, the VobSub writer
omits its size: line, and no caller divides by either dimension." Two of
those three are true. MP4 was not on the list because MP4 has no guard
at all, and a prose list cannot enforce itself. mkv.rs's own comment
even states the principle — "the check belongs in the one accessor
rather than in each caller that remembered to write it" — and
labels/mod.rs still carried its own duplicate Unknown test long after
the accessor took that job over.

So: pixels() now returns Option. Not because Option is tidier, but
because every caller genuinely needs a DIFFERENT answer and the compiler
is the only thing that reliably makes them choose one. Matroska and the
metadata sinks take unwrap_or((0, 0)) with the reason stated at each
site; the VobSub path degrades to a palette-only .idx; MP4 fails with
E_MP4_UNKNOWN_RESOLUTION (9055).

Six call sites, not the five my first grep showed — I piped it through
`head` and acted on a truncated list. The compiler caught the sixth.
That is the same mistake as trusting a lens that reported silence.
This commit is contained in:
Matthew Jackson
2026-07-30 19:34:54 -07:00
parent 30bea12392
commit 9f25a4c454
8 changed files with 119 additions and 32 deletions
+15 -3
View File
@@ -207,6 +207,12 @@ pub const E_MP4_INVALID: u16 = 9049;
/// `mp4://` video track is missing its codec-configuration record
/// (`hvcC`/`avcC`), without which the sample entry can't be written.
pub const E_MP4_MISSING_CODEC_PRIVATE: u16 = 9050;
/// `mp4://` video track has no resolved frame dimensions. ISO/IEC 14496-12
/// makes width and height mandatory in both `tkhd` (8.3.2) and
/// VisualSampleEntry (12.1.3), so unlike Matroska there is no element to omit:
/// the sink would have to write 0x0, producing a structurally complete file no
/// player can render. Refuse instead.
pub const E_MP4_UNKNOWN_RESOLUTION: u16 = 9055;
/// READ CAPACITY returned a short or overflowing transfer.
pub const E_DISC_CAPACITY_MALFORMED: u16 = 9047;
@@ -548,6 +554,9 @@ pub enum Error {
Mp4Invalid,
/// `mp4://` video track is missing its `hvcC`/`avcC` configuration record.
Mp4MissingCodecPrivate,
/// `mp4://` video track has no resolved frame dimensions. See
/// [`E_MP4_UNKNOWN_RESOLUTION`].
Mp4UnknownResolution,
PesFrameTooLarge {
size: usize,
},
@@ -737,6 +746,7 @@ impl Error {
Error::Mp4NoVideoTrack => E_MP4_NO_VIDEO_TRACK,
Error::Mp4Invalid => E_MP4_INVALID,
Error::Mp4MissingCodecPrivate => E_MP4_MISSING_CODEC_PRIVATE,
Error::Mp4UnknownResolution => E_MP4_UNKNOWN_RESOLUTION,
Error::PesFrameTooLarge { .. } => E_PES_FRAME_TOO_LARGE,
Error::PesInvalidMagic => E_PES_INVALID_MAGIC,
Error::PesTrackTooLarge { .. } => E_PES_TRACK_TOO_LARGE,
@@ -992,9 +1002,10 @@ impl From<Error> for std::io::Error {
// mp4:// demux errors: a malformed/truncated source file
// (E_MP4_INVALID), or a source whose tracks the mux can't use — no
// video track / missing codec-private config. All are invalid data.
E_MP4_NO_VIDEO_TRACK | E_MP4_INVALID | E_MP4_MISSING_CODEC_PRIVATE => {
std::io::ErrorKind::InvalidData
}
E_MP4_NO_VIDEO_TRACK
| E_MP4_INVALID
| E_MP4_MISSING_CODEC_PRIVATE
| E_MP4_UNKNOWN_RESOLUTION => std::io::ErrorKind::InvalidData,
// 9030 ExtentNotUnitAligned: a malformed/non-AACS-aligned
// extent was handed to the prefetch producer.
9030 => std::io::ErrorKind::InvalidInput,
@@ -1696,6 +1707,7 @@ mod tests {
(Error::Mp4NoVideoTrack, E_MP4_NO_VIDEO_TRACK),
(Error::Mp4Invalid, E_MP4_INVALID),
(Error::Mp4MissingCodecPrivate, E_MP4_MISSING_CODEC_PRIVATE),
(Error::Mp4UnknownResolution, E_MP4_UNKNOWN_RESOLUTION),
(Error::M2tsPacketMalformed, E_M2TS_PACKET_MALFORMED),
(Error::ExtentNotUnitAligned, E_EXTENT_NOT_UNIT_ALIGNED),
(Error::DiscCapacityMalformed, E_DISC_CAPACITY_MALFORMED),