mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb6bcfebe | ||
|
|
e98e98c8fd | ||
|
|
7c085db133 | ||
|
|
587e26353c | ||
|
|
3eb3e649b1 | ||
|
|
b01505983c |
@@ -1,5 +1,33 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.10] — 2026-08-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- TrueHD/MLP audio: on a source transport-stream discontinuity (lost or gapped
|
||||
packets, signalled by a continuity-counter break), the parser now drops forward
|
||||
to the next major-sync access unit before resuming, instead of splicing the
|
||||
post-gap audio mid-stream. MLP carries predictor + restart state across access
|
||||
units, so resuming mid-stream decoded against stale state and produced a
|
||||
decoder-choking seam — streaming decoders emitted a burst of "restart header
|
||||
sync incorrect" / "Invalid blocksize" errors and could flag the whole track
|
||||
corrupt. This mirrors the video path (resync to the next keyframe after a gap)
|
||||
and how the independent-frame audio codecs (AC-3, DTS, FLAC, AAC) already
|
||||
recover; the fix is guarded so it never arms before a validated major-sync
|
||||
baseline exists (no whole-track drop at stream head). The gapped instant itself
|
||||
is in the source and cannot be recovered, but the muxed track is now
|
||||
decoder-clean.
|
||||
|
||||
## [1.6.9] — 2026-08-22
|
||||
|
||||
### Changed
|
||||
|
||||
- Version aligned to 1.6.9 for the unified release. No functional changes to
|
||||
this crate; the release was driven by autorip (automatic per-episode TV
|
||||
ripping — each episode named `S{NN}E{MM}`, with TMDB runtime-aligned episode
|
||||
numbering across multi-disc seasons — a Manual Rename option, and a unified
|
||||
per-disc staging state file — see the autorip 1.6.9 notes).
|
||||
|
||||
## [1.6.8] — 2026-08-21
|
||||
|
||||
### Changed
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "1.6.8"
|
||||
version = "1.6.10"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "MIT"
|
||||
@@ -26,7 +26,7 @@ aes = "0.9"
|
||||
# Interim path dep for local cross-repo dev; the release script re-pins this to
|
||||
# `{ git = ".../freemkv-unlock", tag = "vX.Y.Z" }` before tagging libfreemkv (so
|
||||
# the released tag resolves freemkv-unlock from git, not a sibling path).
|
||||
freemkv-unlock = { git = "https://github.com/freemkv/freemkv-unlock", tag = "v1.6.8" }
|
||||
freemkv-unlock = { git = "https://github.com/freemkv/freemkv-unlock", tag = "v1.6.10" }
|
||||
rand = "0.10"
|
||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
base64 = "0.23"
|
||||
|
||||
@@ -205,8 +205,12 @@ impl CodecParser for DtsParser {
|
||||
// TRUNCATED. Splicing post-gap bytes onto it corrupts the core/extension
|
||||
// framing (→ "Failed to decode block code(s)" / "Invalid data found").
|
||||
// Drop the partial AU and its PTS marks; the next PES re-bases a fresh
|
||||
// unit. (Audio has no inter-frame refs — dropping the spliced partial is
|
||||
// the whole fix; the video ResyncGate handles video.)
|
||||
// unit. (DTS core/extension frames are independently decodable — every
|
||||
// frame re-inits on its own syncword — so dropping the truncated partial
|
||||
// and resuming on the next frame is sufficient here. This is NOT true of
|
||||
// every audio codec: TrueHD/MLP carries predictor + restart state across
|
||||
// access units and must additionally drop forward to its next major sync;
|
||||
// see `codec::truehd`. The video ResyncGate handles inter-coded video.)
|
||||
//
|
||||
// Handle the discontinuity BEFORE the empty-data guard so the signal can
|
||||
// never be stranded by an empty post-gap PES (defensive; the demuxer only
|
||||
|
||||
@@ -367,6 +367,28 @@ impl CodecParser for TrueHdParser {
|
||||
// emits non-empty PES today).
|
||||
if pes.discontinuity {
|
||||
self.acc.clear();
|
||||
// Unlike AC-3/DTS (independent frames), MLP/TrueHD carries filter +
|
||||
// predictor + restart state ACROSS access units. A demux-signalled
|
||||
// discontinuity (lost TS packets / a concealed source gap) leaves that
|
||||
// state stale, so resuming mid-stream on the next parity-OK AU decodes
|
||||
// against broken state and desyncs a conformant decoder ("restart header
|
||||
// sync incorrect" / "Invalid blocksize"). Arm the same drop-forward the
|
||||
// corruption path uses: keep dropping until the next CRC-valid major sync
|
||||
// (a clean re-init point), then resume. This mirrors the video path's
|
||||
// "resync on the next keyframe" after a gap — the gapped audio can't be
|
||||
// recovered either way, but the output stays decoder-clean instead of
|
||||
// shipping a decoder-choking seam.
|
||||
//
|
||||
// Only once a validated major sync has established a baseline: before
|
||||
// that there is no CRC-validatable re-init point to resume on, so arming
|
||||
// here would collateral-drop the WHOLE track waiting for a major sync we
|
||||
// cannot yet validate — the exact stream-head catastrophe `au_check`
|
||||
// guards (a checksum-failed head major sync is kept, not nuked). At
|
||||
// stream head we therefore keep the current behaviour: drop the truncated
|
||||
// partial (above) and let the fresh post-gap unit through.
|
||||
if self.num_substreams.is_some() {
|
||||
self.resync_pending = true;
|
||||
}
|
||||
}
|
||||
if pes.data.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -913,6 +935,73 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discontinuity_resyncs_forward_to_next_major_sync() {
|
||||
// A source-signalled discontinuity (lost TS packets / a concealed gap)
|
||||
// leaves MLP's cross-AU predictor + restart state stale. Resuming
|
||||
// mid-stream on the next parity-OK AU decodes against broken state and
|
||||
// desyncs a conformant decoder ("restart header sync incorrect" /
|
||||
// "Invalid blocksize"). So a discontinuity must arm the SAME drop-forward
|
||||
// the corruption path uses: drop every post-gap AU until the next
|
||||
// CRC-valid major sync (the clean re-init point), then resume there.
|
||||
//
|
||||
// Regression for a real disc (John Wick: Chapter 2) whose transport
|
||||
// stream has a multi-PID continuity-counter gap at ~6:49: before this
|
||||
// fix the TrueHD path cleared its buffer and resumed on the next NON-major
|
||||
// AU, shipping a decoder-choking seam (559 `restart header sync` errors),
|
||||
// while AC-3/DTS on the same disc were fine because THEIR frames are
|
||||
// independently decodable. Video already resyncs to a keyframe after a
|
||||
// gap (see `mux::resync`); this gives TrueHD the equivalent, keyed on the
|
||||
// codec's own re-init point (a major-sync AU).
|
||||
let mut parser = TrueHdParser::new();
|
||||
|
||||
// Establish a baseline so num_substreams is known and the gate is live.
|
||||
let mut pre = valid_major_sync();
|
||||
pre.extend_from_slice(&valid_normal_au());
|
||||
let pre_frames = parser.parse(&make_pes(pre, Some(90000)));
|
||||
assert_eq!(pre_frames.len(), 2, "baseline: major sync + one normal AU");
|
||||
|
||||
// Post-gap PES: two normal AUs, then a valid major sync, then a normal AU.
|
||||
// `discontinuity: true` says packets were lost before this PES.
|
||||
let mut post = valid_normal_au();
|
||||
post.extend_from_slice(&valid_normal_au());
|
||||
post.extend_from_slice(&valid_major_sync());
|
||||
post.extend_from_slice(&valid_normal_au());
|
||||
let post_pes = PesPacket {
|
||||
source: None,
|
||||
pid: 0x1100,
|
||||
pts: Some(90000 + 4 * 900),
|
||||
dts: None,
|
||||
data: post,
|
||||
discontinuity: true,
|
||||
};
|
||||
let mut frames = parser.parse(&post_pes);
|
||||
frames.extend(parser.flush());
|
||||
|
||||
// Only the major sync and the AU after it survive; the two leading
|
||||
// post-gap normal AUs are dropped for lack of a re-init point.
|
||||
assert_eq!(
|
||||
frames.len(),
|
||||
2,
|
||||
"resume only at the major sync + what follows it (got {})",
|
||||
frames.len()
|
||||
);
|
||||
assert!(
|
||||
frames[0].keyframe,
|
||||
"the first surviving post-gap frame MUST be a major sync (re-init \
|
||||
point), never a mid-stream AU"
|
||||
);
|
||||
assert!(
|
||||
!frames[1].keyframe,
|
||||
"the AU after the re-init point is a normal AU"
|
||||
);
|
||||
assert_eq!(
|
||||
parser.dropped_frames(),
|
||||
2,
|
||||
"the two post-gap AUs before the major sync are dropped as collateral"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crc_failed_head_major_sync_is_kept_not_track_killed() {
|
||||
// REGRESSION (every TrueHD title muxed after the checksum gate landed): a
|
||||
|
||||
+9
-3
@@ -11,9 +11,15 @@
|
||||
//! and resume cleanly there. The gap rounds up to (at most) one GOP — the price
|
||||
//! of never emitting a dangling reference; it is logged.
|
||||
//!
|
||||
//! Audio and subtitle frames are independent (no inter-frame references), so a
|
||||
//! gap there costs only the single already-dropped frame; the gate is a no-op
|
||||
//! for non-video tracks (it always admits).
|
||||
//! Most audio and all subtitle frames are independently decodable (each frame
|
||||
//! re-inits on its own header), so a gap there costs only the single already-
|
||||
//! dropped frame and this gate is a no-op for them (it always admits). The one
|
||||
//! exception is TrueHD/MLP, whose predictor + restart state spans access units:
|
||||
//! its re-init point is a codec-specific major-sync AU (not a generic keyframe),
|
||||
//! so it runs the equivalent drop-forward-to-major-sync inside `codec::truehd`
|
||||
//! rather than through this gate. In short: this gate is NOT keyed on "audio vs
|
||||
//! video" but on "needs a re-init point after a gap" — video here, TrueHD in its
|
||||
//! own parser, everything else genuinely independent.
|
||||
|
||||
/// Per-track keyframe-resync state. One gate per elementary stream; a video
|
||||
/// track's gate stays "armed" from a discontinuity until the next keyframe.
|
||||
|
||||
Reference in New Issue
Block a user