Make six silent mux failures observable
All six confirmed against the code. The governing rule this cluster serves: a
lossy or degraded outcome is never silent, because a corrupt rip the user does
not know about is the worst failure available.
**A 3D MKV re-mux silently lost one eye.** The BlockGroup read path had arms for
BLOCK / BLOCK_DURATION / REFERENCE_BLOCK only, so BLOCK_ADDITIONS fell into the
skip arm — while the writer does emit BlockAdditions > BlockMore > BlockAdditional
for the MVC dependent view. Reconstruction was judged out of scope and the
reasoning is recorded: PesFrame has no side-payload field and the header parser
never reads BlockAdditionMapping, so there is no dependent-view track to route the
AU to. Instead the loss is now LOUD — counted in bytes and events, warned once,
and surfaced through MkvStream's errors()/lost_bytes(), which the driver already
samples into MuxOutcome. One detail in the finding was wrong and is corrected: the
re-mux does NOT still advertise the mvcC mapping, because the header parser
ignores that element, so the output is a plain 2D H.264 track.
**An all-titles rip silently skipped real titles.** The header-buffer-cap
overflow returned Error::MkvInvalid, and is_skippable_title_stub matches exactly
E_MKV_INVALID | E_CSS_KEY_MISSING — verified here — so a 512 MiB-of-frames title
was classified as an empty nav/menu PGC stub and dropped. It now has its own
E9051 / MuxHeaderBufferExceeded { bytes }, outside the skippable set.
**The public pre-mux report contradicted the file.** Mp4Sink::finish() drops an
audio track it cannot describe, which I chose last round over failing an export
whose video is fine — but mp4_fit_report still listed that stream as included, so
the application's plan and the actual output disagreed. Fixed at both levels:
Mp4SkipReason is now non_exhaustive with NoSamples and UndescribableAudio,
Mp4Sink::final_report() describes the FILE rather than the plan, and for the
boxed dyn Stream path a defaulted Stream::undelivered_streams() carries the
information out to MuxOutcome::undelivered_streams with a driver-side warn.
**MP4 track ids could collide.** ids were assigned before the retain that drops
sample-less tracks, while next_id came from the post-retain count, so [1,3]
yielded next_id 3. Now max(track_id) + 1, saturating.
**Stream selection silently skipped its codec_privates prune** when the lists were
not the same length — but codec_privates is consumed POSITIONALLY and trailing
extras are documented as benign, so the length-equality guard was itself the bug.
The prune now runs unconditionally by index.
**The m2ts_mux scaffolding armed params_written on both the absent and the
unparseable codec_private arms** — the same defect already fixed in tsmux.rs.
Split into params_attempted (a latch, since retrying identical bytes cannot help)
and params_emitted, with a warn on each failure arm and an accessor so the
eventual wiring and its test can observe it.
Each fix verified red by mutating back to the prior behaviour: errors() 0 vs 1,
E6008 vs E9051, final_report [0,1] vs [0], next_track_id 3 vs [1,3], and the
selection prune resolving index 1 to the wrong track's record.
API surface deliberately widened: MuxOutcome gains a public field and Mp4Sink
becomes public. Nothing in-repo breaks. Note a behaviour change on the mkv://
input path — a 3D re-mux now reports non-zero loss, so a consumer treating
errors > 0 as disc damage will trip on it. That is intended: the outcome IS
degraded.
This commit is contained in:
@@ -151,6 +151,11 @@ pub const E_NETWORK_ADDR_BLOCKED: u16 = 9022;
|
||||
/// zero-frame mux (undecryptable input, fully-unreadable title, every
|
||||
/// frame dropped before the first keyframe) cannot report success.
|
||||
pub const E_MUX_EMPTY: u16 = 9023;
|
||||
/// The mux driver buffered past its pre-headers cap without every video track's
|
||||
/// `codec_private` resolving. Deliberately NOT [`E_MKV_INVALID`], which
|
||||
/// [`is_skippable_title_stub`] treats as a skippable empty nav/menu stub — a
|
||||
/// cap-overflow is a real title and must never be silently skipped.
|
||||
pub const E_MUX_HEADER_BUFFER_EXCEEDED: u16 = 9051;
|
||||
pub const E_EXTENT_NOT_UNIT_ALIGNED: u16 = 9030;
|
||||
/// `mp4://` output but the title has no (primary) video track to carry.
|
||||
pub const E_MP4_NO_VIDEO_TRACK: u16 = 9048;
|
||||
@@ -442,6 +447,20 @@ pub enum Error {
|
||||
/// every frame dropped before the first keyframe — fails loudly. The
|
||||
/// `m2ts://` analogue of [`Error::MkvInvalid`]'s zero-frame guard.
|
||||
MuxEmpty,
|
||||
/// The mux driver's pre-headers frame buffer passed its cap before every
|
||||
/// video track's `codec_private` resolved: the title keeps yielding real
|
||||
/// frames but its codec init data never appears, so buffering further would
|
||||
/// swap the box to death. Carries the buffered byte count.
|
||||
///
|
||||
/// DISTINCT from [`Error::MkvInvalid`] on purpose. `MkvInvalid` is what an
|
||||
/// empty nav/menu PGC stub yields, and [`is_skippable_title_stub`] classifies
|
||||
/// it as skippable — an all-titles rip drops that title and finishes the
|
||||
/// rest. Hundreds of megabytes of real frames with unresolvable headers is
|
||||
/// NOT a stub; reporting it as one silently dropped a main feature from a
|
||||
/// rip that then exited successfully. This code is not skippable.
|
||||
MuxHeaderBufferExceeded {
|
||||
bytes: u64,
|
||||
},
|
||||
/// `mp4://` target title has no primary video track to mux.
|
||||
Mp4NoVideoTrack,
|
||||
/// `mp4://` source file is malformed/truncated — the MP4 demuxer failed.
|
||||
@@ -629,6 +648,7 @@ impl Error {
|
||||
Error::StreamUrlMissingPort { .. } => E_STREAM_URL_MISSING_PORT,
|
||||
Error::NetworkAddrBlocked { .. } => E_NETWORK_ADDR_BLOCKED,
|
||||
Error::MuxEmpty => E_MUX_EMPTY,
|
||||
Error::MuxHeaderBufferExceeded { .. } => E_MUX_HEADER_BUFFER_EXCEEDED,
|
||||
Error::Mp4NoVideoTrack => E_MP4_NO_VIDEO_TRACK,
|
||||
Error::Mp4Invalid => E_MP4_INVALID,
|
||||
Error::Mp4MissingCodecPrivate => E_MP4_MISSING_CODEC_PRIVATE,
|
||||
@@ -797,6 +817,9 @@ impl std::fmt::Display for Error {
|
||||
Error::SelectionPidUnknown { pid } => {
|
||||
write!(f, "E{}: 0x{:04x}", self.code(), pid)
|
||||
}
|
||||
Error::MuxHeaderBufferExceeded { bytes } => {
|
||||
write!(f, "E{}: {}", self.code(), bytes)
|
||||
}
|
||||
_ => write!(f, "E{}", self.code()),
|
||||
}
|
||||
}
|
||||
@@ -869,6 +892,9 @@ impl From<Error> for std::io::Error {
|
||||
// 9023 MuxEmpty: finish() reached with zero frames — the output
|
||||
// would be a header-only container. Treat as invalid output.
|
||||
E_MUX_EMPTY => std::io::ErrorKind::InvalidData,
|
||||
// 9051 MuxHeaderBufferExceeded: the source kept yielding frames but
|
||||
// never its codec init data — the input is unusable as declared.
|
||||
E_MUX_HEADER_BUFFER_EXCEEDED => std::io::ErrorKind::InvalidData,
|
||||
// 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.
|
||||
@@ -1066,6 +1092,17 @@ mod tests {
|
||||
// A plain io::Error with no E-code prefix is not skippable.
|
||||
let plain = std::io::Error::from(std::io::ErrorKind::BrokenPipe);
|
||||
assert!(!is_skippable_title_stub(&plain));
|
||||
|
||||
// A header-buffer cap overflow is a REAL title whose codec init data
|
||||
// never resolved — hundreds of MiB of frames, not an empty nav/menu
|
||||
// stub. It used to be reported as `MkvInvalid`, which lands in the
|
||||
// skippable set above, so an all-titles rip dropped a main feature and
|
||||
// still exited successfully. It must have its own, non-skippable code.
|
||||
let cap: std::io::Error = Error::MuxHeaderBufferExceeded {
|
||||
bytes: 512 * 1024 * 1024 + 1,
|
||||
}
|
||||
.into();
|
||||
assert!(!is_skippable_title_stub(&cap));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1353,6 +1390,7 @@ mod tests {
|
||||
E_STREAM_URL_MISSING_PORT,
|
||||
E_NETWORK_ADDR_BLOCKED,
|
||||
E_MUX_EMPTY,
|
||||
E_MUX_HEADER_BUFFER_EXCEEDED,
|
||||
E_MP4_NO_VIDEO_TRACK,
|
||||
E_MP4_INVALID,
|
||||
E_MP4_MISSING_CODEC_PRIVATE,
|
||||
@@ -1443,6 +1481,10 @@ mod tests {
|
||||
(Error::PipelineConsumerGone, E_PIPELINE_CONSUMER_GONE),
|
||||
(Error::DiscCapacityOverflow, E_DISC_CAPACITY_OVERFLOW),
|
||||
(Error::MuxEmpty, E_MUX_EMPTY),
|
||||
(
|
||||
Error::MuxHeaderBufferExceeded { bytes: 0 },
|
||||
E_MUX_HEADER_BUFFER_EXCEEDED,
|
||||
),
|
||||
(Error::Mp4NoVideoTrack, E_MP4_NO_VIDEO_TRACK),
|
||||
(Error::Mp4Invalid, E_MP4_INVALID),
|
||||
(Error::Mp4MissingCodecPrivate, E_MP4_MISSING_CODEC_PRIVATE),
|
||||
|
||||
Reference in New Issue
Block a user