Skip to main content

o_sfu_core/options/
media.rs

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