oxpulse_sfu_kit/client/dc_builder.rs
1//! DataChannel builder methods on [`Client`].
2//!
3//! `with_extra_dc` is the generic primitive; `with_chat_dcs` and
4//! `with_voice_dc` are thin convenience shims.
5
6use super::Client;
7use crate::dc::ChannelConfig;
8
9impl Client {
10 /// Register an additional DataChannel to open during SDP negotiation.
11 ///
12 /// The `label`, `id`, and channel `cfg` are stored in [`Client::extra_dcs`].
13 /// The application signalling layer reads these during offer/answer and
14 /// calls `Rtc::open_stream(id, str0m_channel_config)` for each entry.
15 ///
16 /// # Example
17 ///
18 /// ```rust,no_run
19 /// use oxpulse_sfu_kit::{ChannelConfig, Client, SfuRtcBuilder, SfuMetrics};
20 /// use std::sync::Arc;
21 ///
22 /// let rtc = SfuRtcBuilder::new().build();
23 /// let client = Client::new(rtc, Arc::new(SfuMetrics::new_default()))
24 /// .with_extra_dc("telemetry", 20, ChannelConfig::reliable_ordered());
25 /// ```
26 #[must_use]
27 pub fn with_extra_dc(mut self, label: &str, id: u16, cfg: ChannelConfig) -> Self {
28 debug_assert!(
29 !self.extra_dcs.iter().any(|c| c.id() == id),
30 "DC id {} already registered (existing labels: {:?})",
31 id,
32 self.extra_dcs.iter().map(|c| c.label()).collect::<Vec<_>>()
33 );
34 debug_assert!(
35 cfg.max_packet_lifetime_ms.is_none() || cfg.max_retransmits.is_none(),
36 "ChannelConfig invariant violated: max_packet_lifetime_ms and max_retransmits \
37 cannot both be Some (label={label}, id={id})"
38 );
39 let mut entry = cfg;
40 entry.id = id;
41 entry.label = label.to_string();
42 self.extra_dcs.push(entry);
43 self
44 }
45
46 /// Register the standard OxPulse chat DataChannels (id=4 and id=5).
47 ///
48 /// - `"chat-data"` (id=4): reliable ordered — text messages.
49 /// - `"chat-ctrl"` (id=5): unreliable, 0 retransmits — typing / presence signals.
50 ///
51 /// This is a thin shim over [`with_extra_dc`][Self::with_extra_dc]; no
52 /// behaviour change relative to the pre-0.10.0 implementation.
53 #[must_use]
54 pub fn with_chat_dcs(self) -> Self {
55 self.with_extra_dc("chat-data", 4, ChannelConfig::reliable_ordered())
56 .with_extra_dc("chat-ctrl", 5, ChannelConfig::unreliable_max_retransmits(0))
57 }
58
59 /// Register the Phase 8 voice DataChannel (id=6).
60 ///
61 /// Uses unordered delivery with `Reliability::MaxPacketLifetime { lifetime: max_pkt_lifetime_ms }`.
62 /// For voice control signals, 200 ms is the recommended lifetime — packets older
63 /// than that are useless and should be discarded.
64 #[must_use]
65 pub fn with_voice_dc(self, max_pkt_lifetime_ms: u32) -> Self {
66 self.with_extra_dc(
67 "voice",
68 6,
69 ChannelConfig::unreliable_max_lifetime(max_pkt_lifetime_ms),
70 )
71 }
72}
73
74// ── Test seams ─────────────────────────────────────────────────────────────
75
76#[cfg(any(test, feature = "test-utils"))]
77impl Client {
78 /// Build a `Client` for integration tests without real ICE/DTLS setup.
79 ///
80 /// Identical to [`crate::client::test_seed::new_client`] but available as an associated
81 /// function on `Client` so tests can call `Client::new_for_test()` without
82 /// importing the internal module.
83 #[must_use]
84 pub fn new_for_test() -> Self {
85 crate::client::test_seed::new_client(crate::propagate::ClientId(u64::MAX))
86 }
87
88 /// Look up the [`ChannelConfig`] registered under `label` via `with_extra_dc`.
89 ///
90 /// Returns `None` if no DC with that label has been registered.
91 #[must_use]
92 pub fn dc_config_for(&self, label: &str) -> Option<&ChannelConfig> {
93 self.extra_dcs.iter().find(|dc| dc.label() == label)
94 }
95
96 /// Number of DataChannels registered via `with_extra_dc` (and its shims).
97 #[must_use]
98 pub fn dc_count(&self) -> usize {
99 self.extra_dcs.len()
100 }
101}