Skip to main content

gsp/
capabilities.rs

1//! Capability negotiation (`CAPABILITIES_ADVERTISE`, gbp-control-plane ยง3).
2//!
3//! Capability negotiation lets every member tell the rest of the group what
4//! optional features it supports (codecs, extensions, version flags). The
5//! group's effective set is the **intersection** of every member's
6//! capabilities, so any feature outside the intersection is unsafe to use.
7
8use gbp_core::MemberId;
9use std::collections::{BTreeSet, HashMap};
10
11/// Per-member set of advertised capability tokens.
12#[derive(Default)]
13pub struct CapabilitiesNegotiator {
14    advertised: HashMap<MemberId, BTreeSet<String>>,
15}
16
17impl CapabilitiesNegotiator {
18    /// Empty negotiator (no member has advertised anything yet).
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Records an advertisement. Replaces any prior advertisement from the
24    /// same member.
25    pub fn advertise<I, S>(&mut self, member: MemberId, capabilities: I)
26    where
27        I: IntoIterator<Item = S>,
28        S: Into<String>,
29    {
30        let set: BTreeSet<String> = capabilities.into_iter().map(Into::into).collect();
31        self.advertised.insert(member, set);
32    }
33
34    /// Removes a member's advertisement (e.g. after `LEAVE`).
35    pub fn forget(&mut self, member: MemberId) {
36        self.advertised.remove(&member);
37    }
38
39    /// Returns the current advertisement for `member`.
40    pub fn capabilities_of(&self, member: MemberId) -> Option<&BTreeSet<String>> {
41        self.advertised.get(&member)
42    }
43
44    /// `true` if every advertised member supports `cap`.
45    pub fn group_supports(&self, cap: &str) -> bool {
46        if self.advertised.is_empty() {
47            return false;
48        }
49        self.advertised.values().all(|set| set.contains(cap))
50    }
51
52    /// Returns the **intersection** โ€” capabilities that every member
53    /// advertises, i.e. the safe-to-use set.
54    pub fn intersection(&self) -> BTreeSet<String> {
55        let mut iter = self.advertised.values();
56        let Some(first) = iter.next() else {
57            return BTreeSet::new();
58        };
59        let mut acc = first.clone();
60        for set in iter {
61            acc.retain(|c| set.contains(c));
62        }
63        acc
64    }
65
66    /// Returns the **union** โ€” every capability advertised by any member.
67    pub fn union(&self) -> BTreeSet<String> {
68        let mut acc = BTreeSet::new();
69        for set in self.advertised.values() {
70            for c in set {
71                acc.insert(c.clone());
72            }
73        }
74        acc
75    }
76
77    /// Returns the members that did **not** advertise `cap`.
78    pub fn missing(&self, cap: &str) -> Vec<MemberId> {
79        self.advertised
80            .iter()
81            .filter_map(|(m, set)| if set.contains(cap) { None } else { Some(*m) })
82            .collect()
83    }
84
85    /// Number of members that advertised something.
86    pub fn len(&self) -> usize {
87        self.advertised.len()
88    }
89
90    /// Empty?
91    pub fn is_empty(&self) -> bool {
92        self.advertised.is_empty()
93    }
94
95    /// Clears all advertisements. Call on epoch advance for symmetry with
96    /// [`GapClient::sync_epoch`], [`GtpClient::sync_epoch`] and
97    /// [`GspClient::sync_epoch`].
98    pub fn reset_for_epoch(&mut self) {
99        self.advertised.clear();
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn intersection_is_lowest_common() {
109        let mut n = CapabilitiesNegotiator::new();
110        n.advertise(1, ["opus", "fec", "h264"]);
111        n.advertise(2, ["opus", "fec"]);
112        n.advertise(3, ["opus", "av1"]);
113        let common = n.intersection();
114        assert!(common.contains("opus"));
115        assert!(!common.contains("fec"));
116        assert!(!common.contains("h264"));
117        assert_eq!(n.missing("fec"), vec![3]);
118    }
119
120    #[test]
121    fn group_supports_requires_everyone() {
122        let mut n = CapabilitiesNegotiator::new();
123        n.advertise(1, ["opus"]);
124        n.advertise(2, ["opus"]);
125        assert!(n.group_supports("opus"));
126        n.advertise(3, [] as [&str; 0]);
127        assert!(!n.group_supports("opus"));
128    }
129}