use crate::amf::Amf0Value;
pub const FOURCC_INFO_CAN_DECODE: u32 = 0x01;
pub const FOURCC_INFO_CAN_ENCODE: u32 = 0x02;
pub const FOURCC_INFO_CAN_FORWARD: u32 = 0x04;
pub const CAPS_EX_RECONNECT: u32 = 0x01;
pub const CAPS_EX_MULTITRACK: u32 = 0x02;
pub const CAPS_EX_MOD_EX: u32 = 0x04;
pub const CAPS_EX_TIMESTAMP_NANO_OFFSET: u32 = 0x08;
pub const OBJECT_ENCODING_AMF0: u8 = 0;
pub const OBJECT_ENCODING_AMF3: u8 = 3;
pub const FOURCC_WILDCARD: &str = "*";
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FourCcInfoMap {
entries: Vec<(String, u32)>,
}
impl FourCcInfoMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert<S: Into<String>>(&mut self, key: S, mask: u32) -> &mut Self {
let key = key.into();
if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == &key) {
slot.1 = mask;
} else {
self.entries.push((key, mask));
}
self
}
pub fn insert_fourcc(&mut self, fourcc: [u8; 4], mask: u32) -> &mut Self {
let s = String::from_utf8_lossy(&fourcc).into_owned();
self.insert(s, mask)
}
pub fn get(&self, key: &str) -> Option<u32> {
self.entries.iter().find(|(k, _)| k == key).map(|(_, m)| *m)
}
pub fn wildcard(&self) -> Option<u32> {
self.get(FOURCC_WILDCARD)
}
pub fn effective_mask(&self, key: &str) -> u32 {
let direct = self.get(key).unwrap_or(0);
direct | self.wildcard().unwrap_or(0)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, u32)> {
self.entries.iter().map(|(k, m)| (k.as_str(), *m))
}
pub fn to_amf0(&self) -> Amf0Value {
Amf0Value::Object(
self.entries
.iter()
.map(|(k, m)| (k.clone(), Amf0Value::Number(*m as f64)))
.collect(),
)
}
pub fn from_amf0(v: &Amf0Value) -> Self {
let pairs = match v {
Amf0Value::Object(p) | Amf0Value::EcmaArray(p) => p,
_ => return Self::new(),
};
let mut out = Self::new();
for (k, val) in pairs {
if let Amf0Value::Number(n) = val {
if n.is_finite() && *n >= 0.0 {
let m = if *n >= u32::MAX as f64 {
u32::MAX
} else {
*n as u32
};
out.insert(k.clone(), m);
}
}
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ConnectCapabilities {
pub fourcc_list: Vec<String>,
pub video_fourcc_info_map: FourCcInfoMap,
pub audio_fourcc_info_map: FourCcInfoMap,
pub caps_ex: u32,
pub object_encoding: Option<u8>,
}
impl ConnectCapabilities {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.fourcc_list.is_empty()
&& self.video_fourcc_info_map.is_empty()
&& self.audio_fourcc_info_map.is_empty()
&& self.caps_ex == 0
&& self.object_encoding.is_none()
}
pub fn supports_caps_ex(&self, mask: u32) -> bool {
self.caps_ex & mask != 0
}
pub fn has_fourcc(&self, key: &str) -> bool {
self.fourcc_list
.iter()
.any(|s| s == key || s == FOURCC_WILDCARD)
}
pub fn encode_into(&self, pairs: &mut Vec<(String, Amf0Value)>) {
if let Some(enc) = self.object_encoding {
pairs.push(("objectEncoding".into(), Amf0Value::Number(enc as f64)));
}
if !self.fourcc_list.is_empty() {
let arr = Amf0Value::StrictArray(
self.fourcc_list
.iter()
.map(|s| Amf0Value::String(s.clone()))
.collect(),
);
pairs.push(("fourCcList".into(), arr));
}
if !self.video_fourcc_info_map.is_empty() {
pairs.push((
"videoFourCcInfoMap".into(),
self.video_fourcc_info_map.to_amf0(),
));
}
if !self.audio_fourcc_info_map.is_empty() {
pairs.push((
"audioFourCcInfoMap".into(),
self.audio_fourcc_info_map.to_amf0(),
));
}
if self.caps_ex != 0 {
pairs.push(("capsEx".into(), Amf0Value::Number(self.caps_ex as f64)));
}
}
pub fn from_amf0(v: &Amf0Value) -> Self {
let mut out = Self::new();
let pairs: &[(String, Amf0Value)] = match v {
Amf0Value::Object(p) | Amf0Value::EcmaArray(p) => p.as_slice(),
_ => return out,
};
for (k, val) in pairs {
match k.as_str() {
"objectEncoding" => {
if let Amf0Value::Number(n) = val {
if n.is_finite() && *n >= 0.0 && *n <= u8::MAX as f64 {
out.object_encoding = Some(*n as u8);
}
}
}
"fourCcList" => {
if let Amf0Value::StrictArray(items) = val {
out.fourcc_list = items
.iter()
.filter_map(|it| match it {
Amf0Value::String(s) => Some(s.clone()),
_ => None,
})
.collect();
}
}
"videoFourCcInfoMap" => {
out.video_fourcc_info_map = FourCcInfoMap::from_amf0(val);
}
"audioFourCcInfoMap" => {
out.audio_fourcc_info_map = FourCcInfoMap::from_amf0(val);
}
"capsEx" => {
if let Amf0Value::Number(n) = val {
if n.is_finite() && *n >= 0.0 {
out.caps_ex = if *n >= u32::MAX as f64 {
u32::MAX
} else {
*n as u32
};
}
}
}
_ => { }
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::amf;
use crate::flv::{
FOURCC_AAC, FOURCC_AC3, FOURCC_AV1, FOURCC_AVC, FOURCC_EAC3, FOURCC_FLAC, FOURCC_HEVC,
FOURCC_MP3, FOURCC_OPUS, FOURCC_VP8, FOURCC_VP9, FOURCC_VVC,
};
fn fourcc_str(b: [u8; 4]) -> String {
std::str::from_utf8(&b).unwrap().to_owned()
}
#[test]
fn fourcc_info_mask_constants_match_spec() {
assert_eq!(FOURCC_INFO_CAN_DECODE, 0x01);
assert_eq!(FOURCC_INFO_CAN_ENCODE, 0x02);
assert_eq!(FOURCC_INFO_CAN_FORWARD, 0x04);
}
#[test]
fn caps_ex_mask_constants_match_spec() {
assert_eq!(CAPS_EX_RECONNECT, 0x01);
assert_eq!(CAPS_EX_MULTITRACK, 0x02);
assert_eq!(CAPS_EX_MOD_EX, 0x04);
assert_eq!(CAPS_EX_TIMESTAMP_NANO_OFFSET, 0x08);
}
#[test]
fn fourcc_wildcard_is_star() {
assert_eq!(FOURCC_WILDCARD, "*");
}
#[test]
fn fourcc_info_map_insert_preserves_order() {
let mut m = FourCcInfoMap::new();
m.insert("hvc1", FOURCC_INFO_CAN_DECODE);
m.insert_fourcc(FOURCC_AV1, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
m.insert("hvc1", FOURCC_INFO_CAN_FORWARD); let keys: Vec<_> = m.iter().map(|(k, _)| k.to_owned()).collect();
assert_eq!(keys, vec!["hvc1", "av01"]);
assert_eq!(m.get("hvc1"), Some(FOURCC_INFO_CAN_FORWARD));
}
#[test]
fn fourcc_info_map_wildcard_overrides_per_codec() {
let mut m = FourCcInfoMap::new();
m.insert("*", FOURCC_INFO_CAN_FORWARD);
m.insert("vp09", FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
assert_eq!(
m.effective_mask("vp09"),
FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE | FOURCC_INFO_CAN_FORWARD,
);
assert_eq!(m.effective_mask("xxxx"), FOURCC_INFO_CAN_FORWARD);
}
#[test]
fn fourcc_info_map_amf0_roundtrip() {
let mut m = FourCcInfoMap::new();
m.insert("*", FOURCC_INFO_CAN_FORWARD);
m.insert_fourcc(FOURCC_VP9, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
let v = m.to_amf0();
let back = FourCcInfoMap::from_amf0(&v);
assert_eq!(back, m);
}
#[test]
fn fourcc_info_map_skips_non_number_values() {
let v = Amf0Value::Object(vec![
("hvc1".into(), Amf0Value::Number(7.0)),
("Opus".into(), Amf0Value::String("nope".into())),
("avc1".into(), Amf0Value::Number(f64::NAN)),
("vp08".into(), Amf0Value::Number(-1.0)),
]);
let m = FourCcInfoMap::from_amf0(&v);
let keys: Vec<_> = m.iter().map(|(k, _)| k.to_owned()).collect();
assert_eq!(keys, vec!["hvc1"]);
assert_eq!(m.get("hvc1"), Some(7));
}
#[test]
fn fourcc_info_map_saturates_oversize_mask() {
let v = Amf0Value::Object(vec![("hvc1".into(), Amf0Value::Number(1e20))]);
let m = FourCcInfoMap::from_amf0(&v);
assert_eq!(m.get("hvc1"), Some(u32::MAX));
}
#[test]
fn default_capabilities_emit_nothing() {
let caps = ConnectCapabilities::default();
assert!(caps.is_empty());
let mut pairs = Vec::new();
caps.encode_into(&mut pairs);
assert!(pairs.is_empty());
}
#[test]
fn encode_into_uses_documented_order() {
let mut video = FourCcInfoMap::new();
video.insert("*", FOURCC_INFO_CAN_FORWARD);
let mut audio = FourCcInfoMap::new();
audio.insert_fourcc(FOURCC_OPUS, FOURCC_INFO_CAN_DECODE);
let caps = ConnectCapabilities {
object_encoding: Some(OBJECT_ENCODING_AMF3),
fourcc_list: vec![fourcc_str(FOURCC_HEVC), fourcc_str(FOURCC_AV1)],
video_fourcc_info_map: video,
audio_fourcc_info_map: audio,
caps_ex: CAPS_EX_RECONNECT | CAPS_EX_MULTITRACK,
};
let mut pairs = Vec::new();
caps.encode_into(&mut pairs);
let names: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(
names,
vec![
"objectEncoding",
"fourCcList",
"videoFourCcInfoMap",
"audioFourCcInfoMap",
"capsEx",
],
);
}
#[test]
fn full_capabilities_amf0_roundtrip() {
let mut video = FourCcInfoMap::new();
video.insert("*", FOURCC_INFO_CAN_FORWARD);
video.insert_fourcc(FOURCC_HEVC, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
video.insert_fourcc(FOURCC_VP9, FOURCC_INFO_CAN_DECODE);
let mut audio = FourCcInfoMap::new();
audio.insert("*", FOURCC_INFO_CAN_FORWARD);
audio.insert_fourcc(FOURCC_OPUS, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
let caps = ConnectCapabilities {
object_encoding: Some(OBJECT_ENCODING_AMF0),
fourcc_list: vec![
fourcc_str(FOURCC_AV1),
fourcc_str(FOURCC_VP9),
fourcc_str(FOURCC_VP8),
fourcc_str(FOURCC_HEVC),
fourcc_str(FOURCC_AVC),
fourcc_str(FOURCC_VVC),
fourcc_str(FOURCC_AC3),
fourcc_str(FOURCC_EAC3),
fourcc_str(FOURCC_OPUS),
fourcc_str(FOURCC_MP3),
fourcc_str(FOURCC_FLAC),
fourcc_str(FOURCC_AAC),
],
video_fourcc_info_map: video,
audio_fourcc_info_map: audio,
caps_ex: CAPS_EX_RECONNECT
| CAPS_EX_MULTITRACK
| CAPS_EX_MOD_EX
| CAPS_EX_TIMESTAMP_NANO_OFFSET,
};
let mut pairs = vec![("app".into(), Amf0Value::String("live".into()))];
caps.encode_into(&mut pairs);
let obj = Amf0Value::Object(pairs);
let mut buf = Vec::new();
amf::encode(&mut buf, &obj);
let mut pos = 0;
let decoded = amf::decode(&buf, &mut pos).unwrap();
let back = ConnectCapabilities::from_amf0(&decoded);
assert_eq!(back, caps);
}
#[test]
fn fourcc_list_wildcard_and_explicit() {
let caps = ConnectCapabilities {
fourcc_list: vec!["*".into()],
..Default::default()
};
assert!(caps.has_fourcc("av01"));
assert!(caps.has_fourcc("xxxx"));
let caps = ConnectCapabilities {
fourcc_list: vec![fourcc_str(FOURCC_HEVC)],
..Default::default()
};
assert!(caps.has_fourcc("hvc1"));
assert!(!caps.has_fourcc("av01"));
}
#[test]
fn caps_ex_bit_test() {
let caps = ConnectCapabilities {
caps_ex: CAPS_EX_RECONNECT | CAPS_EX_MOD_EX,
..Default::default()
};
assert!(caps.supports_caps_ex(CAPS_EX_RECONNECT));
assert!(caps.supports_caps_ex(CAPS_EX_MOD_EX));
assert!(!caps.supports_caps_ex(CAPS_EX_MULTITRACK));
assert!(caps.supports_caps_ex(CAPS_EX_RECONNECT | CAPS_EX_MOD_EX));
}
#[test]
fn malformed_caps_ex_falls_back_to_default() {
let obj = Amf0Value::Object(vec![
("capsEx".into(), Amf0Value::String("oops".into())),
(
"fourCcList".into(),
Amf0Value::StrictArray(vec![Amf0Value::String("av01".into())]),
),
]);
let caps = ConnectCapabilities::from_amf0(&obj);
assert_eq!(caps.caps_ex, 0);
assert_eq!(caps.fourcc_list, vec!["av01"]);
}
#[test]
fn from_amf0_non_object_returns_empty() {
let caps = ConnectCapabilities::from_amf0(&Amf0Value::Number(7.0));
assert!(caps.is_empty());
let caps = ConnectCapabilities::from_amf0(&Amf0Value::Null);
assert!(caps.is_empty());
}
#[test]
fn object_encoding_values() {
for &enc in &[OBJECT_ENCODING_AMF0, OBJECT_ENCODING_AMF3] {
let caps = ConnectCapabilities {
object_encoding: Some(enc),
..Default::default()
};
let mut pairs = Vec::new();
caps.encode_into(&mut pairs);
let back = ConnectCapabilities::from_amf0(&Amf0Value::Object(pairs));
assert_eq!(back.object_encoding, Some(enc));
}
}
#[test]
fn ecma_array_parses_as_capability_block() {
let arr = Amf0Value::EcmaArray(vec![
("capsEx".into(), Amf0Value::Number(0x05 as f64)),
(
"videoFourCcInfoMap".into(),
Amf0Value::Object(vec![("hvc1".into(), Amf0Value::Number(3.0))]),
),
]);
let caps = ConnectCapabilities::from_amf0(&arr);
assert_eq!(caps.caps_ex, 0x05);
assert_eq!(caps.video_fourcc_info_map.get("hvc1"), Some(3));
}
}