Preserve interlaced scan type in label and MKV output
Interlacing is detected upstream (PAL DVD -> R576i) but was dropped in two places: the video label hardcoded a 'p' suffix, and the muxer never wrote any scan-type flag, so MediaInfo inferred progressive and reported 576p for a 576i source. - Add Resolution::is_interlaced() for the R*i variants. - generate_video_label now branches i/p for the heights that can be interlaced (1080, 576, 480) instead of always emitting 'p'. - MkvTrack carries interlaced + field_order; the video serializer emits FlagInterlaced (0x9A; 1=interlaced, 2=progressive) and, for interlaced content, FieldOrder (0x9D) - bottom-field-first for PAL 576i, top-field-first otherwise. Adds the EBML constants.
This commit is contained in:
@@ -655,6 +655,14 @@ impl Resolution {
|
|||||||
matches!(self, Resolution::R2160p | Resolution::R4320p)
|
matches!(self, Resolution::R2160p | Resolution::R4320p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True if this is an interlaced resolution (the `R*i` variants).
|
||||||
|
pub fn is_interlaced(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Resolution::R480i | Resolution::R576i | Resolution::R1080i
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// True if this is an HD (720p+) resolution.
|
/// True if this is an HD (720p+) resolution.
|
||||||
pub fn is_hd(&self) -> bool {
|
pub fn is_hd(&self) -> bool {
|
||||||
!matches!(
|
!matches!(
|
||||||
|
|||||||
+59
-5
@@ -296,7 +296,13 @@ pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
|
|||||||
} else {
|
} else {
|
||||||
v.resolution.pixels()
|
v.resolution.pixels()
|
||||||
};
|
};
|
||||||
v.label = generate_video_label(&v.codec, px, &v.hdr, v.secondary);
|
v.label = generate_video_label(
|
||||||
|
&v.codec,
|
||||||
|
px,
|
||||||
|
v.resolution.is_interlaced(),
|
||||||
|
&v.hdr,
|
||||||
|
v.secondary,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Stream::Subtitle(s) if s.forced => {
|
Stream::Subtitle(s) if s.forced => {
|
||||||
// Ensure forced subs are labeled even if BD-J didn't set a name
|
// Ensure forced subs are labeled even if BD-J didn't set a name
|
||||||
@@ -311,6 +317,7 @@ pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
|
|||||||
fn generate_video_label(
|
fn generate_video_label(
|
||||||
codec: &crate::disc::Codec,
|
codec: &crate::disc::Codec,
|
||||||
pixels: (u32, u32),
|
pixels: (u32, u32),
|
||||||
|
interlaced: bool,
|
||||||
hdr: &crate::disc::HdrFormat,
|
hdr: &crate::disc::HdrFormat,
|
||||||
secondary: bool,
|
secondary: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
@@ -331,20 +338,22 @@ fn generate_video_label(
|
|||||||
// Codec
|
// Codec
|
||||||
parts.push(codec.name().to_string());
|
parts.push(codec.name().to_string());
|
||||||
|
|
||||||
// Resolution
|
// Resolution. Scan type (i/p) is honored for heights that can be
|
||||||
|
// interlaced on disc (1080 and SD 576/480); 720/4K/8K are always
|
||||||
|
// progressive.
|
||||||
let (w, h) = pixels;
|
let (w, h) = pixels;
|
||||||
let res = if w >= 7680 {
|
let res = if w >= 7680 {
|
||||||
"8K"
|
"8K"
|
||||||
} else if w >= 3840 {
|
} else if w >= 3840 {
|
||||||
"4K"
|
"4K"
|
||||||
} else if w >= 1920 {
|
} else if w >= 1920 {
|
||||||
"1080p"
|
if interlaced { "1080i" } else { "1080p" }
|
||||||
} else if w >= 1280 {
|
} else if w >= 1280 {
|
||||||
"720p"
|
"720p"
|
||||||
} else if h >= 576 {
|
} else if h >= 576 {
|
||||||
"576p"
|
if interlaced { "576i" } else { "576p" }
|
||||||
} else if h >= 480 {
|
} else if h >= 480 {
|
||||||
"480p"
|
if interlaced { "480i" } else { "480p" }
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
@@ -1736,6 +1745,51 @@ mod apply_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spec: an interlaced resolution (`R*i`) must surface the "i" scan type
|
||||||
|
/// in the generated label, not a hardcoded "p". PAL DVD is 576i.
|
||||||
|
/// Mutation: hardcode "p" → 576i video mislabeled as 576p.
|
||||||
|
#[test]
|
||||||
|
fn fill_defaults_video_label_honors_interlaced_scan_type() {
|
||||||
|
let interlaced = Stream::Video(VideoStream {
|
||||||
|
pid: 0x1011,
|
||||||
|
codec: Codec::Mpeg2,
|
||||||
|
resolution: Resolution::R576i,
|
||||||
|
frame_rate: FrameRate::F25,
|
||||||
|
hdr: HdrFormat::Sdr,
|
||||||
|
color_space: ColorSpace::Bt470bg,
|
||||||
|
display_aspect: None,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
});
|
||||||
|
let mut titles = vec![title_with(vec![interlaced])];
|
||||||
|
fill_defaults(&mut titles);
|
||||||
|
if let Stream::Video(v) = &titles[0].streams[0] {
|
||||||
|
assert!(v.label.contains("576i"), "expected 576i, got {}", v.label);
|
||||||
|
assert!(
|
||||||
|
!v.label.contains("576p"),
|
||||||
|
"must not say 576p, got {}",
|
||||||
|
v.label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let progressive = Stream::Video(VideoStream {
|
||||||
|
pid: 0x1011,
|
||||||
|
codec: Codec::Mpeg2,
|
||||||
|
resolution: Resolution::R576p,
|
||||||
|
frame_rate: FrameRate::F25,
|
||||||
|
hdr: HdrFormat::Sdr,
|
||||||
|
color_space: ColorSpace::Bt470bg,
|
||||||
|
display_aspect: None,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
});
|
||||||
|
let mut titles = vec![title_with(vec![progressive])];
|
||||||
|
fill_defaults(&mut titles);
|
||||||
|
if let Stream::Video(v) = &titles[0].streams[0] {
|
||||||
|
assert!(v.label.contains("576p"), "expected 576p, got {}", v.label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── codec_hint_consistent hardening ───────────────────────────────────────
|
// ── codec_hint_consistent hardening ───────────────────────────────────────
|
||||||
|
|
||||||
/// Spec: "Dolby Digital" (AC-3) hint is consistent ONLY with AC-3 streams;
|
/// Spec: "Dolby Digital" (AC-3) hint is consistent ONLY with AC-3 streams;
|
||||||
|
|||||||
@@ -424,6 +424,18 @@ pub const DEFAULT_DURATION: u32 = 0x23_E383;
|
|||||||
pub const VIDEO: u32 = 0xE0;
|
pub const VIDEO: u32 = 0xE0;
|
||||||
pub const PIXEL_WIDTH: u32 = 0xB0;
|
pub const PIXEL_WIDTH: u32 = 0xB0;
|
||||||
pub const PIXEL_HEIGHT: u32 = 0xBA;
|
pub const PIXEL_HEIGHT: u32 = 0xBA;
|
||||||
|
// Scan type (children of Video).
|
||||||
|
pub const FLAG_INTERLACED: u32 = 0x9A;
|
||||||
|
pub const FIELD_ORDER: u32 = 0x9D;
|
||||||
|
// FlagInterlaced values: 1 = interlaced, 2 = progressive (0 = undetermined).
|
||||||
|
pub const INTERLACED_INTERLACED: u64 = 1;
|
||||||
|
pub const INTERLACED_PROGRESSIVE: u64 = 2;
|
||||||
|
// FieldOrder values (Matroska): 0/2 = top-field-first, 1/9 = bottom-field-first.
|
||||||
|
// NTSC DVD (480i) and HD (1080i) are top-field-first; PAL DVD (576i) is
|
||||||
|
// bottom-field-first. 0xFF is our sentinel for "undetermined / omit".
|
||||||
|
pub const FIELD_ORDER_TFF: u8 = 2;
|
||||||
|
pub const FIELD_ORDER_BFF: u8 = 9;
|
||||||
|
pub const FIELD_ORDER_UNDETERMINED: u8 = 0xFF;
|
||||||
pub const DISPLAY_WIDTH: u32 = 0x54B0;
|
pub const DISPLAY_WIDTH: u32 = 0x54B0;
|
||||||
pub const DISPLAY_HEIGHT: u32 = 0x54BA;
|
pub const DISPLAY_HEIGHT: u32 = 0x54BA;
|
||||||
pub const COLOUR: u32 = 0x55B0;
|
pub const COLOUR: u32 = 0x55B0;
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ pub struct MkvTrack {
|
|||||||
pub colour_transfer: u8, // TransferCharacteristics (16=smpte2084/PQ)
|
pub colour_transfer: u8, // TransferCharacteristics (16=smpte2084/PQ)
|
||||||
pub colour_primaries: u8, // Primaries (9=bt2020)
|
pub colour_primaries: u8, // Primaries (9=bt2020)
|
||||||
pub colour_range: u8, // Range (1=tv/limited)
|
pub colour_range: u8, // Range (1=tv/limited)
|
||||||
|
// Scan type. `interlaced` drives FlagInterlaced (0x9A): true → 1
|
||||||
|
// (interlaced), false → 2 (progressive). `field_order` (0x9D) is only
|
||||||
|
// meaningful when interlaced; `FIELD_ORDER_UNDETERMINED` omits it.
|
||||||
|
pub interlaced: bool,
|
||||||
|
pub field_order: u8,
|
||||||
// Audio-specific
|
// Audio-specific
|
||||||
pub sample_rate: f64,
|
pub sample_rate: f64,
|
||||||
pub channels: u8,
|
pub channels: u8,
|
||||||
@@ -125,6 +130,18 @@ impl MkvTrack {
|
|||||||
colour_transfer: transfer,
|
colour_transfer: transfer,
|
||||||
colour_primaries: primaries,
|
colour_primaries: primaries,
|
||||||
colour_range: range,
|
colour_range: range,
|
||||||
|
interlaced: v.resolution.is_interlaced(),
|
||||||
|
// PAL DVD (576i) is bottom-field-first; NTSC DVD (480i) is
|
||||||
|
// top-field-first. HD interlaced (1080i) is top-field-first.
|
||||||
|
// Progressive content leaves the field order undetermined.
|
||||||
|
field_order: if v.resolution.is_interlaced() {
|
||||||
|
match v.resolution {
|
||||||
|
Resolution::R576i => ebml::FIELD_ORDER_BFF,
|
||||||
|
_ => ebml::FIELD_ORDER_TFF,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ebml::FIELD_ORDER_UNDETERMINED
|
||||||
|
},
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -195,6 +212,8 @@ impl MkvTrack {
|
|||||||
colour_transfer: 0,
|
colour_transfer: 0,
|
||||||
colour_primaries: 0,
|
colour_primaries: 0,
|
||||||
colour_range: 0,
|
colour_range: 0,
|
||||||
|
interlaced: false,
|
||||||
|
field_order: ebml::FIELD_ORDER_UNDETERMINED,
|
||||||
sample_rate: sr,
|
sample_rate: sr,
|
||||||
channels: ch,
|
channels: ch,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -228,6 +247,8 @@ impl MkvTrack {
|
|||||||
colour_transfer: 0,
|
colour_transfer: 0,
|
||||||
colour_primaries: 0,
|
colour_primaries: 0,
|
||||||
colour_range: 0,
|
colour_range: 0,
|
||||||
|
interlaced: false,
|
||||||
|
field_order: ebml::FIELD_ORDER_UNDETERMINED,
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -666,6 +687,21 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?;
|
let vid_pos = ebml::start_master(&mut writer, ebml::VIDEO)?;
|
||||||
ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?;
|
ebml::write_uint(&mut writer, ebml::PIXEL_WIDTH, track.pixel_width as u64)?;
|
||||||
ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?;
|
ebml::write_uint(&mut writer, ebml::PIXEL_HEIGHT, track.pixel_height as u64)?;
|
||||||
|
// Scan type. FlagInterlaced: 1 = interlaced, 2 = progressive.
|
||||||
|
// FieldOrder is only written for interlaced content with a
|
||||||
|
// determined order (TFF=0/2/6/14, BFF=1/9/13...).
|
||||||
|
ebml::write_uint(
|
||||||
|
&mut writer,
|
||||||
|
ebml::FLAG_INTERLACED,
|
||||||
|
if track.interlaced {
|
||||||
|
ebml::INTERLACED_INTERLACED
|
||||||
|
} else {
|
||||||
|
ebml::INTERLACED_PROGRESSIVE
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
if track.interlaced && track.field_order != ebml::FIELD_ORDER_UNDETERMINED {
|
||||||
|
ebml::write_uint(&mut writer, ebml::FIELD_ORDER, track.field_order as u64)?;
|
||||||
|
}
|
||||||
if track.display_width > 0 && track.display_height > 0 {
|
if track.display_width > 0 && track.display_height > 0 {
|
||||||
ebml::write_uint(&mut writer, ebml::DISPLAY_WIDTH, track.display_width as u64)?;
|
ebml::write_uint(&mut writer, ebml::DISPLAY_WIDTH, track.display_width as u64)?;
|
||||||
ebml::write_uint(
|
ebml::write_uint(
|
||||||
@@ -1161,6 +1197,8 @@ mod tests {
|
|||||||
colour_transfer: 0,
|
colour_transfer: 0,
|
||||||
colour_primaries: 0,
|
colour_primaries: 0,
|
||||||
colour_range: 0,
|
colour_range: 0,
|
||||||
|
interlaced: false,
|
||||||
|
field_order: ebml::FIELD_ORDER_UNDETERMINED,
|
||||||
sample_rate: 0.0,
|
sample_rate: 0.0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -1186,6 +1224,8 @@ mod tests {
|
|||||||
colour_transfer: 0,
|
colour_transfer: 0,
|
||||||
colour_primaries: 0,
|
colour_primaries: 0,
|
||||||
colour_range: 0,
|
colour_range: 0,
|
||||||
|
interlaced: false,
|
||||||
|
field_order: ebml::FIELD_ORDER_UNDETERMINED,
|
||||||
sample_rate: 48000.0,
|
sample_rate: 48000.0,
|
||||||
channels: 6,
|
channels: 6,
|
||||||
bit_depth: 0,
|
bit_depth: 0,
|
||||||
@@ -2944,6 +2984,52 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn video_emits_flag_interlaced_and_field_order() {
|
||||||
|
// An interlaced (576i PAL) track must emit FlagInterlaced=1 and
|
||||||
|
// FieldOrder=9 (bottom-field-first). A progressive track must emit
|
||||||
|
// FlagInterlaced=2 and NO FieldOrder.
|
||||||
|
let mut interlaced = make_video_track();
|
||||||
|
interlaced.interlaced = true;
|
||||||
|
interlaced.field_order = ebml::FIELD_ORDER_BFF;
|
||||||
|
let muxer = MkvMuxer::new(Cursor::new(Vec::new()), &[interlaced], None, 0.0, &[]).unwrap();
|
||||||
|
let data = muxer.writer.into_inner();
|
||||||
|
let fi = find_id(&data, ebml::FLAG_INTERLACED).expect("FlagInterlaced present");
|
||||||
|
// [id=0x9A][size=0x81][value]
|
||||||
|
assert_eq!(
|
||||||
|
data[fi + 2],
|
||||||
|
ebml::INTERLACED_INTERLACED as u8,
|
||||||
|
"FlagInterlaced must be 1 (interlaced)"
|
||||||
|
);
|
||||||
|
let fo = find_id(&data, ebml::FIELD_ORDER).expect("FieldOrder present");
|
||||||
|
assert_eq!(
|
||||||
|
data[fo + 2],
|
||||||
|
ebml::FIELD_ORDER_BFF,
|
||||||
|
"FieldOrder must be 9 (bottom-field-first) for PAL DVD"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Progressive track: FlagInterlaced=2, no FieldOrder.
|
||||||
|
let muxer = MkvMuxer::new(
|
||||||
|
Cursor::new(Vec::new()),
|
||||||
|
&[make_video_track()],
|
||||||
|
None,
|
||||||
|
0.0,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let data = muxer.writer.into_inner();
|
||||||
|
let fi = find_id(&data, ebml::FLAG_INTERLACED).expect("FlagInterlaced present");
|
||||||
|
assert_eq!(
|
||||||
|
data[fi + 2],
|
||||||
|
ebml::INTERLACED_PROGRESSIVE as u8,
|
||||||
|
"FlagInterlaced must be 2 (progressive)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
find_id(&data, ebml::FIELD_ORDER).is_none(),
|
||||||
|
"no FieldOrder for progressive content"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dolby_vision_track_emits_block_addition_mapping() {
|
fn dolby_vision_track_emits_block_addition_mapping() {
|
||||||
// A DV track (dv_config set) must emit BlockAdditionMapping (0x41E4)
|
// A DV track (dv_config set) must emit BlockAdditionMapping (0x41E4)
|
||||||
|
|||||||
Reference in New Issue
Block a user