use shiguredo_mp4::bitstream::av1::{
Av1FrameHeaderPrefix, Av1FrameType, Av1ObuParseContext, Av1ObuType, Av1SequenceHeader,
parse_frame_header_prefix, parse_obus, parse_sequence_header,
};
use shiguredo_webrtc::{SdpVideoFormat, SdpVideoFormatRef, VideoCodecType};
use crate::video_codecs::mp4::Mp4Error;
pub(super) type Result<T> = std::result::Result<T, Mp4Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Av1TrackConfig {
pub(super) seq_profile: u8,
pub(super) seq_level_idx_0: u8,
pub(super) seq_tier_0: u8,
pub(super) high_bitdepth: bool,
pub(super) twelve_bit: bool,
pub(super) monochrome: bool,
pub(super) chroma_subsampling_x: u8,
pub(super) chroma_subsampling_y: u8,
pub(super) chroma_sample_position: u8,
pub(super) initial_presentation_delay_minus_one: Option<u8>,
pub(super) config_obus: Vec<u8>,
}
pub(super) fn av1_frame_type_name(t: Av1FrameType) -> &'static str {
match t {
Av1FrameType::Key => "key_frame",
Av1FrameType::Inter => "inter_frame",
Av1FrameType::IntraOnly => "intra_only_frame",
Av1FrameType::Switch => "switch_frame",
}
}
pub(super) fn av1_obu_type_name(t: Av1ObuType) -> &'static str {
match t {
Av1ObuType::Reserved(_) => "reserved",
Av1ObuType::SequenceHeader => "sequence_header",
Av1ObuType::TemporalDelimiter => "temporal_delimiter",
Av1ObuType::FrameHeader => "frame_header",
Av1ObuType::TileGroup => "tile_group",
Av1ObuType::Metadata => "metadata",
Av1ObuType::Frame => "frame",
Av1ObuType::RedundantFrameHeader => "redundant_frame_header",
Av1ObuType::TileList => "tile_list",
Av1ObuType::Padding => "padding",
}
}
fn sequence_header_matches_av1c(sh: &Av1SequenceHeader, av1c: &Av1TrackConfig) -> bool {
sh.seq_profile == av1c.seq_profile
&& sh.seq_level_idx_0 == av1c.seq_level_idx_0
&& sh.seq_tier_0 == av1c.seq_tier_0
&& sh.high_bitdepth == av1c.high_bitdepth
&& sh.twelve_bit == av1c.twelve_bit
&& sh.monochrome == av1c.monochrome
&& sh.chroma_subsampling_x == av1c.chroma_subsampling_x
&& sh.chroma_subsampling_y == av1c.chroma_subsampling_y
&& sh.chroma_sample_position == av1c.chroma_sample_position
}
pub(super) fn has_supported_operating_points(seq: &Av1SequenceHeader) -> bool {
seq.operating_points_cnt_minus_1 == 0 && seq.operating_point_idc_0 == 0
}
pub(super) fn validate_av1_track(
is_keyframes: &[bool],
av1_config: &Av1TrackConfig,
mut read_sample: impl FnMut(usize) -> Result<Vec<u8>>,
) -> Result<()> {
let config_obus = parse_obus(&av1_config.config_obus, Av1ObuParseContext::ConfigObus)
.map_err(|e| Mp4Error::InvalidAv1Track(format!("failed to parse configOBUs: {e:?}")))?;
let config_sh_indices: Vec<usize> = config_obus
.iter()
.enumerate()
.filter(|(_, obu)| obu.obu_type == Av1ObuType::SequenceHeader)
.map(|(i, _)| i)
.collect();
match config_sh_indices.as_slice() {
[] => {}
[first] => {
if *first != 0 {
return Err(Mp4Error::InvalidAv1Track(format!(
"configOBUs Sequence Header OBU is not first: sh_index={first}"
)));
}
}
[first, ..] => {
if *first != 0 {
return Err(Mp4Error::InvalidAv1Track(format!(
"configOBUs Sequence Header OBU is not first: sh_index={first}"
)));
}
return Err(Mp4Error::InvalidAv1Track(
"configOBUs contain multiple Sequence Header OBUs".to_string(),
));
}
}
let config_sh_payload_and_fields = match config_obus
.first()
.filter(|o| o.obu_type == Av1ObuType::SequenceHeader)
{
Some(obu) => {
let fields = parse_sequence_header(obu.payload).map_err(|e| {
Mp4Error::InvalidAv1Track(format!(
"failed to parse Sequence Header in configOBUs: {e:?}"
))
})?;
if !has_supported_operating_points(&fields) {
return Err(Mp4Error::InvalidAv1Track(format!(
"configOBUs Sequence Header is not a single operating point: operating_points_cnt_minus_1={} operating_point_idc_0={}",
fields.operating_points_cnt_minus_1, fields.operating_point_idc_0
)));
}
if !sequence_header_matches_av1c(&fields, av1_config) {
return Err(Mp4Error::InvalidAv1Track(
"configOBUs Sequence Header does not match av1C".to_string(),
));
}
Some((obu.payload.to_vec(), fields))
}
None => None,
};
if !is_keyframes.iter().any(|k| *k) {
return Err(Mp4Error::InvalidAv1Track(
"AV1 track has no sync sample".to_string(),
));
}
if !is_keyframes[0] {
return Err(Mp4Error::InvalidAv1Track(
"first sample of AV1 track is not a sync sample".to_string(),
));
}
let config_has_permanent_sh = config_sh_payload_and_fields.is_some();
let permanent_sh_payload: Option<Vec<u8>> = config_sh_payload_and_fields
.as_ref()
.map(|(p, _)| p.clone());
let mut current_sh_context: Option<Av1SequenceHeader> =
config_sh_payload_and_fields.as_ref().map(|(_, f)| *f);
let mut cvs_sh_payload: Option<Vec<u8>> = None;
for (sample_index, is_keyframe) in is_keyframes.iter().enumerate() {
let bytes = read_sample(sample_index)?;
let sample_obus = parse_obus(&bytes, Av1ObuParseContext::Sample).map_err(|e| {
Mp4Error::InvalidAv1Track(format!(
"failed to parse OBUs in sample={sample_index}: {e:?}"
))
})?;
if *is_keyframe && !config_has_permanent_sh {
cvs_sh_payload = None;
}
let mut first_sh_index: Option<usize> = None;
let mut first_frame_index: Option<usize> = None;
for (obu_index, obu) in sample_obus.iter().enumerate() {
match obu.obu_type {
Av1ObuType::SequenceHeader => {
if first_sh_index.is_none() {
first_sh_index = Some(obu_index);
}
let fields = parse_sequence_header(obu.payload).map_err(|e| {
Mp4Error::InvalidAv1Track(format!(
"failed to parse Sequence Header in sample={sample_index}: {e:?}"
))
})?;
if !has_supported_operating_points(&fields) {
return Err(Mp4Error::InvalidAv1Track(format!(
"Sequence Header in sample={sample_index} is not a single operating point: operating_points_cnt_minus_1={} operating_point_idc_0={}",
fields.operating_points_cnt_minus_1, fields.operating_point_idc_0
)));
}
if !sequence_header_matches_av1c(&fields, av1_config) {
return Err(Mp4Error::InvalidAv1Track(format!(
"Sequence Header in sample={sample_index} does not match av1C"
)));
}
let baseline: Option<&[u8]> = if config_has_permanent_sh {
permanent_sh_payload.as_deref()
} else {
cvs_sh_payload.as_deref()
};
match baseline {
Some(b) => {
if obu.payload != b {
return Err(Mp4Error::InvalidAv1Track(format!(
"Sequence Header payload in sample={sample_index} does not match the baseline in the same coded video sequence"
)));
}
}
None => {
cvs_sh_payload = Some(obu.payload.to_vec());
}
}
current_sh_context = Some(fields);
}
Av1ObuType::FrameHeader | Av1ObuType::Frame if first_frame_index.is_none() => {
first_frame_index = Some(obu_index);
if *is_keyframe && let Some(sh) = current_sh_context {
let prefix = parse_frame_header_prefix(obu.payload, &sh).map_err(|e| {
Mp4Error::InvalidAv1Track(format!(
"failed to parse Frame Header in sample={sample_index}: {e:?}"
))
})?;
match prefix {
Av1FrameHeaderPrefix::ShowExistingFrame => {
return Err(Mp4Error::InvalidAv1Track(format!(
"sync sample has show_existing_frame=1: sample={sample_index}"
)));
}
Av1FrameHeaderPrefix::NewFrame {
frame_type,
show_frame,
} => {
if frame_type != Av1FrameType::Key {
return Err(Mp4Error::InvalidAv1Track(format!(
"sync sample frame_type is not KEY_FRAME: sample={sample_index} frame_type={}",
av1_frame_type_name(frame_type)
)));
}
if !show_frame {
return Err(Mp4Error::InvalidAv1Track(format!(
"sync sample has show_frame=0: sample={sample_index}"
)));
}
}
}
}
}
_ => {}
}
}
if *is_keyframe {
let sh_index = first_sh_index.ok_or_else(|| {
Mp4Error::InvalidAv1Track(format!(
"sync sample does not contain a Sequence Header OBU: sample={sample_index}"
))
})?;
let fr_index = first_frame_index.ok_or_else(|| {
Mp4Error::InvalidAv1Track(format!(
"sync sample has no Frame Header / Frame OBU: sample={sample_index}"
))
})?;
if sh_index > fr_index {
return Err(Mp4Error::InvalidAv1Track(format!(
"Sequence Header appears after the first Frame in sync sample: sample={sample_index}"
)));
}
let first_significant = config_obus
.iter()
.map(|o| o.obu_type)
.chain(sample_obus.iter().map(|o| o.obu_type))
.find(|t| !matches!(t, Av1ObuType::TemporalDelimiter | Av1ObuType::Padding));
match first_significant {
Some(Av1ObuType::SequenceHeader) => {}
Some(t) => {
return Err(Mp4Error::InvalidAv1Track(format!(
"first RTP payload OBU is not a Sequence Header: sample={sample_index} first_obu_type={}",
av1_obu_type_name(t)
)));
}
None => {
return Err(Mp4Error::InvalidAv1Track(format!(
"sync sample does not contain a Sequence Header OBU: sample={sample_index}"
)));
}
}
}
}
Ok(())
}
pub(super) fn assemble_av1_encoded_sample_data(
raw_sample: Vec<u8>,
is_keyframe: bool,
av1_config: Option<&Av1TrackConfig>,
) -> Vec<u8> {
if is_keyframe
&& let Some(config) = av1_config
&& !config.config_obus.is_empty()
{
let mut combined = Vec::with_capacity(config.config_obus.len() + raw_sample.len());
combined.extend_from_slice(&config.config_obus);
combined.extend_from_slice(&raw_sample);
combined
} else {
raw_sample
}
}
pub(super) fn parse_av1_sdp_field(
params: &std::collections::HashMap<String, String>,
key: &str,
default: u8,
max: u8,
) -> Option<u8> {
let Some(raw) = params.get(key) else {
return Some(default);
};
let parsed: u8 = raw.parse().ok()?;
if parsed > max { None } else { Some(parsed) }
}
pub(super) fn resolve_av1_incoming(
required: &SdpVideoFormat,
incoming: SdpVideoFormatRef<'_>,
) -> Option<SdpVideoFormat> {
let name = incoming.name().ok()?;
if VideoCodecType::try_from(name.as_str()).ok()? != VideoCodecType::Av1 {
return None;
}
let incoming_params: std::collections::HashMap<String, String> =
incoming.parameters().iter().collect();
let incoming_profile = parse_av1_sdp_field(&incoming_params, "profile", 0, 2)?;
let incoming_level = parse_av1_sdp_field(&incoming_params, "level-idx", 5, 31)?;
let incoming_tier = parse_av1_sdp_field(&incoming_params, "tier", 0, 1)?;
let mut required_owned = required.clone();
let required_params: std::collections::HashMap<String, String> =
required_owned.parameters_mut().iter().collect();
let required_profile = parse_av1_sdp_field(&required_params, "profile", 0, 2)?;
let required_level = parse_av1_sdp_field(&required_params, "level-idx", 5, 31)?;
let required_tier = parse_av1_sdp_field(&required_params, "tier", 0, 1)?;
if required_profile != incoming_profile {
return None;
}
if required_level > incoming_level {
return None;
}
if required_tier > incoming_tier {
return None;
}
Some(required_owned)
}
pub(super) fn av1_required_sdp_format(av1_config: Option<&Av1TrackConfig>) -> SdpVideoFormat {
let mut format = SdpVideoFormat::new("AV1");
if let Some(config) = av1_config {
let mut params = format.parameters_mut();
params.set("profile", &config.seq_profile.to_string());
params.set("level-idx", &config.seq_level_idx_0.to_string());
params.set("tier", &config.seq_tier_0.to_string());
}
format
}
#[cfg(test)]
mod tests {
use super::*;
use shiguredo_mp4::bitstream::av1::parse_sequence_header;
fn base_config() -> Av1TrackConfig {
Av1TrackConfig {
seq_profile: 0,
seq_level_idx_0: 0,
seq_tier_0: 0,
high_bitdepth: false,
twelve_bit: false,
monochrome: false,
chroma_subsampling_x: 1,
chroma_subsampling_y: 1,
chroma_sample_position: 0,
initial_presentation_delay_minus_one: None,
config_obus: vec![0x0A, 0x0B],
}
}
fn parse_sh(payload: &[u8]) -> Av1SequenceHeader {
parse_sequence_header(payload).expect("テスト用 Sequence Header は parse できるはず")
}
fn make_reduced_still_sh_payload(seq_profile: u8, seq_level_idx_0: u8) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_bits(seq_profile as u32, 3); w.write_bit(true); w.write_bit(true); w.write_bits(seq_level_idx_0 as u32, 5); w.write_bits(0, 4); w.write_bits(0, 4); w.write_bits(0, 1); w.write_bits(0, 1); w.write_bits(0, 3); w.write_bits(0, 3); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bits(0, 2); w.write_bit(false); w.write_bit(false); w.finish()
}
fn make_reduced_still_sh_payload_with_dimensions(
max_w_minus_1: bool,
max_h_minus_1: bool,
) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_bits(0, 3); w.write_bit(true); w.write_bit(true); w.write_bits(0, 5); w.write_bits(0, 4); w.write_bits(0, 4); w.write_bit(max_w_minus_1); w.write_bit(max_h_minus_1); w.write_bits(0, 3); w.write_bits(0, 3); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bits(0, 2); w.write_bit(false); w.write_bit(false); w.finish()
}
fn make_full_sh_payload(
operating_points_cnt_minus_1: u8,
operating_point_idc_0: u16,
seq_level_idx_0: u8,
) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_bits(0, 3); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bits(operating_points_cnt_minus_1 as u32, 5); for i in 0..=operating_points_cnt_minus_1 {
let idc = if i == 0 { operating_point_idc_0 } else { 0 };
w.write_bits(idc as u32, 12); let level = if i == 0 { seq_level_idx_0 } else { 0 };
w.write_bits(level as u32, 5); if level > 7 {
w.write_bit(false); }
}
w.write_bits(0, 4); w.write_bits(0, 4); w.write_bits(0, 1); w.write_bits(0, 1); w.write_bit(false); w.write_bits(0, 3); w.write_bits(0, 5); w.write_bit(false); w.write_bit(false); w.write_bits(0, 3); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bit(false); w.write_bits(0, 2); w.write_bit(false); w.write_bit(false); w.finish()
}
struct BitWriter {
buf: Vec<u8>,
bit_position: usize,
}
impl BitWriter {
fn new() -> Self {
Self {
buf: Vec::new(),
bit_position: 0,
}
}
fn write_bit(&mut self, bit: bool) {
let byte_idx = self.bit_position / 8;
if byte_idx >= self.buf.len() {
self.buf.push(0);
}
let bit_in_byte = 7 - (self.bit_position % 8);
if bit {
self.buf[byte_idx] |= 1 << bit_in_byte;
}
self.bit_position += 1;
}
fn write_bits(&mut self, value: u32, n: usize) {
for i in (0..n).rev() {
self.write_bit(((value >> i) & 1) != 0);
}
}
fn finish(self) -> Vec<u8> {
self.buf
}
}
fn encode_leb128(mut value: u32) -> Vec<u8> {
let mut out = Vec::new();
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
out.push(byte);
if value == 0 {
break;
}
}
out
}
fn make_obu(obu_type_bits: u8, payload: &[u8]) -> Vec<u8> {
assert!(obu_type_bits < 16, "obu_type_bits は 4 bit 範囲");
let mut buf = Vec::new();
buf.push((obu_type_bits << 3) | 0b010);
buf.extend_from_slice(&encode_leb128(payload.len() as u32));
buf.extend_from_slice(payload);
buf
}
fn av1_track_config_for_reduced_still(config_obus: Vec<u8>) -> Av1TrackConfig {
Av1TrackConfig {
seq_profile: 0,
seq_level_idx_0: 0,
seq_tier_0: 0,
high_bitdepth: false,
twelve_bit: false,
monochrome: false,
chroma_subsampling_x: 1,
chroma_subsampling_y: 1,
chroma_sample_position: 0,
initial_presentation_delay_minus_one: None,
config_obus,
}
}
fn validate_av1_samples(entries: Vec<(Vec<u8>, bool)>, config: &Av1TrackConfig) -> Result<()> {
let mut is_keyframes = Vec::new();
let mut bytes_table = Vec::new();
for (data, is_keyframe) in entries {
is_keyframes.push(is_keyframe);
bytes_table.push(data);
}
validate_av1_track(&is_keyframes, config, |i| Ok(bytes_table[i].clone()))
}
fn assert_invalid_av1_track(result: &Result<()>, expected: &str) {
match result {
Err(Mp4Error::InvalidAv1Track(message)) => {
assert!(
message.contains(expected),
"エラーメッセージ {message:?} に {expected:?} が含まれるはずです"
);
}
other => panic!("InvalidAv1Track を期待しましたが: {other:?}"),
}
}
const OBU_TYPE_SEQUENCE_HEADER: u8 = 1;
const OBU_TYPE_TEMPORAL_DELIMITER: u8 = 2;
const OBU_TYPE_METADATA: u8 = 5;
const OBU_TYPE_FRAME: u8 = 6;
#[test]
fn sequence_header_matches_av1c_compares_corresponding_fields() {
let sh = parse_sh(&make_reduced_still_sh_payload(0, 0));
let av1c = base_config();
assert!(
sequence_header_matches_av1c(&sh, &av1c),
"SH と av1C が一致する場合は true を返すはずです"
);
let mut mismatched = av1c;
mismatched.seq_profile = 1;
assert!(
!sequence_header_matches_av1c(&sh, &mismatched),
"対応 field が食い違う場合は false を返すはずです"
);
}
#[test]
fn has_supported_operating_points_accepts_single_zero_idc() {
let sh = parse_sh(&make_reduced_still_sh_payload(0, 0));
assert!(
has_supported_operating_points(&sh),
"単一 operating point かつ idc 0 は受理されるはずです"
);
}
#[test]
fn validate_av1_track_accepts_minimal_sync_only() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert!(
result.is_ok(),
"最小 sync sample の validate_av1_track は Ok を返すはずですが: {result:?}"
);
}
#[test]
fn validate_av1_track_rejects_random_access_not_possible() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sync_sample = Vec::new();
sync_sample.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sync_sample.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sync_sample.clone(), false)], &config);
assert_invalid_av1_track(&result, "has no sync sample");
let non_sync_sample = make_obu(OBU_TYPE_FRAME, &[]);
let result =
validate_av1_samples(vec![(non_sync_sample, false), (sync_sample, true)], &config);
assert_invalid_av1_track(&result, "first sample of AV1 track is not a sync sample");
}
#[test]
fn validate_av1_track_rejects_sync_sample_without_leading_sequence_header() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let config = av1_track_config_for_reduced_still(Vec::new());
let sample_bytes = make_obu(OBU_TYPE_FRAME, &[]);
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "does not contain a Sequence Header OBU");
let mut reversed = Vec::new();
reversed.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
reversed.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
let result = validate_av1_samples(vec![(reversed, true)], &config);
assert_invalid_av1_track(&result, "appears after the first Frame");
}
#[test]
fn validate_av1_track_rejects_sync_sample_without_frame() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let sample_bytes = make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload);
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "has no Frame Header / Frame OBU");
}
#[test]
fn validate_av1_track_rejects_metadata_before_sequence_header() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_TEMPORAL_DELIMITER, &[]));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_METADATA, &[]));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "first RTP payload OBU is not a Sequence Header");
assert_invalid_av1_track(&result, "metadata");
}
#[test]
fn validate_av1_track_rejects_sample_sh_mismatch_with_av1c() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let mut config = av1_track_config_for_reduced_still(Vec::new());
config.seq_profile = 1;
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "does not match av1C");
}
#[test]
fn validate_av1_track_rejects_config_sequence_header_placement() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let mut config_obus = Vec::new();
config_obus.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
config_obus.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
let config = av1_track_config_for_reduced_still(config_obus);
let result = validate_av1_samples(vec![(sample_bytes.clone(), true)], &config);
assert_invalid_av1_track(&result, "multiple Sequence Header OBUs");
let mut config_obus = Vec::new();
config_obus.extend_from_slice(&make_obu(OBU_TYPE_TEMPORAL_DELIMITER, &[]));
config_obus.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
let config = av1_track_config_for_reduced_still(config_obus);
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "is not first");
}
#[test]
fn validate_av1_track_rejects_unsupported_operating_points() {
let sh_payload = make_reduced_still_sh_payload(0, 0);
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_payload));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let config_obus = make_obu(OBU_TYPE_SEQUENCE_HEADER, &make_full_sh_payload(1, 0, 0));
let config = av1_track_config_for_reduced_still(config_obus);
let result = validate_av1_samples(vec![(sample_bytes.clone(), true)], &config);
assert_invalid_av1_track(&result, "not a single operating point");
let mut sample_bytes = Vec::new();
sample_bytes.extend_from_slice(&make_obu(
OBU_TYPE_SEQUENCE_HEADER,
&make_full_sh_payload(0, 0xABC, 0),
));
sample_bytes.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sample_bytes, true)], &config);
assert_invalid_av1_track(&result, "not a single operating point");
}
#[test]
fn validate_av1_track_rejects_sh_payload_inconsistent_within_cvs() {
let sh_a = make_reduced_still_sh_payload_with_dimensions(false, false);
let mut sample0 = Vec::new();
sample0.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_a));
sample0.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
let sh_b = make_reduced_still_sh_payload_with_dimensions(true, false);
let mut sample1 = Vec::new();
sample1.extend_from_slice(&make_obu(OBU_TYPE_SEQUENCE_HEADER, &sh_b));
sample1.extend_from_slice(&make_obu(OBU_TYPE_FRAME, &[]));
assert_ne!(
sh_a, sh_b,
"テスト前提: 2 つの SH payload は byte 単位で異なるはずです"
);
let config = av1_track_config_for_reduced_still(Vec::new());
let result = validate_av1_samples(vec![(sample0, true), (sample1, false)], &config);
assert_invalid_av1_track(
&result,
"does not match the baseline in the same coded video sequence",
);
}
#[test]
fn assemble_av1_encoded_sample_data_prepends_config_obus_for_sync_sample() {
let config = Av1TrackConfig {
config_obus: vec![0xAA, 0xBB, 0xCC],
..av1_track_config_for_reduced_still(Vec::new())
};
let sample = vec![0x11, 0x22];
let out = assemble_av1_encoded_sample_data(sample.clone(), true, Some(&config));
assert_eq!(
out,
vec![0xAA, 0xBB, 0xCC, 0x11, 0x22],
"sync sample には configOBUs が sample data の前に付与されるはずです"
);
}
#[test]
fn assemble_av1_encoded_sample_data_leaves_sample_unchanged_without_prepend_conditions() {
let config = Av1TrackConfig {
config_obus: vec![0xAA, 0xBB, 0xCC],
..av1_track_config_for_reduced_still(Vec::new())
};
let sample = vec![0x11, 0x22];
let out = assemble_av1_encoded_sample_data(sample.clone(), false, Some(&config));
assert_eq!(
out, sample,
"non-sync sample では configOBUs を付与せず raw sample を返すはずです"
);
let empty_config = av1_track_config_for_reduced_still(Vec::new());
assert!(
empty_config.config_obus.is_empty(),
"テスト前提: config_obus は空であるはずです"
);
let out = assemble_av1_encoded_sample_data(sample.clone(), true, Some(&empty_config));
assert_eq!(
out, sample,
"configOBUs が空なら sync sample でも既存 payload を変更しないはずです"
);
let out = assemble_av1_encoded_sample_data(sample.clone(), true, None);
assert_eq!(
out, sample,
"av1_config が None なら sync sample でも既存 payload を変更しないはずです"
);
}
#[test]
fn av1_required_sdp_format_sets_profile_level_tier_from_config() {
let config = Av1TrackConfig {
seq_profile: 1,
seq_level_idx_0: 10,
seq_tier_0: 1,
..av1_track_config_for_reduced_still(Vec::new())
};
let mut format = av1_required_sdp_format(Some(&config));
assert_eq!(
format.name().expect("codec 名を取得できるはず"),
"AV1",
"codec 名は AV1 であるはずです"
);
let params: std::collections::HashMap<String, String> =
format.parameters_mut().iter().collect();
assert_eq!(
params.get("profile").map(String::as_str),
Some("1"),
"profile は av1_config.seq_profile の 10 進文字列であるはずです"
);
assert_eq!(
params.get("level-idx").map(String::as_str),
Some("10"),
"level-idx は av1_config.seq_level_idx_0 の 10 進文字列であるはずです"
);
assert_eq!(
params.get("tier").map(String::as_str),
Some("1"),
"tier は av1_config.seq_tier_0 の 10 進文字列であるはずです"
);
}
#[test]
fn av1_required_sdp_format_sets_defaults_explicitly() {
let config = Av1TrackConfig {
seq_profile: 0,
seq_level_idx_0: 5,
seq_tier_0: 0,
..av1_track_config_for_reduced_still(Vec::new())
};
let mut format = av1_required_sdp_format(Some(&config));
let params: std::collections::HashMap<String, String> =
format.parameters_mut().iter().collect();
assert_eq!(params.get("profile").map(String::as_str), Some("0"));
assert_eq!(params.get("level-idx").map(String::as_str), Some("5"));
assert_eq!(params.get("tier").map(String::as_str), Some("0"));
}
#[test]
fn av1_required_sdp_format_omits_parameters_when_config_missing() {
let mut format = av1_required_sdp_format(None);
assert_eq!(format.name().expect("codec 名を取得できるはず"), "AV1");
let params: std::collections::HashMap<String, String> =
format.parameters_mut().iter().collect();
assert!(
params.is_empty(),
"av1_config が None なら profile / level-idx / tier は設定されないはずです: {params:?}"
);
}
#[test]
fn resolve_av1_incoming_accepts_matching_profile_with_higher_level_and_tier() {
let config = av1_track_config_for_reduced_still(Vec::new());
let required = av1_required_sdp_format(Some(&config));
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set("profile", "0");
params.set("level-idx", "10");
params.set("tier", "0");
}
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_some(),
"profile 一致かつ level/tier 上限内は受理されるはずです"
);
}
#[test]
fn resolve_av1_incoming_rejects_profile_mismatch() {
let config = av1_track_config_for_reduced_still(Vec::new());
let required = av1_required_sdp_format(Some(&config));
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set("profile", "1"); }
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(resolved.is_none(), "profile 不一致は拒否されるはずです");
}
#[test]
fn resolve_av1_incoming_rejects_when_required_level_exceeds_incoming() {
let config = Av1TrackConfig {
seq_level_idx_0: 15,
..av1_track_config_for_reduced_still(Vec::new())
};
let required = av1_required_sdp_format(Some(&config));
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set("profile", "0");
params.set("level-idx", "10"); }
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_none(),
"required level が incoming より大きいなら拒否されるはずですが、Some でした"
);
}
#[test]
fn resolve_av1_incoming_rejects_when_required_tier_exceeds_incoming() {
let config = Av1TrackConfig {
seq_tier_0: 1, ..av1_track_config_for_reduced_still(Vec::new())
};
let required = av1_required_sdp_format(Some(&config));
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set("profile", "0");
params.set("level-idx", "5");
params.set("tier", "0"); }
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_none(),
"required tier が incoming より大きいなら拒否されるはずですが、Some でした"
);
}
#[test]
fn resolve_av1_incoming_uses_default_values_for_omitted_parameters() {
let config = Av1TrackConfig {
seq_level_idx_0: 5,
..av1_track_config_for_reduced_still(Vec::new())
};
let required = av1_required_sdp_format(Some(&config));
let incoming = SdpVideoFormat::new("AV1");
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_some(),
"省略値どうしなら受理されるはずですが、None でした"
);
}
#[test]
fn resolve_av1_incoming_rejects_out_of_range_values() {
let config = av1_track_config_for_reduced_still(Vec::new());
let required = av1_required_sdp_format(Some(&config));
for (key, out_of_range) in [("profile", "3"), ("level-idx", "32"), ("tier", "2")] {
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set(key, out_of_range);
}
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_none(),
"{key}={out_of_range} は範囲外なので拒否されるはずですが、Some でした"
);
}
}
#[test]
fn resolve_av1_incoming_rejects_non_decimal_or_overflow_values() {
let config = av1_track_config_for_reduced_still(Vec::new());
let required = av1_required_sdp_format(Some(&config));
for bad_profile in ["abc", "", "-1", "256", "0x1", "1.0"] {
let mut incoming = SdpVideoFormat::new("AV1");
{
let mut params = incoming.parameters_mut();
params.set("profile", bad_profile);
}
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_none(),
"profile={bad_profile:?} は不正 parse として拒否されるはずですが、Some でした"
);
}
}
#[test]
fn resolve_av1_incoming_rejects_non_av1_codec() {
let config = av1_track_config_for_reduced_still(Vec::new());
let required = av1_required_sdp_format(Some(&config));
let incoming = SdpVideoFormat::new("VP9");
let resolved = resolve_av1_incoming(&required, incoming.as_ref());
assert!(
resolved.is_none(),
"codec 名が AV1 でなければ拒否されるはずですが、Some でした"
);
}
}