Skip to main content

o_sfu_core/options/
media.rs

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