oxideav_rtmp/caps.rs
1//! Enhanced RTMP NetConnection `connect` capability negotiation.
2//!
3//! When a client opens a NetConnection it sends the `connect` command
4//! whose Command Object carries a small bag of name/value pairs that
5//! declare its protocol level. Enhanced RTMP v1 (Veovera, 2023) added
6//! the `fourCcList` strict-array of supported FourCC codecs. Enhanced
7//! RTMP v2 (Veovera, 2026, `enhanced-rtmp-v2.pdf` §"Enhancing
8//! NetConnection connect Command") added two further entries:
9//!
10//! * `videoFourCcInfoMap` / `audioFourCcInfoMap` — per-codec object
11//! maps whose values are bitmask numbers built from `FourCcInfoMask`
12//! (`CanDecode` / `CanEncode` / `CanForward`). A FourCC key of `"*"`
13//! acts as a catch-all for any codec.
14//! * `capsEx` — a single u32 bitfield built from `CapsExMask` declaring
15//! extended capabilities: `Reconnect`, `Multitrack`, `ModEx`,
16//! `TimestampNanoOffset`.
17//!
18//! Servers echo their own capabilities back in the `_result` reply's
19//! properties object using the same names so both sides converge on a
20//! common feature subset before any media flows.
21//!
22//! [`ConnectCapabilities`] is the strongly-typed representation of all
23//! four properties plus the legacy `objectEncoding` u8 (0 = AMF0, 3 =
24//! AMF0+AMF3 switch via `avmplus-object-marker`). It encodes/decodes
25//! against an [`Amf0Value`] graph via [`ConnectCapabilities::encode_into`]
26//! / [`ConnectCapabilities::from_amf0`] without disturbing the surrounding
27//! command-object key order — additions append after the legacy
28//! `audioCodecs` / `videoCodecs` / `videoFunction` block, so a pre-2023
29//! receiver still parses everything it understands.
30
31use crate::amf::Amf0Value;
32
33// ---------------------------------------------------------------------------
34// FourCcInfoMask — per-codec capability bits (v2 §"Enhancing connect")
35// ---------------------------------------------------------------------------
36
37/// `FourCcInfoMask.CanDecode` — the endpoint can decode this codec.
38pub const FOURCC_INFO_CAN_DECODE: u32 = 0x01;
39/// `FourCcInfoMask.CanEncode` — the endpoint can encode this codec.
40pub const FOURCC_INFO_CAN_ENCODE: u32 = 0x02;
41/// `FourCcInfoMask.CanForward` — the endpoint can forward the codec
42/// without decoding (relay / recorder / forwarding ingest).
43pub const FOURCC_INFO_CAN_FORWARD: u32 = 0x04;
44
45// ---------------------------------------------------------------------------
46// CapsExMask — extended-capability bitfield (v2 §"Enhancing connect")
47// ---------------------------------------------------------------------------
48
49/// `CapsExMask.Reconnect` — the endpoint honours the
50/// `NetConnection.Connect.ReconnectRequest` onStatus event.
51pub const CAPS_EX_RECONNECT: u32 = 0x01;
52/// `CapsExMask.Multitrack` — the endpoint understands the v2 Multitrack
53/// audio + video PacketTypes (per-track FourCC + size-prefixed track
54/// chunks).
55pub const CAPS_EX_MULTITRACK: u32 = 0x02;
56/// `CapsExMask.ModEx` — the endpoint can parse the v2 ModEx
57/// packet-type prelude (size-prefixed extension chain ahead of the
58/// real packet type).
59pub const CAPS_EX_MOD_EX: u32 = 0x04;
60/// `CapsExMask.TimestampNanoOffset` — the endpoint applies the
61/// ModEx `TimestampOffsetNano = 0` sub-millisecond presentation offset
62/// to its decode pipeline.
63pub const CAPS_EX_TIMESTAMP_NANO_OFFSET: u32 = 0x08;
64
65/// `objectEncoding` value 0 — AMF0-only.
66pub const OBJECT_ENCODING_AMF0: u8 = 0;
67/// `objectEncoding` value 3 — AMF0 with `avmplus-object-marker`
68/// switching to AMF3 per the AMF0 spec.
69pub const OBJECT_ENCODING_AMF3: u8 = 3;
70
71/// FourCC wildcard string — a key of `"*"` in a FourCcInfoMap means
72/// "applies to every codec in the relevant audio/video bucket".
73pub const FOURCC_WILDCARD: &str = "*";
74
75// ---------------------------------------------------------------------------
76// FourCcInfoMap — `videoFourCcInfoMap` / `audioFourCcInfoMap`
77// ---------------------------------------------------------------------------
78
79/// `videoFourCcInfoMap` / `audioFourCcInfoMap` payload — an ordered
80/// list of `(fourCC-or-wildcard, mask)` pairs.
81///
82/// The spec stores this as an AMF0 Object whose property names are
83/// FourCC ASCII (e.g. `"hvc1"`, `"Opus"`) or the catch-all `"*"`, and
84/// whose values are numbers built from [`FOURCC_INFO_CAN_DECODE`] etc.
85/// We keep the entries in insertion order — most peers walk the
86/// properties top-down looking for a wildcard first, then per-codec
87/// overrides, so order is informally load-bearing even though the spec
88/// doesn't mandate it.
89#[derive(Debug, Clone, Default, PartialEq, Eq)]
90pub struct FourCcInfoMap {
91 entries: Vec<(String, u32)>,
92}
93
94impl FourCcInfoMap {
95 /// Empty map — declares no per-codec capabilities.
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 /// Append an entry with the given FourCC string and mask. If `key`
101 /// already exists the new mask replaces the existing value while
102 /// preserving insertion position.
103 pub fn insert<S: Into<String>>(&mut self, key: S, mask: u32) -> &mut Self {
104 let key = key.into();
105 if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == &key) {
106 slot.1 = mask;
107 } else {
108 self.entries.push((key, mask));
109 }
110 self
111 }
112
113 /// Convenience: insert a `[u8; 4]` FourCC verbatim. The bytes are
114 /// taken to be ASCII (e.g. `*b"hvc1"`); a non-ASCII byte falls back
115 /// to the lossy UTF-8 conversion, which keeps round-tripping clean
116 /// against a forwarding peer.
117 pub fn insert_fourcc(&mut self, fourcc: [u8; 4], mask: u32) -> &mut Self {
118 let s = String::from_utf8_lossy(&fourcc).into_owned();
119 self.insert(s, mask)
120 }
121
122 /// Look up a mask for the given key. Returns `None` if the key
123 /// isn't present — callers that want wildcard fallback should also
124 /// check [`Self::wildcard`].
125 pub fn get(&self, key: &str) -> Option<u32> {
126 self.entries.iter().find(|(k, _)| k == key).map(|(_, m)| *m)
127 }
128
129 /// Mask carried by the catch-all `"*"` key, if any.
130 pub fn wildcard(&self) -> Option<u32> {
131 self.get(FOURCC_WILDCARD)
132 }
133
134 /// Effective mask for `key`, applying the v2 spec rule that a
135 /// wildcard entry overrides per-codec entries for any flag it sets.
136 pub fn effective_mask(&self, key: &str) -> u32 {
137 let direct = self.get(key).unwrap_or(0);
138 direct | self.wildcard().unwrap_or(0)
139 }
140
141 /// Number of entries.
142 pub fn len(&self) -> usize {
143 self.entries.len()
144 }
145
146 /// True if the map carries no entries.
147 pub fn is_empty(&self) -> bool {
148 self.entries.is_empty()
149 }
150
151 /// Iterate `(key, mask)` pairs in insertion order.
152 pub fn iter(&self) -> impl Iterator<Item = (&str, u32)> {
153 self.entries.iter().map(|(k, m)| (k.as_str(), *m))
154 }
155
156 /// Encode as the AMF0 Object the spec expects.
157 pub fn to_amf0(&self) -> Amf0Value {
158 Amf0Value::Object(
159 self.entries
160 .iter()
161 .map(|(k, m)| (k.clone(), Amf0Value::Number(*m as f64)))
162 .collect(),
163 )
164 }
165
166 /// Lift from an AMF0 Object (or ECMA-array, which some peers emit).
167 /// Non-numeric / non-finite / negative-valued entries are silently
168 /// skipped — the spec demands numeric mask bits and a forged
169 /// `String` or `Date` payload here would otherwise pull the rest of
170 /// the connect command into a hard error. Out-of-u32 numbers
171 /// saturate to `u32::MAX`.
172 pub fn from_amf0(v: &Amf0Value) -> Self {
173 let pairs = match v {
174 Amf0Value::Object(p) | Amf0Value::EcmaArray(p) => p,
175 _ => return Self::new(),
176 };
177 let mut out = Self::new();
178 for (k, val) in pairs {
179 if let Amf0Value::Number(n) = val {
180 if n.is_finite() && *n >= 0.0 {
181 let m = if *n >= u32::MAX as f64 {
182 u32::MAX
183 } else {
184 *n as u32
185 };
186 out.insert(k.clone(), m);
187 }
188 }
189 }
190 out
191 }
192}
193
194// ---------------------------------------------------------------------------
195// ConnectCapabilities — the full v1+v2 capability block.
196// ---------------------------------------------------------------------------
197
198/// Capability block exchanged in the NetConnection `connect` command.
199///
200/// Owns all four spec entries (`fourCcList`, `videoFourCcInfoMap`,
201/// `audioFourCcInfoMap`, `capsEx`) plus the long-standing
202/// `objectEncoding` byte. Encoded into the existing Command Object by
203/// [`Self::encode_into`] without touching the surrounding key order, and
204/// parsed back with [`Self::from_amf0`] from either the client's
205/// Command Object or the server's `_result` properties object.
206///
207/// A default-constructed instance is empty — `is_empty()` is true and
208/// `encode_into` writes nothing, so a caller composing a legacy AVC /
209/// AAC-only `connect` command keeps the pre-2023 byte layout exactly.
210#[derive(Debug, Clone, Default, PartialEq, Eq)]
211pub struct ConnectCapabilities {
212 /// `fourCcList` — Enhanced RTMP v1 strict-array of supported FourCC
213 /// strings (e.g. `"av01"`, `"hvc1"`). The v2 spec deprecates this on
214 /// the client side in favour of `audio/videoFourCcInfoMap`, but
215 /// servers are encouraged to keep supporting both for older clients.
216 pub fourcc_list: Vec<String>,
217 /// `videoFourCcInfoMap` — v2 per-codec capability bits for video
218 /// codecs.
219 pub video_fourcc_info_map: FourCcInfoMap,
220 /// `audioFourCcInfoMap` — v2 per-codec capability bits for audio
221 /// codecs.
222 pub audio_fourcc_info_map: FourCcInfoMap,
223 /// `capsEx` — v2 bag of extended capability bits.
224 pub caps_ex: u32,
225 /// `objectEncoding` — 0 for AMF0-only, 3 for AMF0+AMF3.
226 pub object_encoding: Option<u8>,
227}
228
229impl ConnectCapabilities {
230 /// Empty capability block — encodes to nothing.
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 /// True when every field is empty / default.
236 pub fn is_empty(&self) -> bool {
237 self.fourcc_list.is_empty()
238 && self.video_fourcc_info_map.is_empty()
239 && self.audio_fourcc_info_map.is_empty()
240 && self.caps_ex == 0
241 && self.object_encoding.is_none()
242 }
243
244 /// Test for a specific `CapsExMask` flag.
245 pub fn supports_caps_ex(&self, mask: u32) -> bool {
246 self.caps_ex & mask != 0
247 }
248
249 /// True when `fourcc_list` includes either the wildcard `"*"` or
250 /// the literal FourCC `key`.
251 pub fn has_fourcc(&self, key: &str) -> bool {
252 self.fourcc_list
253 .iter()
254 .any(|s| s == key || s == FOURCC_WILDCARD)
255 }
256
257 /// Append our capability properties to a command-object pair list.
258 ///
259 /// Each property is only appended when the corresponding field is
260 /// non-default, so encoding an empty block adds zero bytes. Caller
261 /// keeps any other Command Object properties they want around the
262 /// call site — `pairs` is mutated in place.
263 pub fn encode_into(&self, pairs: &mut Vec<(String, Amf0Value)>) {
264 if let Some(enc) = self.object_encoding {
265 pairs.push(("objectEncoding".into(), Amf0Value::Number(enc as f64)));
266 }
267 if !self.fourcc_list.is_empty() {
268 let arr = Amf0Value::StrictArray(
269 self.fourcc_list
270 .iter()
271 .map(|s| Amf0Value::String(s.clone()))
272 .collect(),
273 );
274 pairs.push(("fourCcList".into(), arr));
275 }
276 if !self.video_fourcc_info_map.is_empty() {
277 pairs.push((
278 "videoFourCcInfoMap".into(),
279 self.video_fourcc_info_map.to_amf0(),
280 ));
281 }
282 if !self.audio_fourcc_info_map.is_empty() {
283 pairs.push((
284 "audioFourCcInfoMap".into(),
285 self.audio_fourcc_info_map.to_amf0(),
286 ));
287 }
288 if self.caps_ex != 0 {
289 pairs.push(("capsEx".into(), Amf0Value::Number(self.caps_ex as f64)));
290 }
291 }
292
293 /// Parse any subset of capability properties out of an Object /
294 /// ECMA-array. Missing properties stay at their default; malformed
295 /// values are silently ignored (the spec's "fail gracefully" rule —
296 /// a forged `capsEx = "abc"` from a stale peer must not abort the
297 /// connect handshake).
298 pub fn from_amf0(v: &Amf0Value) -> Self {
299 let mut out = Self::new();
300 let pairs: &[(String, Amf0Value)] = match v {
301 Amf0Value::Object(p) | Amf0Value::EcmaArray(p) => p.as_slice(),
302 _ => return out,
303 };
304 for (k, val) in pairs {
305 match k.as_str() {
306 "objectEncoding" => {
307 if let Amf0Value::Number(n) = val {
308 if n.is_finite() && *n >= 0.0 && *n <= u8::MAX as f64 {
309 out.object_encoding = Some(*n as u8);
310 }
311 }
312 }
313 "fourCcList" => {
314 if let Amf0Value::StrictArray(items) = val {
315 out.fourcc_list = items
316 .iter()
317 .filter_map(|it| match it {
318 Amf0Value::String(s) => Some(s.clone()),
319 _ => None,
320 })
321 .collect();
322 }
323 }
324 "videoFourCcInfoMap" => {
325 out.video_fourcc_info_map = FourCcInfoMap::from_amf0(val);
326 }
327 "audioFourCcInfoMap" => {
328 out.audio_fourcc_info_map = FourCcInfoMap::from_amf0(val);
329 }
330 "capsEx" => {
331 if let Amf0Value::Number(n) = val {
332 if n.is_finite() && *n >= 0.0 {
333 out.caps_ex = if *n >= u32::MAX as f64 {
334 u32::MAX
335 } else {
336 *n as u32
337 };
338 }
339 }
340 }
341 _ => { /* not a capability property — ignored */ }
342 }
343 }
344 out
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::amf;
352 use crate::flv::{
353 FOURCC_AAC, FOURCC_AC3, FOURCC_AV1, FOURCC_AVC, FOURCC_EAC3, FOURCC_FLAC, FOURCC_HEVC,
354 FOURCC_MP3, FOURCC_OPUS, FOURCC_VP8, FOURCC_VP9, FOURCC_VVC,
355 };
356
357 fn fourcc_str(b: [u8; 4]) -> String {
358 std::str::from_utf8(&b).unwrap().to_owned()
359 }
360
361 /// `FourCcInfoMask` constants match the spec table verbatim.
362 #[test]
363 fn fourcc_info_mask_constants_match_spec() {
364 // From enhanced-rtmp-v2.pdf §"Enhancing NetConnection connect Command":
365 // enum FourCcInfoMask { CanDecode = 0x01, CanEncode = 0x02, CanForward = 0x04 }
366 assert_eq!(FOURCC_INFO_CAN_DECODE, 0x01);
367 assert_eq!(FOURCC_INFO_CAN_ENCODE, 0x02);
368 assert_eq!(FOURCC_INFO_CAN_FORWARD, 0x04);
369 }
370
371 /// `CapsExMask` constants match the spec table verbatim.
372 #[test]
373 fn caps_ex_mask_constants_match_spec() {
374 // enum CapsExMask {
375 // Reconnect = 0x01, Multitrack = 0x02, ModEx = 0x04, TimestampNanoOffset = 0x08
376 // }
377 assert_eq!(CAPS_EX_RECONNECT, 0x01);
378 assert_eq!(CAPS_EX_MULTITRACK, 0x02);
379 assert_eq!(CAPS_EX_MOD_EX, 0x04);
380 assert_eq!(CAPS_EX_TIMESTAMP_NANO_OFFSET, 0x08);
381 }
382
383 /// FourCC wildcard is the single-byte `"*"`.
384 #[test]
385 fn fourcc_wildcard_is_star() {
386 assert_eq!(FOURCC_WILDCARD, "*");
387 }
388
389 /// `FourCcInfoMap::insert` preserves insertion order and replaces
390 /// duplicate keys without moving them.
391 #[test]
392 fn fourcc_info_map_insert_preserves_order() {
393 let mut m = FourCcInfoMap::new();
394 m.insert("hvc1", FOURCC_INFO_CAN_DECODE);
395 m.insert_fourcc(FOURCC_AV1, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
396 m.insert("hvc1", FOURCC_INFO_CAN_FORWARD); // replace
397 let keys: Vec<_> = m.iter().map(|(k, _)| k.to_owned()).collect();
398 assert_eq!(keys, vec!["hvc1", "av01"]);
399 assert_eq!(m.get("hvc1"), Some(FOURCC_INFO_CAN_FORWARD));
400 }
401
402 /// `effective_mask` ORs in the wildcard entry per spec.
403 #[test]
404 fn fourcc_info_map_wildcard_overrides_per_codec() {
405 let mut m = FourCcInfoMap::new();
406 m.insert("*", FOURCC_INFO_CAN_FORWARD);
407 m.insert("vp09", FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
408 // Per-codec entry is preserved, wildcard adds CanForward on top.
409 assert_eq!(
410 m.effective_mask("vp09"),
411 FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE | FOURCC_INFO_CAN_FORWARD,
412 );
413 // Unknown codec inherits wildcard alone.
414 assert_eq!(m.effective_mask("xxxx"), FOURCC_INFO_CAN_FORWARD);
415 }
416
417 /// `FourCcInfoMap` round-trips through the AMF0 Object shape.
418 #[test]
419 fn fourcc_info_map_amf0_roundtrip() {
420 let mut m = FourCcInfoMap::new();
421 m.insert("*", FOURCC_INFO_CAN_FORWARD);
422 m.insert_fourcc(FOURCC_VP9, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
423 let v = m.to_amf0();
424 let back = FourCcInfoMap::from_amf0(&v);
425 assert_eq!(back, m);
426 }
427
428 /// Malformed mask entries are dropped, not propagated.
429 #[test]
430 fn fourcc_info_map_skips_non_number_values() {
431 let v = Amf0Value::Object(vec![
432 ("hvc1".into(), Amf0Value::Number(7.0)),
433 ("Opus".into(), Amf0Value::String("nope".into())),
434 ("avc1".into(), Amf0Value::Number(f64::NAN)),
435 ("vp08".into(), Amf0Value::Number(-1.0)),
436 ]);
437 let m = FourCcInfoMap::from_amf0(&v);
438 let keys: Vec<_> = m.iter().map(|(k, _)| k.to_owned()).collect();
439 assert_eq!(keys, vec!["hvc1"]);
440 assert_eq!(m.get("hvc1"), Some(7));
441 }
442
443 /// Out-of-u32 numeric mask saturates to `u32::MAX`.
444 #[test]
445 fn fourcc_info_map_saturates_oversize_mask() {
446 let v = Amf0Value::Object(vec![("hvc1".into(), Amf0Value::Number(1e20))]);
447 let m = FourCcInfoMap::from_amf0(&v);
448 assert_eq!(m.get("hvc1"), Some(u32::MAX));
449 }
450
451 /// Default capability block is empty and writes no bytes.
452 #[test]
453 fn default_capabilities_emit_nothing() {
454 let caps = ConnectCapabilities::default();
455 assert!(caps.is_empty());
456 let mut pairs = Vec::new();
457 caps.encode_into(&mut pairs);
458 assert!(pairs.is_empty());
459 }
460
461 /// Encoded properties land in the documented v1+v2 order:
462 /// `objectEncoding`, `fourCcList`, `videoFourCcInfoMap`,
463 /// `audioFourCcInfoMap`, `capsEx`.
464 #[test]
465 fn encode_into_uses_documented_order() {
466 let mut video = FourCcInfoMap::new();
467 video.insert("*", FOURCC_INFO_CAN_FORWARD);
468 let mut audio = FourCcInfoMap::new();
469 audio.insert_fourcc(FOURCC_OPUS, FOURCC_INFO_CAN_DECODE);
470 let caps = ConnectCapabilities {
471 object_encoding: Some(OBJECT_ENCODING_AMF3),
472 fourcc_list: vec![fourcc_str(FOURCC_HEVC), fourcc_str(FOURCC_AV1)],
473 video_fourcc_info_map: video,
474 audio_fourcc_info_map: audio,
475 caps_ex: CAPS_EX_RECONNECT | CAPS_EX_MULTITRACK,
476 };
477
478 let mut pairs = Vec::new();
479 caps.encode_into(&mut pairs);
480 let names: Vec<&str> = pairs.iter().map(|(k, _)| k.as_str()).collect();
481 assert_eq!(
482 names,
483 vec![
484 "objectEncoding",
485 "fourCcList",
486 "videoFourCcInfoMap",
487 "audioFourCcInfoMap",
488 "capsEx",
489 ],
490 );
491 }
492
493 /// Round-trip a fully-populated capability block through encode →
494 /// AMF0 wire → decode and assert every field comes back equal.
495 #[test]
496 fn full_capabilities_amf0_roundtrip() {
497 let mut video = FourCcInfoMap::new();
498 video.insert("*", FOURCC_INFO_CAN_FORWARD);
499 video.insert_fourcc(FOURCC_HEVC, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
500 video.insert_fourcc(FOURCC_VP9, FOURCC_INFO_CAN_DECODE);
501 let mut audio = FourCcInfoMap::new();
502 audio.insert("*", FOURCC_INFO_CAN_FORWARD);
503 audio.insert_fourcc(FOURCC_OPUS, FOURCC_INFO_CAN_DECODE | FOURCC_INFO_CAN_ENCODE);
504 let caps = ConnectCapabilities {
505 object_encoding: Some(OBJECT_ENCODING_AMF0),
506 fourcc_list: vec![
507 fourcc_str(FOURCC_AV1),
508 fourcc_str(FOURCC_VP9),
509 fourcc_str(FOURCC_VP8),
510 fourcc_str(FOURCC_HEVC),
511 fourcc_str(FOURCC_AVC),
512 fourcc_str(FOURCC_VVC),
513 fourcc_str(FOURCC_AC3),
514 fourcc_str(FOURCC_EAC3),
515 fourcc_str(FOURCC_OPUS),
516 fourcc_str(FOURCC_MP3),
517 fourcc_str(FOURCC_FLAC),
518 fourcc_str(FOURCC_AAC),
519 ],
520 video_fourcc_info_map: video,
521 audio_fourcc_info_map: audio,
522 caps_ex: CAPS_EX_RECONNECT
523 | CAPS_EX_MULTITRACK
524 | CAPS_EX_MOD_EX
525 | CAPS_EX_TIMESTAMP_NANO_OFFSET,
526 };
527
528 let mut pairs = vec![("app".into(), Amf0Value::String("live".into()))];
529 caps.encode_into(&mut pairs);
530 let obj = Amf0Value::Object(pairs);
531 // Encode-decode the wire bytes so the round-trip walks the same
532 // AMF0 path used by the live `connect` handshake.
533 let mut buf = Vec::new();
534 amf::encode(&mut buf, &obj);
535 let mut pos = 0;
536 let decoded = amf::decode(&buf, &mut pos).unwrap();
537 let back = ConnectCapabilities::from_amf0(&decoded);
538 assert_eq!(back, caps);
539 }
540
541 /// `has_fourcc` recognises both the exact entry and a `"*"`
542 /// wildcard.
543 #[test]
544 fn fourcc_list_wildcard_and_explicit() {
545 let caps = ConnectCapabilities {
546 fourcc_list: vec!["*".into()],
547 ..Default::default()
548 };
549 assert!(caps.has_fourcc("av01"));
550 assert!(caps.has_fourcc("xxxx"));
551
552 let caps = ConnectCapabilities {
553 fourcc_list: vec![fourcc_str(FOURCC_HEVC)],
554 ..Default::default()
555 };
556 assert!(caps.has_fourcc("hvc1"));
557 assert!(!caps.has_fourcc("av01"));
558 }
559
560 /// `supports_caps_ex` is a bit-wise AND.
561 #[test]
562 fn caps_ex_bit_test() {
563 let caps = ConnectCapabilities {
564 caps_ex: CAPS_EX_RECONNECT | CAPS_EX_MOD_EX,
565 ..Default::default()
566 };
567 assert!(caps.supports_caps_ex(CAPS_EX_RECONNECT));
568 assert!(caps.supports_caps_ex(CAPS_EX_MOD_EX));
569 assert!(!caps.supports_caps_ex(CAPS_EX_MULTITRACK));
570 // Combined-mask test: requires BOTH bits set.
571 assert!(caps.supports_caps_ex(CAPS_EX_RECONNECT | CAPS_EX_MOD_EX));
572 }
573
574 /// A forged Object whose `capsEx` is a String parses cleanly with
575 /// the rest of the block intact.
576 #[test]
577 fn malformed_caps_ex_falls_back_to_default() {
578 let obj = Amf0Value::Object(vec![
579 ("capsEx".into(), Amf0Value::String("oops".into())),
580 (
581 "fourCcList".into(),
582 Amf0Value::StrictArray(vec![Amf0Value::String("av01".into())]),
583 ),
584 ]);
585 let caps = ConnectCapabilities::from_amf0(&obj);
586 assert_eq!(caps.caps_ex, 0);
587 assert_eq!(caps.fourcc_list, vec!["av01"]);
588 }
589
590 /// Non-object inputs return an empty block (a forwarder may pass a
591 /// pre-resolved capability struct in via a top-level Number for
592 /// instance — that's silently ignored).
593 #[test]
594 fn from_amf0_non_object_returns_empty() {
595 let caps = ConnectCapabilities::from_amf0(&Amf0Value::Number(7.0));
596 assert!(caps.is_empty());
597 let caps = ConnectCapabilities::from_amf0(&Amf0Value::Null);
598 assert!(caps.is_empty());
599 }
600
601 /// `objectEncoding` must round-trip the documented 0 / 3 values.
602 #[test]
603 fn object_encoding_values() {
604 for &enc in &[OBJECT_ENCODING_AMF0, OBJECT_ENCODING_AMF3] {
605 let caps = ConnectCapabilities {
606 object_encoding: Some(enc),
607 ..Default::default()
608 };
609 let mut pairs = Vec::new();
610 caps.encode_into(&mut pairs);
611 let back = ConnectCapabilities::from_amf0(&Amf0Value::Object(pairs));
612 assert_eq!(back.object_encoding, Some(enc));
613 }
614 }
615
616 /// An ECMA-array carrying the capability properties parses the same
617 /// way an Object does. Some commodity peers use ECMA-array for the
618 /// `_result` properties slot.
619 #[test]
620 fn ecma_array_parses_as_capability_block() {
621 let arr = Amf0Value::EcmaArray(vec![
622 ("capsEx".into(), Amf0Value::Number(0x05 as f64)),
623 (
624 "videoFourCcInfoMap".into(),
625 Amf0Value::Object(vec![("hvc1".into(), Amf0Value::Number(3.0))]),
626 ),
627 ]);
628 let caps = ConnectCapabilities::from_amf0(&arr);
629 assert_eq!(caps.caps_ex, 0x05);
630 assert_eq!(caps.video_fourcc_info_map.get("hvc1"), Some(3));
631 }
632}