use crate::encoder::bitstream::BitWriter;
use crate::vui::{HrdParameters, VuiParameters};
#[derive(Debug, Clone)]
pub struct BaselineSpsConfig {
pub seq_parameter_set_id: u32,
pub level_idc: u8,
pub width_in_mbs: u32,
pub height_in_mbs: u32,
pub log2_max_frame_num_minus4: u32,
pub log2_max_poc_lsb_minus4: u32,
pub max_num_ref_frames: u32,
pub profile_idc: u8,
pub chroma_format_idc: u32,
pub separate_colour_plane: bool,
pub bit_depth_luma_minus8: u32,
pub bit_depth_chroma_minus8: u32,
pub seq_scaling_lists: Option<super::ScalingListsSpec>,
pub interlaced_fields: bool,
pub mbaff: bool,
pub vui: Option<VuiParameters>,
}
impl Default for BaselineSpsConfig {
fn default() -> Self {
Self {
seq_parameter_set_id: 0,
level_idc: 30,
width_in_mbs: 0,
height_in_mbs: 0,
log2_max_frame_num_minus4: 4,
log2_max_poc_lsb_minus4: 4,
max_num_ref_frames: 1,
profile_idc: 66,
chroma_format_idc: 1,
separate_colour_plane: false,
seq_scaling_lists: None,
interlaced_fields: false,
mbaff: false,
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
vui: None,
}
}
}
pub(crate) fn write_scaling_list_values(w: &mut BitWriter, values: &[i32]) {
let mut last = 8i32;
for &v in values {
debug_assert!(
(1..=255).contains(&v),
"scaling-list value {v} out of the 1..=255 range"
);
let mut delta = v - last;
if delta > 127 {
delta -= 256;
}
if delta < -128 {
delta += 256;
}
w.se(delta);
last = v;
}
}
pub(crate) fn write_scaling_list_slot(
w: &mut BitWriter,
spec: &super::ScalingListsSpec,
list_idx: usize,
) {
match spec {
super::ScalingListsSpec::Default => {
w.se(-8);
}
super::ScalingListsSpec::Custom(l) => {
if list_idx < 6 {
let vals = if list_idx < 3 { &l.intra4 } else { &l.inter4 };
write_scaling_list_values(w, vals);
} else if (list_idx - 6) % 2 == 0 {
write_scaling_list_values(w, &l.intra8);
} else {
write_scaling_list_values(w, &l.inter8);
}
}
}
}
pub fn build_baseline_sps_rbsp(cfg: &BaselineSpsConfig) -> Vec<u8> {
build_sps_rbsp_with_bypass(cfg, false)
}
pub fn build_sps_rbsp_with_bypass(cfg: &BaselineSpsConfig, transform_bypass: bool) -> Vec<u8> {
let mut w = BitWriter::new();
debug_assert!(
matches!(cfg.profile_idc, 66 | 77 | 88 | 100 | 110 | 122 | 244),
"this writer only emits SPS bodies for profile_idc ∈ {{66, 77, 88, 100, 110, 122, 244}} \
(Baseline / Main / Extended / High / High 10 / High 4:2:2 / High 4:4:4 Predictive). \
Profile {} would require additional bit_depth_* / scaling-matrix wiring per §7.3.2.1.1.",
cfg.profile_idc,
);
debug_assert!(
matches!(cfg.chroma_format_idc, 0..=3),
"chroma_format_idc {} not in writer scope (0=4:0:0, 1=4:2:0, 2=4:2:2, 3=4:4:4)",
cfg.chroma_format_idc,
);
debug_assert!(
!cfg.separate_colour_plane
|| (cfg.profile_idc == 244
&& cfg.chroma_format_idc == 0
&& cfg.seq_scaling_lists.is_none()),
"separate_colour_plane requires profile 244, an internally-monochrome \
(chroma_format_idc = 0) per-plane encoder, and flat scaling lists",
);
debug_assert!(
match cfg.chroma_format_idc {
0 => matches!(cfg.profile_idc, 100 | 110 | 122 | 244),
1 => true,
2 => matches!(cfg.profile_idc, 122 | 244),
3 => cfg.profile_idc == 244,
_ => false,
},
"chroma_format_idc={} not paired with the expected profile_idc; got profile {}",
cfg.chroma_format_idc,
cfg.profile_idc,
);
debug_assert!(
(cfg.bit_depth_luma_minus8 == 0 && cfg.bit_depth_chroma_minus8 == 0)
|| matches!(cfg.profile_idc, 100 | 110 | 122 | 244),
">8-bit depths require a chroma-extended profile (§A.2)",
);
w.u(8, cfg.profile_idc as u32);
let cs0 = if cfg.profile_idc == 66 { 1 } else { 0 };
w.u(1, cs0);
for _ in 0..5 {
w.u(1, 0);
}
w.u(2, 0);
w.u(8, cfg.level_idc as u32);
w.ue(cfg.seq_parameter_set_id);
if matches!(
cfg.profile_idc,
100 | 110 | 122 | 244 | 44 | 83 | 86 | 118 | 128 | 138 | 139 | 134 | 135
) {
let wire_chroma_format_idc = if cfg.separate_colour_plane {
3
} else {
cfg.chroma_format_idc
};
w.ue(wire_chroma_format_idc);
if wire_chroma_format_idc == 3 {
w.u(1, u32::from(cfg.separate_colour_plane));
}
debug_assert!(cfg.bit_depth_luma_minus8 <= 6 && cfg.bit_depth_chroma_minus8 <= 6);
w.ue(cfg.bit_depth_luma_minus8);
w.ue(cfg.bit_depth_chroma_minus8);
w.u(1, u32::from(transform_bypass));
if let Some(spec) = &cfg.seq_scaling_lists {
w.u(1, 1);
let n_lists = if cfg.chroma_format_idc == 3 { 12 } else { 8 };
for i in 0..n_lists {
w.u(1, 1); write_scaling_list_slot(&mut w, spec, i);
}
} else {
w.u(1, 0);
}
}
w.ue(cfg.log2_max_frame_num_minus4);
w.ue(0);
w.ue(cfg.log2_max_poc_lsb_minus4);
w.ue(cfg.max_num_ref_frames);
w.u(1, 0);
debug_assert!(cfg.width_in_mbs >= 1);
debug_assert!(cfg.height_in_mbs >= 1);
w.ue(cfg.width_in_mbs - 1);
if cfg.interlaced_fields {
debug_assert!(
cfg.profile_idc != 66,
"interlaced_fields requires a non-Baseline profile (§A.2.1)",
);
debug_assert!(
cfg.height_in_mbs % 2 == 0,
"FrameHeightInMbs must be even for field coding",
);
w.ue(cfg.height_in_mbs / 2 - 1);
w.u(1, 0);
w.u(1, u32::from(cfg.mbaff));
} else {
w.ue(cfg.height_in_mbs - 1);
w.u(1, 1);
}
w.u(1, 1);
w.u(1, 0);
if let Some(vui) = &cfg.vui {
w.u(1, 1);
write_vui_parameters(&mut w, vui);
} else {
w.u(1, 0);
}
w.rbsp_trailing_bits();
w.into_bytes()
}
pub fn write_vui_parameters(w: &mut BitWriter, vui: &VuiParameters) {
if let Some(ar) = &vui.aspect_ratio {
w.u(1, 1);
w.u(8, ar.aspect_ratio_idc as u32);
if ar.aspect_ratio_idc == 255 {
let (sw_, sh) = ar
.extended_sar
.expect("aspect_ratio_idc == Extended_SAR requires extended_sar");
w.u(16, sw_ as u32);
w.u(16, sh as u32);
} else {
debug_assert!(
ar.extended_sar.is_none(),
"extended_sar is only writable with aspect_ratio_idc == 255"
);
}
} else {
w.u(1, 0);
}
if let Some(f) = vui.overscan_appropriate_flag {
w.u(1, 1);
w.u(1, u32::from(f));
} else {
w.u(1, 0);
}
if let Some(vst) = &vui.video_signal_type {
w.u(1, 1);
w.u(3, vst.video_format as u32);
w.u(1, u32::from(vst.video_full_range_flag));
if let Some(cd) = &vst.colour_description {
w.u(1, 1);
w.u(8, cd.colour_primaries as u32);
w.u(8, cd.transfer_characteristics as u32);
w.u(8, cd.matrix_coefficients as u32);
} else {
w.u(1, 0);
}
} else {
w.u(1, 0);
}
if let Some(cl) = &vui.chroma_loc_info {
w.u(1, 1);
w.ue(cl.chroma_sample_loc_type_top_field);
w.ue(cl.chroma_sample_loc_type_bottom_field);
} else {
w.u(1, 0);
}
if let Some(t) = &vui.timing_info {
w.u(1, 1);
w.u(32, t.num_units_in_tick);
w.u(32, t.time_scale);
w.u(1, u32::from(t.fixed_frame_rate_flag));
} else {
w.u(1, 0);
}
if let Some(h) = &vui.nal_hrd_parameters {
w.u(1, 1);
write_hrd_parameters(w, h);
} else {
w.u(1, 0);
}
if let Some(h) = &vui.vcl_hrd_parameters {
w.u(1, 1);
write_hrd_parameters(w, h);
} else {
w.u(1, 0);
}
if vui.nal_hrd_parameters.is_some() || vui.vcl_hrd_parameters.is_some() {
w.u(1, u32::from(vui.low_delay_hrd_flag.unwrap_or(false)));
} else {
debug_assert!(
vui.low_delay_hrd_flag.is_none(),
"low_delay_hrd_flag is only writable when an HRD block is present"
);
}
w.u(1, u32::from(vui.pic_struct_present_flag));
if let Some(br) = &vui.bitstream_restriction {
w.u(1, 1);
w.u(1, u32::from(br.motion_vectors_over_pic_boundaries_flag));
w.ue(br.max_bytes_per_pic_denom);
w.ue(br.max_bits_per_mb_denom);
w.ue(br.log2_max_mv_length_horizontal);
w.ue(br.log2_max_mv_length_vertical);
w.ue(br.max_num_reorder_frames);
w.ue(br.max_dec_frame_buffering);
} else {
w.u(1, 0);
}
}
pub fn write_hrd_parameters(w: &mut BitWriter, h: &HrdParameters) {
debug_assert!(
h.cpb_cnt_minus1 <= 31,
"cpb_cnt_minus1 range 0..=31 (§E.2.2)"
);
let count = h.cpb_cnt_minus1 as usize + 1;
debug_assert_eq!(h.bit_rate_value_minus1.len(), count);
debug_assert_eq!(h.cpb_size_value_minus1.len(), count);
debug_assert_eq!(h.cbr_flag.len(), count);
w.ue(h.cpb_cnt_minus1);
w.u(4, h.bit_rate_scale as u32);
w.u(4, h.cpb_size_scale as u32);
for i in 0..count {
w.ue(h.bit_rate_value_minus1[i]);
w.ue(h.cpb_size_value_minus1[i]);
w.u(1, u32::from(h.cbr_flag[i]));
}
w.u(5, h.initial_cpb_removal_delay_length_minus1 as u32);
w.u(5, h.cpb_removal_delay_length_minus1 as u32);
w.u(5, h.dpb_output_delay_length_minus1 as u32);
w.u(5, h.time_offset_length as u32);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sps::Sps;
use crate::vui::TimingInfo;
#[test]
fn sps_with_vui_timing_and_nal_hrd_round_trips() {
let hrd = HrdParameters {
cpb_cnt_minus1: 0,
bit_rate_scale: 0,
cpb_size_scale: 0,
bit_rate_value_minus1: vec![1874],
cpb_size_value_minus1: vec![7499],
cbr_flag: vec![true],
initial_cpb_removal_delay_length_minus1: 23,
cpb_removal_delay_length_minus1: 23,
dpb_output_delay_length_minus1: 23,
time_offset_length: 24,
};
let vui = VuiParameters {
timing_info: Some(TimingInfo {
num_units_in_tick: 1,
time_scale: 60,
fixed_frame_rate_flag: true,
}),
nal_hrd_parameters: Some(hrd),
low_delay_hrd_flag: Some(false),
..VuiParameters::default()
};
let cfg = BaselineSpsConfig {
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
width_in_mbs: 5,
height_in_mbs: 4,
vui: Some(vui.clone()),
..BaselineSpsConfig::default()
};
let rbsp = build_baseline_sps_rbsp(&cfg);
let sps = Sps::parse(&rbsp).expect("decoder parses our SPS");
assert!(sps.vui_parameters_present_flag);
assert_eq!(sps.vui.as_ref(), Some(&vui), "VUI must round-trip exactly");
let parsed_hrd = sps
.vui
.as_ref()
.and_then(|v| v.nal_hrd_parameters.as_ref())
.expect("nal hrd present");
assert_eq!((parsed_hrd.bit_rate_value_minus1[0] + 1) << 6, 120_000);
assert_eq!((parsed_hrd.cpb_size_value_minus1[0] + 1) << 4, 120_000);
assert!(parsed_hrd.cbr_flag[0]);
}
#[test]
fn baseline_sps_round_trips_through_decoder_parser() {
let cfg = BaselineSpsConfig {
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
seq_scaling_lists: None,
interlaced_fields: false,
mbaff: false,
vui: None,
seq_parameter_set_id: 0,
level_idc: 30,
width_in_mbs: 4, height_in_mbs: 4,
log2_max_frame_num_minus4: 4,
log2_max_poc_lsb_minus4: 4,
max_num_ref_frames: 1,
profile_idc: 66,
chroma_format_idc: 1,
separate_colour_plane: false,
};
let rbsp = build_baseline_sps_rbsp(&cfg);
let sps = Sps::parse(&rbsp).expect("decoder parses our SPS");
assert_eq!(sps.profile_idc, 66);
assert_eq!(sps.constraint_set_flags & 1, 1);
assert_eq!(sps.level_idc, 30);
assert_eq!(sps.seq_parameter_set_id, 0);
assert_eq!(sps.chroma_format_idc, 1);
assert_eq!(sps.log2_max_frame_num_minus4, 4);
assert_eq!(sps.pic_order_cnt_type, 0);
assert_eq!(sps.log2_max_pic_order_cnt_lsb_minus4, 4);
assert_eq!(sps.max_num_ref_frames, 1);
assert!(!sps.gaps_in_frame_num_value_allowed_flag);
assert_eq!(sps.pic_width_in_mbs(), 4);
assert_eq!(sps.frame_height_in_mbs(), 4);
assert!(sps.frame_mbs_only_flag);
assert!(sps.frame_cropping.is_none());
assert!(!sps.vui_parameters_present_flag);
}
#[test]
fn high_444_sps_emits_chroma_extended_group_with_separate_colour_plane_flag() {
let cfg = BaselineSpsConfig {
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
seq_scaling_lists: None,
interlaced_fields: false,
mbaff: false,
vui: None,
seq_parameter_set_id: 0,
level_idc: 30,
width_in_mbs: 4,
height_in_mbs: 4,
log2_max_frame_num_minus4: 4,
log2_max_poc_lsb_minus4: 4,
max_num_ref_frames: 1,
profile_idc: 244,
chroma_format_idc: 3,
separate_colour_plane: false,
};
let rbsp = build_baseline_sps_rbsp(&cfg);
let sps = Sps::parse(&rbsp).expect("decoder parses our 4:4:4 SPS");
assert_eq!(sps.profile_idc, 244);
assert_eq!(sps.constraint_set_flags, 0);
assert_eq!(sps.chroma_format_idc, 3);
assert!(!sps.separate_colour_plane_flag);
assert_eq!(sps.bit_depth_luma_minus8, 0);
assert_eq!(sps.bit_depth_chroma_minus8, 0);
assert!(!sps.qpprime_y_zero_transform_bypass_flag);
assert!(!sps.seq_scaling_matrix_present_flag);
assert_eq!(sps.chroma_array_type(), 3);
assert_eq!(sps.pic_width_in_mbs(), 4);
assert_eq!(sps.frame_height_in_mbs(), 4);
}
#[test]
fn high_422_sps_emits_chroma_extended_group() {
let cfg = BaselineSpsConfig {
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
seq_scaling_lists: None,
interlaced_fields: false,
mbaff: false,
vui: None,
seq_parameter_set_id: 0,
level_idc: 30,
width_in_mbs: 4,
height_in_mbs: 4,
log2_max_frame_num_minus4: 4,
log2_max_poc_lsb_minus4: 4,
max_num_ref_frames: 1,
profile_idc: 122,
chroma_format_idc: 2,
separate_colour_plane: false,
};
let rbsp = build_baseline_sps_rbsp(&cfg);
let sps = Sps::parse(&rbsp).expect("decoder parses our 4:2:2 SPS");
assert_eq!(sps.profile_idc, 122);
assert_eq!(sps.constraint_set_flags, 0);
assert_eq!(sps.chroma_format_idc, 2);
assert!(!sps.separate_colour_plane_flag);
assert_eq!(sps.bit_depth_luma_minus8, 0);
assert_eq!(sps.bit_depth_chroma_minus8, 0);
assert!(!sps.qpprime_y_zero_transform_bypass_flag);
assert!(!sps.seq_scaling_matrix_present_flag);
assert_eq!(sps.chroma_array_type(), 2);
assert_eq!(sps.pic_width_in_mbs(), 4);
assert_eq!(sps.frame_height_in_mbs(), 4);
}
}