disc: detect forced PGS subtitles from stream content for info
Give `info` the same forced-subtitle verdict the muxer derives during a rip, so the two agree. A shared classifier (mux::codec::pgs::ForcedTracker) folds a PGS track's display sets — forced iff every one carries the forced_on_flag — and is used by BOTH the MKV writer and a new scan-time probe that reads the title's PGS streams (reusing the TS demuxer and PGS parser). The probe only overrides a track it actually observed content for, so an undecrypted/unread stream keeps its vendor-derived flag. Gated behind ScanOptions::probe_forced_subtitles (off for the rip path, which detects forced while muxing without a second read).
This commit is contained in:
@@ -57,6 +57,64 @@ pub fn display_set_is_forced(frame_data: &[u8]) -> Option<bool> {
|
||||
Some(flags & PCS_FORCED_ON_FLAG != 0)
|
||||
}
|
||||
|
||||
/// Accumulates the "is this PGS subtitle track a forced-narrative track?" verdict
|
||||
/// from its display sets. A track is forced iff it displayed at least one subtitle
|
||||
/// and EVERY display set carried the forced_on_flag — a dedicated forced track,
|
||||
/// as opposed to a full track that merely has occasional forced signs.
|
||||
///
|
||||
/// This is the SINGLE classification used by both the MKV muxer (accumulating a
|
||||
/// track's frames during a rip) and the `info`-time forced probe (feeding the
|
||||
/// demuxed display sets), so both reach the identical verdict.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ForcedTracker {
|
||||
has_display: bool,
|
||||
all_forced: bool,
|
||||
}
|
||||
|
||||
impl Default for ForcedTracker {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
has_display: false,
|
||||
all_forced: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForcedTracker {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Fold one emitted PGS block into the verdict. Non-display blocks (clear
|
||||
/// PCS, other segments) are ignored.
|
||||
pub fn observe(&mut self, frame_data: &[u8]) {
|
||||
if let Some(forced) = display_set_is_forced(frame_data) {
|
||||
self.has_display = true;
|
||||
self.all_forced &= forced;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the track has already shown a NON-forced subtitle — i.e. its
|
||||
/// verdict is settled at "not forced" and further observation can be skipped
|
||||
/// (the early-exit the probe uses to avoid reading the whole clip).
|
||||
pub fn settled_not_forced(&self) -> bool {
|
||||
self.has_display && !self.all_forced
|
||||
}
|
||||
|
||||
/// Whether ANY display set was observed. When false the track's forced state
|
||||
/// is unknown (no PGS content seen — e.g. an undecrypted/unread stream), so a
|
||||
/// probe should leave any existing (vendor-derived) flag untouched rather
|
||||
/// than assert "not forced".
|
||||
pub fn observed(&self) -> bool {
|
||||
self.has_display
|
||||
}
|
||||
|
||||
/// Final verdict: forced iff it displayed subtitles and every one was forced.
|
||||
pub fn is_forced(&self) -> bool {
|
||||
self.has_display && self.all_forced
|
||||
}
|
||||
}
|
||||
|
||||
/// Stateful parser that collapses PGS display/clear PCS pairs into
|
||||
/// duration-bearing Matroska frames. Implements [`CodecParser`].
|
||||
pub struct PgsParser {
|
||||
|
||||
+6
-13
@@ -704,12 +704,9 @@ struct Ac3ChannelFixup {
|
||||
struct PgsForcedFixup {
|
||||
/// Absolute file offset of the 1-byte `FlagForced` value in the Tracks element.
|
||||
value_offset: u64,
|
||||
/// Whether the track has displayed at least one subtitle (a display PCS).
|
||||
has_display: bool,
|
||||
/// Whether EVERY display set so far carried the forced_on_flag. Starts true;
|
||||
/// cleared by the first non-forced display set. With `has_display`, a value of
|
||||
/// true at `finish()` means the whole track is forced narrative.
|
||||
all_forced: bool,
|
||||
/// Shared forced-narrative classifier fed the track's display sets. The same
|
||||
/// type drives the `info`-time forced probe, so both classify identically.
|
||||
tracker: super::codec::pgs::ForcedTracker,
|
||||
}
|
||||
|
||||
/// TimestampScale: nanoseconds per Matroska timestamp tick. 0.1 ms (100_000 ns).
|
||||
@@ -984,8 +981,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
i,
|
||||
PgsForcedFixup {
|
||||
value_offset,
|
||||
has_display: false,
|
||||
all_forced: true,
|
||||
tracker: super::codec::pgs::ForcedTracker::new(),
|
||||
},
|
||||
);
|
||||
} else if track.is_forced {
|
||||
@@ -1560,10 +1556,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
// non-forced set appears. `finish()` promotes FlagForced only for a track
|
||||
// that displayed subtitles and had every one forced.
|
||||
if let Some(fixup) = self.pgs_forced_fixups.get_mut(&track_idx) {
|
||||
if let Some(forced) = super::codec::pgs::display_set_is_forced(data) {
|
||||
fixup.has_display = true;
|
||||
fixup.all_forced &= forced;
|
||||
}
|
||||
fixup.tracker.observe(data);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1606,7 +1599,7 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
let forced_offsets: Vec<u64> = self
|
||||
.pgs_forced_fixups
|
||||
.values()
|
||||
.filter(|f| f.has_display && f.all_forced)
|
||||
.filter(|f| f.tracker.is_forced())
|
||||
.map(|f| f.value_offset)
|
||||
.collect();
|
||||
if !forced_offsets.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user