Skip to main content

o_sfu_core/options/
media.rs

1use std::{fmt, net::SocketAddr, time::Duration};
2
3use crate::Bitrate;
4
5/// RTC packet-loop UDP I/O backend.
6///
7/// [`TokioBatch`](Self::TokioBatch) and [`IoUring`](Self::IoUring) are available
8/// only on Linux. Selecting either on another target makes transport
9/// construction return
10/// [`MediaTransportBuildError::UnsupportedUdpIoBackend`].
11///
12/// [`MediaTransportBuildError::UnsupportedUdpIoBackend`]:
13///     crate::engine::media_transport::MediaTransportBuildError::UnsupportedUdpIoBackend
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub enum RtcUdpIoBackend {
16    #[default]
17    Tokio,
18    /// Tokio UDP receives with `sendmmsg` batches for outgoing datagrams.
19    TokioBatch,
20    IoUring,
21}
22
23impl RtcUdpIoBackend {
24    #[must_use]
25    pub const fn wire_name(self) -> &'static str {
26        match self {
27            Self::Tokio => "tokio",
28            Self::TokioBatch => "tokio_batch",
29            Self::IoUring => "io_uring",
30        }
31    }
32}
33
34impl fmt::Display for RtcUdpIoBackend {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        f.write_str(self.wire_name())
37    }
38}
39
40/// Room media activation limits.
41///
42/// These limits control receiver delivery. They do not erase publication state
43/// or user subscription intent.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct RoomMediaLimits {
46    max_active_audio_speakers: usize,
47    max_video_downloads_per_receiver: usize,
48}
49
50/// Invalid room media limit input.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
52pub enum RoomMediaLimitsError {
53    #[error("maximum active audio speakers must be greater than zero")]
54    MaxActiveAudioSpeakersZero,
55    #[error("maximum video downloads per receiver must be greater than zero")]
56    MaxVideoDownloadsPerReceiverZero,
57}
58
59/// Invalid RTC port range bounds.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
61pub enum RtcPortRangeError {
62    #[error("RTC port range minimum must be greater than zero")]
63    MinZero,
64    #[error("RTC port range maximum must be greater than or equal to the minimum")]
65    MinAboveMax,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct RtcTcpConfig {
70    pub bind_addr: SocketAddr,
71    pub announced_addr: SocketAddr,
72    pub max_conn: usize,
73}
74
75/// Inclusive UDP port range assigned across RTC workers.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub struct RtcPortRange {
78    min: u16,
79    max: u16,
80}
81
82impl RtcPortRange {
83    /// Builds a port range from its inclusive bounds.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`RtcPortRangeError::MinZero`] when `min` is zero, or
88    /// [`RtcPortRangeError::MinAboveMax`] when `max` is lower than `min`.
89    pub const fn try_new(min: u16, max: u16) -> Result<Self, RtcPortRangeError> {
90        if min == 0 {
91            return Err(RtcPortRangeError::MinZero);
92        }
93        if max < min {
94            return Err(RtcPortRangeError::MinAboveMax);
95        }
96        Ok(Self { min, max })
97    }
98
99    #[must_use]
100    pub const fn min(self) -> u16 {
101        self.min
102    }
103
104    #[must_use]
105    pub const fn max(self) -> u16 {
106        self.max
107    }
108
109    /// Returns the number of ports in the range.
110    #[must_use]
111    pub const fn port_count(self) -> u16 {
112        self.max - self.min + 1
113    }
114
115    pub fn ports(self) -> impl Iterator<Item = u16> {
116        self.min..=self.max
117    }
118
119    /// Splits the range into contiguous worker ranges.
120    ///
121    /// Earlier workers receive one extra port when the range does not divide
122    /// evenly. Returns `None` for zero workers or more workers than ports.
123    #[must_use]
124    pub fn split_for_workers(self, worker_count: usize) -> Option<Vec<Self>> {
125        if worker_count == 0 || worker_count > usize::from(self.port_count()) {
126            return None;
127        }
128        let total_ports = usize::from(self.port_count());
129        let base_ports_per_worker = total_ports / worker_count;
130        let extra_ports = total_ports % worker_count;
131        let mut next_min = u32::from(self.min);
132        let mut ranges = Vec::with_capacity(worker_count);
133        for worker_idx in 0..worker_count {
134            let worker_port_count = base_ports_per_worker + usize::from(worker_idx < extra_ports);
135            let worker_port_count = u32::try_from(worker_port_count).ok()?;
136            let max_inclusive = next_min + worker_port_count - 1;
137            ranges.push(
138                Self::try_new(
139                    u16::try_from(next_min).ok()?,
140                    u16::try_from(max_inclusive).ok()?,
141                )
142                .ok()?,
143            );
144            next_min = max_inclusive + 1;
145        }
146        Some(ranges)
147    }
148}
149
150impl RoomMediaLimits {
151    pub const DEFAULT_MAX_ACTIVE_AUDIO_SPEAKERS: usize = 4;
152    pub const DEFAULT_MAX_VIDEO_DOWNLOADS_PER_RECEIVER: usize = 10;
153
154    /// Build room media limits after validating their invariants.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`RoomMediaLimitsError`] when a limit is zero.
159    pub const fn try_new(
160        max_active_audio_speakers: usize,
161        max_video_downloads_per_receiver: usize,
162    ) -> Result<Self, RoomMediaLimitsError> {
163        if max_active_audio_speakers == 0 {
164            return Err(RoomMediaLimitsError::MaxActiveAudioSpeakersZero);
165        }
166        if max_video_downloads_per_receiver == 0 {
167            return Err(RoomMediaLimitsError::MaxVideoDownloadsPerReceiverZero);
168        }
169        Ok(Self {
170            max_active_audio_speakers,
171            max_video_downloads_per_receiver,
172        })
173    }
174
175    #[must_use]
176    pub const fn conservative() -> Self {
177        Self {
178            max_active_audio_speakers: Self::DEFAULT_MAX_ACTIVE_AUDIO_SPEAKERS,
179            max_video_downloads_per_receiver: Self::DEFAULT_MAX_VIDEO_DOWNLOADS_PER_RECEIVER,
180        }
181    }
182
183    #[must_use]
184    pub const fn max_active_audio_speakers(self) -> usize {
185        self.max_active_audio_speakers
186    }
187
188    #[must_use]
189    pub const fn max_video_downloads_per_receiver(self) -> usize {
190        self.max_video_downloads_per_receiver
191    }
192}
193
194impl Default for RoomMediaLimits {
195    fn default() -> Self {
196        Self::conservative()
197    }
198}
199
200/// Room-wide video budget and adaptation dwell.
201///
202/// When room membership reaches `multiparty_scalable_video_threshold`,
203/// scalable-video per-route targets use available receiver bandwidth and the
204/// resolved layout role. Pinned, featured, readable-detail and active-speaker
205/// targets use the full receiver budget. When several visible scalable routes
206/// share a receiver, other scalable targets divide that budget by their count and
207/// `thumbnail_budget_divisor`. Soft pauses require continuous receiver pressure
208/// for `soft_pause_dwell`. Upgrades require one eligible target for `upgrade_dwell`.
209/// Headroom is removed before `audio_reserve_per_speaker` is subtracted
210/// for each admitted audio route the receiver consumes.
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub struct VideoAdaptationTuning {
213    pub(crate) multiparty_scalable_video_threshold: usize,
214    pub(crate) thumbnail_budget_divisor: u64,
215    pub(crate) soft_pause_dwell: Duration,
216    pub(crate) upgrade_dwell: Duration,
217    pub(crate) receiver_budget_headroom_percent: u8,
218    pub(crate) audio_reserve_per_speaker: Bitrate,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
222pub enum VideoAdaptationTuningError {
223    #[error("multiparty scalable video threshold must be greater than zero")]
224    MultipartyScalableVideoThresholdZero,
225    #[error("thumbnail budget divisor must be greater than zero")]
226    ThumbnailBudgetDivisorZero,
227    #[error("soft pause dwell must be greater than zero")]
228    SoftPauseDwellZero,
229    #[error("upgrade dwell must be greater than zero")]
230    UpgradeDwellZero,
231    #[error("soft pause dwell exceeds the portable deadline range")]
232    SoftPauseDwellTooLong,
233    #[error("upgrade dwell exceeds the portable deadline range")]
234    UpgradeDwellTooLong,
235    #[error("receiver budget headroom percent must not exceed 100")]
236    ReceiverBudgetHeadroomPercentTooHigh,
237}
238
239impl VideoAdaptationTuning {
240    pub const DEFAULT_MULTIPARTY_SCALABLE_VIDEO_THRESHOLD: usize = 3;
241    pub const DEFAULT_THUMBNAIL_BUDGET_DIVISOR: u64 = 2;
242    pub const DEFAULT_SOFT_PAUSE_DWELL: Duration = Duration::from_millis(750);
243    pub const DEFAULT_UPGRADE_DWELL: Duration = Duration::from_millis(750);
244    /// Bounds deadline addition to the roughly 100-year portable `Instant` range.
245    ///
246    /// See [`Instant` OS-specific behavior](std::time::Instant#os-specific-behaviors).
247    pub const MAX_DWELL: Duration = Duration::from_hours(100 * 365 * 24);
248    pub const DEFAULT_RECEIVER_BUDGET_HEADROOM_PERCENT: u8 = 0;
249    pub const DEFAULT_AUDIO_RESERVE_PER_SPEAKER: Bitrate = Bitrate::zero();
250
251    /// Builds validated video adaptation tuning.
252    ///
253    /// # Errors
254    ///
255    /// Returns [`VideoAdaptationTuningError`] when the scalable-video threshold,
256    /// the thumbnail budget divisor or either dwell is zero, either dwell exceeds
257    /// [`Self::MAX_DWELL`] or the headroom percent exceeds 100.
258    pub const fn try_new(
259        multiparty_scalable_video_threshold: usize,
260        thumbnail_budget_divisor: u64,
261        soft_pause_dwell: Duration,
262        upgrade_dwell: Duration,
263        receiver_budget_headroom_percent: u8,
264        audio_reserve_per_speaker: Bitrate,
265    ) -> Result<Self, VideoAdaptationTuningError> {
266        if multiparty_scalable_video_threshold == 0 {
267            return Err(VideoAdaptationTuningError::MultipartyScalableVideoThresholdZero);
268        }
269        if thumbnail_budget_divisor == 0 {
270            return Err(VideoAdaptationTuningError::ThumbnailBudgetDivisorZero);
271        }
272        if soft_pause_dwell.is_zero() {
273            return Err(VideoAdaptationTuningError::SoftPauseDwellZero);
274        }
275        if upgrade_dwell.is_zero() {
276            return Err(VideoAdaptationTuningError::UpgradeDwellZero);
277        }
278        if soft_pause_dwell.as_nanos() > Self::MAX_DWELL.as_nanos() {
279            return Err(VideoAdaptationTuningError::SoftPauseDwellTooLong);
280        }
281        if upgrade_dwell.as_nanos() > Self::MAX_DWELL.as_nanos() {
282            return Err(VideoAdaptationTuningError::UpgradeDwellTooLong);
283        }
284        if receiver_budget_headroom_percent > 100 {
285            return Err(VideoAdaptationTuningError::ReceiverBudgetHeadroomPercentTooHigh);
286        }
287        Ok(Self {
288            multiparty_scalable_video_threshold,
289            thumbnail_budget_divisor,
290            soft_pause_dwell,
291            upgrade_dwell,
292            receiver_budget_headroom_percent,
293            audio_reserve_per_speaker,
294        })
295    }
296}
297
298impl Default for VideoAdaptationTuning {
299    fn default() -> Self {
300        Self {
301            multiparty_scalable_video_threshold: Self::DEFAULT_MULTIPARTY_SCALABLE_VIDEO_THRESHOLD,
302            thumbnail_budget_divisor: Self::DEFAULT_THUMBNAIL_BUDGET_DIVISOR,
303            soft_pause_dwell: Self::DEFAULT_SOFT_PAUSE_DWELL,
304            upgrade_dwell: Self::DEFAULT_UPGRADE_DWELL,
305            receiver_budget_headroom_percent: Self::DEFAULT_RECEIVER_BUDGET_HEADROOM_PERCENT,
306            audio_reserve_per_speaker: Self::DEFAULT_AUDIO_RESERVE_PER_SPEAKER,
307        }
308    }
309}
310
311/// Per-session bandwidth policy applied by RTC workers.
312///
313/// `max_bitrate_in` sets REMB requests on producer receive streams.
314/// `max_bitrate_out` seeds str0m send-side BWE and caps room-selected desired
315/// bitrate updates.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub struct SessionBitrateLimits {
318    max_bitrate_in: Bitrate,
319    max_bitrate_out: Bitrate,
320}
321
322impl SessionBitrateLimits {
323    #[must_use]
324    pub const fn new(max_bitrate_in: Bitrate, max_bitrate_out: Bitrate) -> Self {
325        Self {
326            max_bitrate_in,
327            max_bitrate_out,
328        }
329    }
330
331    #[must_use]
332    pub const fn max_bitrate_in(self) -> Bitrate {
333        self.max_bitrate_in
334    }
335
336    #[must_use]
337    pub const fn max_bitrate_out(self) -> Bitrate {
338        self.max_bitrate_out
339    }
340}
341
342/// Bitrate limit used by generated VP8 and H.264 simulcast upload profiles.
343///
344/// The high RID uses `max_video_bitrate`. The low RID uses the lower of
345/// `max_video_bitrate` and 150 kbps. The middle RID uses one fifth of
346/// `max_video_bitrate`, raised to the low RID's limit when necessary.
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub struct VideoBitrateLimits {
349    max_video_bitrate: Bitrate,
350}
351
352impl VideoBitrateLimits {
353    pub const DEFAULT_MAX_VIDEO_BITRATE: Bitrate = Bitrate::from_mbps(4);
354
355    #[must_use]
356    pub const fn new(max_video_bitrate: Bitrate) -> Self {
357        Self { max_video_bitrate }
358    }
359
360    #[must_use]
361    pub const fn max_video_bitrate(self) -> Bitrate {
362        self.max_video_bitrate
363    }
364}
365
366impl Default for VideoBitrateLimits {
367    fn default() -> Self {
368        Self::new(Self::DEFAULT_MAX_VIDEO_BITRATE)
369    }
370}
371
372#[cfg(test)]
373#[path = "TESTS/media.rs"]
374mod tests;