use shiguredo_mp4::bitstream::h264::H264ProfileLevelId;
use shiguredo_webrtc::{SdpVideoFormat, SdpVideoFormatRef, VideoCodecType};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct H264TrackConfig {
pub(super) profile_level_id: H264ProfileLevelId,
pub(super) avcc_box: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum H264SubProfile {
ConstrainedBaseline,
Baseline,
Main,
High,
ConstrainedHigh,
PredictiveHigh444,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum H264Level {
Level1b,
Level1,
Level1_1,
Level1_2,
Level1_3,
Level2,
Level2_1,
Level2_2,
Level3,
Level3_1,
Level3_2,
Level4,
Level4_1,
Level4_2,
Level5,
Level5_1,
Level5_2,
}
impl H264Level {
fn capability_order(self) -> u8 {
match self {
Self::Level1 => 1,
Self::Level1b => 2,
Self::Level1_1 => 3,
Self::Level1_2 => 4,
Self::Level1_3 => 5,
Self::Level2 => 6,
Self::Level2_1 => 7,
Self::Level2_2 => 8,
Self::Level3 => 9,
Self::Level3_1 => 10,
Self::Level3_2 => 11,
Self::Level4 => 12,
Self::Level4_1 => 13,
Self::Level4_2 => 14,
Self::Level5 => 15,
Self::Level5_1 => 16,
Self::Level5_2 => 17,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct H264ProfileLevel {
profile: H264SubProfile,
level: H264Level,
}
struct H264ProfilePattern {
profile: H264SubProfile,
profile_idc: u8,
mask: u8,
value: u8,
}
const H264_PROFILE_PATTERNS: &[H264ProfilePattern] = &[
H264ProfilePattern {
profile: H264SubProfile::ConstrainedBaseline,
profile_idc: 0x42,
mask: 0b0100_1111,
value: 0b0100_0000,
},
H264ProfilePattern {
profile: H264SubProfile::ConstrainedBaseline,
profile_idc: 0x4D,
mask: 0b1000_1111,
value: 0b1000_0000,
},
H264ProfilePattern {
profile: H264SubProfile::ConstrainedBaseline,
profile_idc: 0x58,
mask: 0b1100_1111,
value: 0b1100_0000,
},
H264ProfilePattern {
profile: H264SubProfile::Baseline,
profile_idc: 0x42,
mask: 0b0100_1111,
value: 0,
},
H264ProfilePattern {
profile: H264SubProfile::Baseline,
profile_idc: 0x58,
mask: 0b1100_1111,
value: 0b1000_0000,
},
H264ProfilePattern {
profile: H264SubProfile::Main,
profile_idc: 0x4D,
mask: 0b1010_1111,
value: 0,
},
H264ProfilePattern {
profile: H264SubProfile::High,
profile_idc: 0x64,
mask: 0xFF,
value: 0,
},
H264ProfilePattern {
profile: H264SubProfile::ConstrainedHigh,
profile_idc: 0x64,
mask: 0xFF,
value: 0x0C,
},
H264ProfilePattern {
profile: H264SubProfile::PredictiveHigh444,
profile_idc: 0xF4,
mask: 0xFF,
value: 0,
},
];
pub(super) fn parse_profile_level_id(plid: H264ProfileLevelId) -> Option<H264ProfileLevel> {
let level = match plid.level_idc {
10 => H264Level::Level1,
11 => {
if plid.profile_iop & 0b0001_0000 != 0 {
H264Level::Level1b
} else {
H264Level::Level1_1
}
}
12 => H264Level::Level1_2,
13 => H264Level::Level1_3,
20 => H264Level::Level2,
21 => H264Level::Level2_1,
22 => H264Level::Level2_2,
30 => H264Level::Level3,
31 => H264Level::Level3_1,
32 => H264Level::Level3_2,
40 => H264Level::Level4,
41 => H264Level::Level4_1,
42 => H264Level::Level4_2,
50 => H264Level::Level5,
51 => H264Level::Level5_1,
52 => H264Level::Level5_2,
_ => return None,
};
let profile = H264_PROFILE_PATTERNS
.iter()
.find(|pattern| {
pattern.profile_idc == plid.profile_idc
&& (plid.profile_iop & pattern.mask) == pattern.value
})
.map(|pattern| pattern.profile)?;
Some(H264ProfileLevel { profile, level })
}
pub(super) fn resolve_h264_incoming(
required: &SdpVideoFormat,
incoming: SdpVideoFormatRef<'_>,
) -> Option<SdpVideoFormat> {
let name = incoming.name().ok()?;
if VideoCodecType::try_from(name.as_str()).ok()? != VideoCodecType::H264 {
return None;
}
let incoming_params: std::collections::HashMap<String, String> =
incoming.parameters().iter().collect();
if incoming_params
.get("packetization-mode")
.map(String::as_str)
!= Some("1")
{
return None;
}
let incoming_plid_hex = incoming_params.get("profile-level-id")?;
let incoming_plid = H264ProfileLevelId::from_hex(incoming_plid_hex).ok()?;
let incoming_parsed = parse_profile_level_id(incoming_plid)?;
let mut required_owned = required.clone();
let required_params: std::collections::HashMap<String, String> =
required_owned.parameters_mut().iter().collect();
let required_plid_hex = required_params
.get("profile-level-id")
.expect("BUG: H.264 required format must have profile-level-id");
let required_plid = H264ProfileLevelId::from_hex(required_plid_hex)
.expect("BUG: H.264 required profile-level-id must be valid hex");
let required_parsed = parse_profile_level_id(required_plid)
.expect("BUG: H.264 required profile-level-id must be recognized by libwebrtc");
if incoming_parsed.profile != required_parsed.profile {
return None;
}
if incoming_parsed.level.capability_order() < required_parsed.level.capability_order() {
return None;
}
Some(incoming.to_owned())
}
pub(super) fn h264_required_sdp_format(h264_config: &H264TrackConfig) -> SdpVideoFormat {
let mut format = SdpVideoFormat::new("H264");
format.parameters_mut().set("packetization-mode", "1");
format
.parameters_mut()
.set("profile-level-id", &h264_config.profile_level_id.to_hex());
format
}
#[cfg(test)]
mod tests {
use super::*;
use shiguredo_mp4::bitstream::h264::H264ProfileLevelId;
fn parse(profile_idc: u8, profile_iop: u8, level_idc: u8) -> Option<H264ProfileLevel> {
parse_profile_level_id(H264ProfileLevelId {
profile_idc,
profile_iop,
level_idc,
})
}
fn assert_profile_level(result: &H264ProfileLevel, profile: H264SubProfile, level: H264Level) {
assert_eq!(
result.profile, profile,
"sub-profile が期待値と一致するはずです"
);
assert_eq!(result.level, level, "level が期待値と一致するはずです");
}
#[test]
fn parses_all_k_profile_patterns_rows() {
assert_profile_level(
&parse(0x42, 0b0100_0000, 30).expect("CB (42 / x1xx0000) は成功するはずです"),
H264SubProfile::ConstrainedBaseline,
H264Level::Level3,
);
assert_profile_level(
&parse(0x4D, 0b1000_0000, 30).expect("CB (4D / 1xxx0000) は成功するはずです"),
H264SubProfile::ConstrainedBaseline,
H264Level::Level3,
);
assert_profile_level(
&parse(0x58, 0b1100_0000, 30).expect("CB (58 / 11xx0000) は成功するはずです"),
H264SubProfile::ConstrainedBaseline,
H264Level::Level3,
);
assert_profile_level(
&parse(0x42, 0b0000_0000, 30).expect("B (42 / x0xx0000) は成功するはずです"),
H264SubProfile::Baseline,
H264Level::Level3,
);
assert_profile_level(
&parse(0x58, 0b1000_0000, 30).expect("B (58 / 10xx0000) は成功するはずです"),
H264SubProfile::Baseline,
H264Level::Level3,
);
assert_profile_level(
&parse(0x4D, 0b0000_0000, 30).expect("M (4D / 0x0x0000) は成功するはずです"),
H264SubProfile::Main,
H264Level::Level3,
);
assert_profile_level(
&parse(0x64, 0b0000_0000, 30).expect("H (64 / 00000000) は成功するはずです"),
H264SubProfile::High,
H264Level::Level3,
);
assert_profile_level(
&parse(0x64, 0b0000_1100, 30)
.expect("Constrained High (64 / 00001100) は成功するはずです"),
H264SubProfile::ConstrainedHigh,
H264Level::Level3,
);
assert_profile_level(
&parse(0xF4, 0b0000_0000, 30)
.expect("Predictive High 4:4:4 (F4 / 00000000) は成功するはずです"),
H264SubProfile::PredictiveHigh444,
H264Level::Level3,
);
}
#[test]
fn normalizes_equivalent_representations_to_same_sub_profile() {
let constrained_baselines = [
parse(0x42, 0b1111_0000, 30).expect("CB (42 / x1xx0000) は成功するはずです"),
parse(0x4D, 0b1111_0000, 30).expect("CB (4D / 1xxx0000) は成功するはずです"),
parse(0x58, 0b1111_0000, 30).expect("CB (58 / 11xx0000) は成功するはずです"),
];
for result in constrained_baselines {
assert_eq!(
result.profile,
H264SubProfile::ConstrainedBaseline,
"3 つの CB 表現はすべて ConstrainedBaseline へ正規化されるはずです"
);
}
let baselines = [
parse(0x42, 0b1011_0000, 30).expect("B (42 / x0xx0000) は成功するはずです"),
parse(0x58, 0b1000_0000, 30).expect("B (58 / 10xx0000) は成功するはずです"),
];
for result in baselines {
assert_eq!(
result.profile,
H264SubProfile::Baseline,
"2 つの B 表現はすべて Baseline へ正規化されるはずです"
);
}
}
#[test]
fn rejects_constrained_high_with_other_high_iop_values() {
assert!(
parse(0x64, 0b1000_1100, 30).is_none(),
"64 / 10001100 は Constrained High の完全一致に合致しないため拒否されるはずです"
);
assert!(
parse(0x64, 0b0000_1101, 30).is_none(),
"64 / 00001101 は High / Constrained High のいずれにも一致しないため拒否されるはずです"
);
assert!(
parse(0x64, 0b0000_0001, 30).is_none(),
"64 / 00000001 は High (mask 0xFF) に一致しないため拒否されるはずです"
);
}
#[test]
fn rejects_sub_profiles_not_in_k_profile_patterns() {
assert!(
parse(0x58, 0b0000_0000, 30).is_none(),
"Extended (58 / 00xx0000) は libwebrtc 非認識のため拒否されるはずです"
);
assert!(
parse(0x6E, 0b0000_0000, 30).is_none(),
"High10 (6E / 00000000) は libwebrtc 非認識のため拒否されるはずです"
);
assert!(
parse(0x7A, 0b0000_0000, 30).is_none(),
"High42 (7A / 00000000) は libwebrtc 非認識のため拒否されるはずです"
);
assert!(
parse(0xF4, 0b0001_0000, 30).is_none(),
"High44 Intra (F4 / 00010000) は libwebrtc 非認識のため拒否されるはずです"
);
assert!(
parse(0x2C, 0b0001_0000, 30).is_none(),
"CAVLC 4:4:4 Intra (2C / 00010000) は libwebrtc 非認識のため拒否されるはずです"
);
assert!(
parse(0x00, 0b0000_0000, 30).is_none(),
"未知の profile_idc は拒否されるはずです"
);
}
#[test]
fn parses_level_1b_only_for_level_idc_11_with_constraint_set3() {
assert_profile_level(
&parse(0x42, 0b0101_0000, 11).expect("CB + 11 + cs3 は Level 1b になるはずです"),
H264SubProfile::ConstrainedBaseline,
H264Level::Level1b,
);
assert_profile_level(
&parse(0x42, 0b0100_0000, 11).expect("CB + 11 は Level 1.1 になるはずです"),
H264SubProfile::ConstrainedBaseline,
H264Level::Level1_1,
);
assert_profile_level(
&parse(0x4D, 0b0001_0000, 11).expect("M + 11 + cs3 は Level 1b になるはずです"),
H264SubProfile::Main,
H264Level::Level1b,
);
assert!(
parse(0x64, 0b0001_0000, 11).is_none(),
"High は cs3 を立てた iop が pattern に合致しないため拒否されるはずです"
);
}
#[test]
fn rejects_level_1b_with_level_idc_9() {
assert!(
parse(0x42, 0b0000_0000, 9).is_none(),
"level_idc == 9 の Level 1b 表現は拒否されるはずです"
);
assert!(
parse(0x64, 0b0000_0000, 9).is_none(),
"High でも level_idc == 9 は拒否されるはずです"
);
}
#[test]
fn rejects_level_6_and_unknown_level_idc() {
for level_idc in [60, 61, 62] {
assert!(
parse(0x42, 0b0000_0000, level_idc).is_none(),
"level_idc == {level_idc} は拒否されるはずです"
);
}
for level_idc in [0, 1, 15, 63, 100, 255] {
assert!(
parse(0x42, 0b0000_0000, level_idc).is_none(),
"未知の level_idc == {level_idc} は拒否されるはずです"
);
}
}
#[test]
fn orders_level_1b_between_level_1_and_level_1_1() {
assert!(
H264Level::Level1.capability_order() < H264Level::Level1b.capability_order(),
"Level 1 は Level 1b より小さいはずです"
);
assert!(
H264Level::Level1b.capability_order() < H264Level::Level1_1.capability_order(),
"Level 1b は Level 1.1 より小さいはずです"
);
let ordered = [
H264Level::Level1,
H264Level::Level1b,
H264Level::Level1_1,
H264Level::Level1_2,
H264Level::Level1_3,
H264Level::Level2,
H264Level::Level2_1,
H264Level::Level2_2,
H264Level::Level3,
H264Level::Level3_1,
H264Level::Level3_2,
H264Level::Level4,
H264Level::Level4_1,
H264Level::Level4_2,
H264Level::Level5,
H264Level::Level5_1,
H264Level::Level5_2,
];
for pair in ordered.windows(2) {
assert!(
pair[0].capability_order() < pair[1].capability_order(),
"level の順序が崩れています: {:?} < {:?}",
pair[0].capability_order(),
pair[1].capability_order()
);
}
}
#[test]
fn rejects_reserved_zero_bits_in_profile_iop() {
assert!(
parse(0x42, 0b0100_0011, 30).is_none(),
"CB でも reserved_zero_2bits 非 0 は拒否されるはずです"
);
assert!(
parse(0x64, 0b0000_0011, 30).is_none(),
"High でも reserved_zero_2bits 非 0 は拒否されるはずです"
);
}
fn h264_format(profile_level_id: &str) -> SdpVideoFormat {
let mut format = SdpVideoFormat::new("H264");
let mut params = format.parameters_mut();
params.set("packetization-mode", "1");
params.set("profile-level-id", profile_level_id);
format
}
#[test]
fn resolve_accepts_same_sub_profile_with_higher_level() {
let required = h264_format("4d0028"); let incoming = h264_format("4d0028"); let resolved = resolve_h264_incoming(&required, incoming.as_ref())
.expect("同一 format は受理されるはずです");
let mut resolved_owned = resolved.clone();
let params: std::collections::HashMap<String, String> =
resolved_owned.parameters_mut().iter().collect();
assert_eq!(
params.get("profile-level-id").map(String::as_str),
Some("4d0028"),
"検証済みの incoming format を parameter ごと保持して返すはずです"
);
let higher = h264_format("4d0032"); let resolved = resolve_h264_incoming(&required, higher.as_ref())
.expect("互換な higher level は受理されるはずです");
let mut resolved_owned = resolved.clone();
let params: std::collections::HashMap<String, String> =
resolved_owned.parameters_mut().iter().collect();
assert_eq!(
params.get("profile-level-id").map(String::as_str),
Some("4d0032"),
"higher level の negotiated format は parameter を保持するはずです"
);
}
#[test]
fn resolve_rejects_incompatible_sub_profile() {
let required = h264_format("4d0028"); let incoming = h264_format("640028"); assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"sub-profile が異なる format は拒否されるはずです"
);
}
#[test]
fn resolve_rejects_lower_level() {
let required = h264_format("4d0028"); let incoming = h264_format("4d001e"); assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"required より低い level は拒否されるはずです"
);
}
#[test]
fn resolve_level_1b_negotiation_bidirectional() {
let required_1b = h264_format("4d100b"); let incoming_1 = h264_format("4d000a"); assert!(
resolve_h264_incoming(&required_1b, incoming_1.as_ref()).is_none(),
"required が Level 1b のとき Level 1 は拒否されるはずです"
);
let required_1 = h264_format("4d000a"); let incoming_1b = h264_format("4d100b"); let resolved = resolve_h264_incoming(&required_1, incoming_1b.as_ref())
.expect("required が Level 1 のとき Level 1b は受理されるはずです");
let mut resolved_owned = resolved.clone();
let params: std::collections::HashMap<String, String> =
resolved_owned.parameters_mut().iter().collect();
assert_eq!(
params.get("profile-level-id").map(String::as_str),
Some("4d100b"),
"negotiated format は incoming の profile-level-id を保持するはずです"
);
let required_1_1 = h264_format("4d000b"); assert!(
resolve_h264_incoming(&required_1_1, incoming_1b.as_ref()).is_none(),
"required が Level 1.1 のとき Level 1b は拒否されるはずです"
);
let incoming_1_1 = h264_format("4d000b"); assert!(
resolve_h264_incoming(&required_1b, incoming_1_1.as_ref()).is_some(),
"required が Level 1b のとき Level 1.1 は受理されるはずです"
);
}
#[test]
fn resolve_rejects_missing_profile_level_id() {
let required = h264_format("4d0028");
let mut incoming = SdpVideoFormat::new("H264");
incoming.parameters_mut().set("packetization-mode", "1");
assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"profile-level-id のない incoming format は拒否されるはずです"
);
}
#[test]
fn resolve_rejects_invalid_profile_level_id() {
let required = h264_format("4d0028");
for bad in ["4d00", "4d002", "zz0028", "4d002g", "4d0 28"] {
let incoming = h264_format(bad);
assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"不正な profile-level-id ({bad:?}) は拒否されるはずです"
);
}
let unrecognized = h264_format("6e0028"); assert!(
resolve_h264_incoming(&required, unrecognized.as_ref()).is_none(),
"libwebrtc 非認識の profile は拒否されるはずです"
);
let unknown_level = h264_format("4d003c"); assert!(
resolve_h264_incoming(&required, unknown_level.as_ref()).is_none(),
"未知の level は拒否されるはずです"
);
}
#[test]
fn resolve_rejects_non_one_packetization_mode() {
let required = h264_format("4d0028");
let mut incoming = SdpVideoFormat::new("H264");
incoming.parameters_mut().set("packetization-mode", "0");
incoming.parameters_mut().set("profile-level-id", "4d0028");
assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"packetization-mode が 1 以外の format は拒否されるはずです"
);
}
#[test]
fn resolve_rejects_non_h264_codec_name() {
let required = h264_format("4d0028");
let incoming = SdpVideoFormat::new("VP8");
assert!(
resolve_h264_incoming(&required, incoming.as_ref()).is_none(),
"codec 名が H.264 以外の format は拒否されるはずです"
);
}
#[test]
fn h264_required_sdp_format_sets_packetization_mode_and_profile_level_id() {
let config = H264TrackConfig {
profile_level_id: H264ProfileLevelId {
profile_idc: 0x4d,
profile_iop: 0x40,
level_idc: 0x15,
},
avcc_box: None,
};
let format = h264_required_sdp_format(&config);
assert_eq!(
format
.name()
.expect("H264 format の name を取得できるはずです"),
"H264"
);
let mut format_owned = format.clone();
let params: std::collections::HashMap<String, String> =
format_owned.parameters_mut().iter().collect();
assert_eq!(
params.get("packetization-mode").map(String::as_str),
Some("1"),
"packetization-mode=1 を広告するはずです"
);
assert_eq!(
params.get("profile-level-id").map(String::as_str),
Some("4d4015"),
"検証済みの profile-level-id を広告するはずです"
);
}
}