mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
disc: wire bdnav as the top nav-feature tier in main-feature selection
scan_with runs the bdnav VM for BDMV discs and feeds the video-gated result as the new top MAIN_FEATURE_ORDER key (nav-feature > authoring-feature > standalone > has-video). A nav result is only honoured for a video-bearing title, so a mis-resolve can never reopen the decoy hole; when the VM abstains (BD-J, non-convergence) the structural/heuristic order decides exactly as before. Adds nav selection tests and updates the diag decision reason.
This commit is contained in:
+1
-1
@@ -691,7 +691,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
reason,
|
||||
"main_feature_order(authoring-feature, standalone, has-video, fits-disc, largest-size, longest, richest-audio)",
|
||||
"main_feature_order(nav-feature, authoring-feature, standalone, has-video, fits-disc, largest-size, longest, richest-audio)",
|
||||
"the reason must name the selection keys (authoring, standalone-over-composite, and has-video gates, then the physical keys) in priority order"
|
||||
);
|
||||
}
|
||||
|
||||
+128
-29
@@ -192,6 +192,10 @@ pub struct Clip {
|
||||
/// threaded into the comparator.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TitleRank {
|
||||
/// The disc's own HDMV navigation (played like a real player by the
|
||||
/// [`crate::bdnav`] VM) reaches this video-bearing title as the feature —
|
||||
/// the authoritative `nav-feature` signal, above `authoring`.
|
||||
pub nav: bool,
|
||||
/// The disc's authoring names this (video-bearing, non-composite,
|
||||
/// duration-corroborated) title as the feature.
|
||||
pub authoring: bool,
|
||||
@@ -2137,16 +2141,48 @@ impl Disc {
|
||||
let meta_title = Self::read_meta_title(reader, &udf_fs);
|
||||
let feature_hint = crate::labels::apply(reader, &udf_fs, &mut titles);
|
||||
|
||||
// The authoritative signal: run the disc's own HDMV navigation the way a
|
||||
// real player would (First-Play in the `bdnav` VM) and see which playlist
|
||||
// it plays as the feature. Only for a BDMV tree; the VM abstains on BD-J
|
||||
// discs and any non-convergence, returning `None`. Restrict the accepted
|
||||
// result to video-bearing, non-trivial titles (duration within half of
|
||||
// the longest video title's) so a short logo/pre-roll `PlayPlayList`
|
||||
// cannot be mistaken for the feature.
|
||||
let nav_feature = if udf_fs.find_dir("/BDMV").is_some() {
|
||||
let longest_video_dur = titles
|
||||
.iter()
|
||||
.filter(|t| t.has_probable_video())
|
||||
.map(|t| t.duration_secs)
|
||||
.fold(0.0_f64, f64::max);
|
||||
let candidates: std::collections::HashSet<u16> = titles
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
t.has_video()
|
||||
&& (longest_video_dur <= 0.0 || t.duration_secs >= 0.5 * longest_video_dur)
|
||||
})
|
||||
.map(|t| t.playlist_id)
|
||||
.collect();
|
||||
crate::bdnav::resolve_feature(reader, &udf_fs, |id| candidates.contains(&id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Authoritative main-feature ordering. The physical `canonical_title_order`
|
||||
// sort above is refined once labels have run: a play-all / wrapper COMPOSITE
|
||||
// (e.g. Spider-Man 3 UHD `00245` = `[bumper][00001 feature clip][outro]`) is
|
||||
// demoted below the standalone title it wraps, a title with no plausible
|
||||
// video is dropped, and a feature the disc's own menu authoring designates
|
||||
// (`feature_hint`) is promoted outright. After this, `titles[0]` is the
|
||||
// selected main feature. The `canonical_title_order` sort is kept first
|
||||
// because label ANCHORING (`labels::apply`, above) reads titles in
|
||||
// duration-descending order.
|
||||
Self::sort_titles_by_main_feature(&mut titles, capacity_bytes, feature_hint.as_ref());
|
||||
// sort above is refined once labels have run: the disc's own navigation
|
||||
// pick (`nav_feature`) wins outright when present; otherwise a play-all /
|
||||
// wrapper COMPOSITE (e.g. Spider-Man 3 UHD `00245` =
|
||||
// `[bumper][00001 feature clip][outro]`) is demoted below the standalone
|
||||
// title it wraps, a title with no plausible video is dropped, and a
|
||||
// feature the disc's own menu authoring designates (`feature_hint`) is
|
||||
// promoted. After this, `titles[0]` is the selected main feature. The
|
||||
// `canonical_title_order` sort is kept first because label ANCHORING
|
||||
// (`labels::apply`, above) reads titles in duration-descending order.
|
||||
Self::sort_titles_by_main_feature(
|
||||
&mut titles,
|
||||
capacity_bytes,
|
||||
feature_hint.as_ref(),
|
||||
nav_feature,
|
||||
);
|
||||
|
||||
// Optional content-based forced-subtitle detection. `info` opts in so its
|
||||
// forced flags match what the muxer derives during a rip (both use the
|
||||
@@ -2350,10 +2386,12 @@ impl Disc {
|
||||
/// decision row can NAME the ordering instead of restating it (same
|
||||
/// anti-drift contract as `CANONICAL_TITLE_ORDER_KEYS`).
|
||||
///
|
||||
/// (A future `nav-feature` key — the BD navigation resolver's own play-path
|
||||
/// pick — is reserved as the top slot ABOVE `authoring-feature`; it is not
|
||||
/// yet wired, so it is absent from this list rather than named-but-inert.)
|
||||
///
|
||||
/// - `nav-feature`: the title the disc's own HDMV navigation plays as the
|
||||
/// feature, resolved by running First-Play in the [`crate::bdnav`] VM the
|
||||
/// way a real player would. This is the authoritative, size-independent
|
||||
/// pick; it is video-gated (a mis-resolve cannot select a streamless
|
||||
/// playlist) and simply absent when the VM abstains (BD-J discs, or
|
||||
/// non-convergence), in which case the lower keys decide.
|
||||
/// - `authoring-feature`: a title the disc's own menu authoring designates
|
||||
/// as the feature (has video, is not itself a composite, and is within a
|
||||
/// sane fraction of the longest title's duration) outranks everything
|
||||
@@ -2368,8 +2406,12 @@ impl Disc {
|
||||
/// - `has-video`: a title with no plausible video content (see
|
||||
/// [`DiscTitle::has_probable_video`]) is demoted below every title that
|
||||
/// has some — a floor that drops streamless menu/metadata playlists.
|
||||
pub const MAIN_FEATURE_ORDER_KEYS: &'static [&'static str] =
|
||||
&["authoring-feature", "standalone", "has-video"];
|
||||
pub const MAIN_FEATURE_ORDER_KEYS: &'static [&'static str] = &[
|
||||
"nav-feature",
|
||||
"authoring-feature",
|
||||
"standalone",
|
||||
"has-video",
|
||||
];
|
||||
|
||||
/// Precompute the [`TitleRank`] for every title, in the same index order.
|
||||
///
|
||||
@@ -2390,6 +2432,7 @@ impl Disc {
|
||||
pub fn rank_titles(
|
||||
titles: &[DiscTitle],
|
||||
hint: Option<&crate::labels::FeaturePlaylistHint>,
|
||||
nav_feature: Option<u16>,
|
||||
) -> Vec<TitleRank> {
|
||||
let clip_sets: Vec<std::collections::BTreeSet<&str>> = titles
|
||||
.iter()
|
||||
@@ -2428,7 +2471,12 @@ impl Disc {
|
||||
&& hint.is_some_and(|h| h.matches(p.playlist_id, &p.playlist))
|
||||
&& p.has_video()
|
||||
&& (longest_video_dur <= 0.0 || p.duration_secs >= 0.5 * longest_video_dur);
|
||||
// The nav result is video-gated exactly like the authoring hint,
|
||||
// so a mis-resolved playlist that happens to be streamless can
|
||||
// never win (and thus cannot reopen the decoy hole).
|
||||
let nav = nav_feature == Some(p.playlist_id) && p.has_video();
|
||||
TitleRank {
|
||||
nav,
|
||||
authoring,
|
||||
composite,
|
||||
}
|
||||
@@ -2449,8 +2497,9 @@ impl Disc {
|
||||
ra: &TitleRank,
|
||||
rb: &TitleRank,
|
||||
) -> std::cmp::Ordering {
|
||||
rb.authoring
|
||||
.cmp(&ra.authoring)
|
||||
rb.nav
|
||||
.cmp(&ra.nav)
|
||||
.then_with(|| rb.authoring.cmp(&ra.authoring))
|
||||
.then_with(|| ra.composite.cmp(&rb.composite))
|
||||
.then_with(|| b.has_probable_video().cmp(&a.has_probable_video()))
|
||||
.then_with(|| Self::canonical_title_order(a, b, capacity_bytes))
|
||||
@@ -2464,8 +2513,9 @@ impl Disc {
|
||||
titles: &mut Vec<DiscTitle>,
|
||||
capacity_bytes: u64,
|
||||
hint: Option<&crate::labels::FeaturePlaylistHint>,
|
||||
nav_feature: Option<u16>,
|
||||
) {
|
||||
let ranks = Self::rank_titles(titles, hint);
|
||||
let ranks = Self::rank_titles(titles, hint, nav_feature);
|
||||
let mut decorated: Vec<(TitleRank, DiscTitle)> =
|
||||
ranks.into_iter().zip(std::mem::take(titles)).collect();
|
||||
decorated
|
||||
@@ -4965,12 +5015,12 @@ mod tests {
|
||||
"pre-gate physical order would pick the wrapper composite decoy"
|
||||
);
|
||||
// The decoy is classified a composite; the feature is standalone.
|
||||
let ranks = Disc::rank_titles(&[decoy.clone(), feature.clone()], None);
|
||||
let ranks = Disc::rank_titles(&[decoy.clone(), feature.clone()], None, None);
|
||||
assert!(ranks[0].composite, "the wrapper decoy is a composite");
|
||||
assert!(!ranks[1].composite, "the feature it wraps is standalone");
|
||||
|
||||
let mut titles = vec![decoy, feature, extra];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"the standalone feature is selected as titles[0]"
|
||||
@@ -4998,7 +5048,7 @@ mod tests {
|
||||
let capacity = 88_000_000_000u64;
|
||||
|
||||
let mut titles = vec![composite, feature];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"a video-bearing composite must still lose to the standalone feature it contains"
|
||||
@@ -5022,7 +5072,7 @@ mod tests {
|
||||
let capacity = 0u64; // unknown → oversize gate inert
|
||||
|
||||
let mut titles = vec![playall, feature];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 800,
|
||||
"the standalone feature wins even when the oversize gate is disabled"
|
||||
@@ -5043,7 +5093,7 @@ mod tests {
|
||||
|
||||
// No hint: the larger generic title wins on size.
|
||||
let mut no_hint = vec![authored.clone(), generic.clone()];
|
||||
Disc::sort_titles_by_main_feature(&mut no_hint, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut no_hint, capacity, None, None);
|
||||
assert_eq!(
|
||||
no_hint[0].playlist_id, 1,
|
||||
"without a hint the larger generic title wins"
|
||||
@@ -5051,13 +5101,62 @@ mod tests {
|
||||
|
||||
// With the hint: the authored feature wins outright despite being smaller.
|
||||
let mut titles = vec![generic, authored];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint));
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint), None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 222,
|
||||
"the authoring hint promotes the designated feature above the larger title"
|
||||
);
|
||||
}
|
||||
|
||||
/// The disc's own HDMV navigation pick (`nav_feature`, resolved by the
|
||||
/// `bdnav` VM playing First-Play like a real player) wins outright — above
|
||||
/// the authoring hint and the physical size order — when the nav-resolved
|
||||
/// title has video.
|
||||
#[test]
|
||||
fn nav_feature_promotes_navigated_title_over_larger() {
|
||||
let navigated = bd_title("00222.mpls", 222, 6000.0, 40_000_000_000, &["00222"]);
|
||||
let generic = bd_title("00001.mpls", 1, 8000.0, 80_000_000_000, &["00001"]);
|
||||
let capacity = 100_000_000_000u64;
|
||||
|
||||
// Nav resolves 222 → it wins despite the generic title being larger/longer.
|
||||
let mut titles = vec![generic.clone(), navigated.clone()];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, Some(222));
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 222,
|
||||
"the disc's own navigation pick wins outright"
|
||||
);
|
||||
|
||||
// Sanity: without a nav result, the larger generic title wins.
|
||||
let mut titles = vec![generic, navigated];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(titles[0].playlist_id, 1);
|
||||
}
|
||||
|
||||
/// A nav result pointing at a STREAMLESS title is ignored (the `nav-feature`
|
||||
/// key is video-gated exactly like the authoring hint), so a mis-resolve can
|
||||
/// never select a decoy; selection falls through to the normal order.
|
||||
#[test]
|
||||
fn nav_feature_ignored_when_target_is_streamless() {
|
||||
// The SM3-shaped decoy: streamless wrapper composite of the feature's clip.
|
||||
let streamless = bd_title_no_stn_video(
|
||||
"00245.mpls",
|
||||
245,
|
||||
8350.0,
|
||||
78_000_000_000,
|
||||
&["00339", "00001", "00336"],
|
||||
);
|
||||
let feature = bd_title("00001.mpls", 1, 8000.0, 76_000_000_000, &["00001"]);
|
||||
let capacity = 88_000_000_000u64;
|
||||
|
||||
let mut titles = vec![streamless, feature];
|
||||
// Nav (wrongly) points at the streamless 245 — must be ignored.
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, Some(245));
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"a streamless nav target is ignored (video-gated), so the real feature wins"
|
||||
);
|
||||
}
|
||||
|
||||
/// A hint pointing at a STREAMLESS entry must not resurrect the decoy bug:
|
||||
/// the authoring preference requires the designated title to have real video
|
||||
/// streams, and a clip-less streamless title fails the `has-video` floor too.
|
||||
@@ -5076,7 +5175,7 @@ mod tests {
|
||||
let capacity = 100_000_000_000u64;
|
||||
|
||||
let mut titles = vec![streamless, feature];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint));
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint), None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"a hint at a streamless title must not override the has-video floor"
|
||||
@@ -5096,7 +5195,7 @@ mod tests {
|
||||
let capacity = 100_000_000_000u64;
|
||||
|
||||
let mut titles = vec![branch, feature];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint));
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, Some(&hint), None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"a hint at a 9-minute branch must not beat the 2h13m feature"
|
||||
@@ -5118,7 +5217,7 @@ mod tests {
|
||||
"the bumper-lead feature has no first-PlayItem video but IS probable video"
|
||||
);
|
||||
let mut titles = vec![bonus, feature];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 1,
|
||||
"the bumper-lead feature is selected, not the small bonus"
|
||||
@@ -5152,7 +5251,7 @@ mod tests {
|
||||
let capacity = 50_000_000_000u64;
|
||||
|
||||
let mut titles = vec![b, a];
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None);
|
||||
Disc::sort_titles_by_main_feature(&mut titles, capacity, None, None);
|
||||
assert_eq!(
|
||||
titles[0].playlist_id, 10,
|
||||
"the largest title is the deterministic fallback pick"
|
||||
@@ -5172,7 +5271,7 @@ mod tests {
|
||||
fn equal_clip_twins_are_not_composite() {
|
||||
let t0 = bd_title("00800.mpls", 800, 7200.0, 57_000_000_000, &["00001"]);
|
||||
let t1 = bd_title("00801.mpls", 801, 7200.0, 56_000_000_000, &["00001"]);
|
||||
let ranks = Disc::rank_titles(&[t0, t1], None);
|
||||
let ranks = Disc::rank_titles(&[t0, t1], None, None);
|
||||
assert!(
|
||||
!ranks[0].composite && !ranks[1].composite,
|
||||
"identical-clip twins must not be flagged composite"
|
||||
|
||||
Reference in New Issue
Block a user