Skip to main content

o_sfu_core/engine/source_model/
policy.rs

1use crate::{Bitrate, engine::VideoLayoutIntent};
2
3/// room policy applied to one published source
4///
5/// [`SourcePolicy`] is the source contract between application publish intent
6/// and core room policy
7/// it tells core what it may do with a source after
8/// publish, but it does not name the product feature that created the stream
9/// and it does not limit how many streams a user may publish
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct SourcePolicy {
12    layout: Option<SourceLayoutPolicy>,
13    adaptation: SourceAdaptationPolicy,
14    active_speaker: Option<ActiveSpeakerPolicy>,
15    video_bitrate_cap: Option<Bitrate>,
16}
17
18impl SourcePolicy {
19    #[must_use]
20    pub const fn new(
21        layout: Option<SourceLayoutPolicy>,
22        adaptation: SourceAdaptationPolicy,
23        active_speaker: Option<ActiveSpeakerPolicy>,
24    ) -> Self {
25        Self {
26            layout,
27            adaptation,
28            active_speaker,
29            video_bitrate_cap: None,
30        }
31    }
32
33    #[must_use]
34    pub const fn with_video_bitrate_cap(self, max_bitrate: Bitrate) -> Self {
35        Self {
36            video_bitrate_cap: Some(max_bitrate),
37            ..self
38        }
39    }
40
41    #[must_use]
42    pub const fn hidden() -> Self {
43        Self::new(None, SourceAdaptationPolicy::None, None)
44    }
45
46    #[must_use]
47    pub const fn layout(self) -> Option<SourceLayoutPolicy> {
48        self.layout
49    }
50
51    #[must_use]
52    pub const fn adaptation(self) -> SourceAdaptationPolicy {
53        self.adaptation
54    }
55
56    #[must_use]
57    pub const fn active_speaker(self) -> Option<ActiveSpeakerPolicy> {
58        self.active_speaker
59    }
60
61    #[must_use]
62    pub const fn video_bitrate_cap(self) -> Option<Bitrate> {
63        self.video_bitrate_cap
64    }
65}
66
67/// default receiver-layout role for one source
68///
69/// core combines publish intent, receiver layout preference and active-speaker
70/// state to choose one [`SourceRoomPolicySelector`] per receiver/source route
71/// sources without layout policy stay out of video budget planning
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct SourceLayoutPolicy {
74    visible_selector: SourceRoomPolicySelector,
75    active_speaker_selector: Option<SourceRoomPolicySelector>,
76}
77
78impl SourceLayoutPolicy {
79    #[must_use]
80    pub const fn new(
81        visible_selector: SourceRoomPolicySelector,
82        active_speaker_selector: Option<SourceRoomPolicySelector>,
83    ) -> Self {
84        Self {
85            visible_selector,
86            active_speaker_selector,
87        }
88    }
89
90    /// Resolves a receiver-specific layout role.
91    ///
92    /// Explicit [`VideoLayoutIntent`] wins so active-speaker observations cannot
93    /// override that receiver's layout. Without explicit intent, an active speaker
94    /// uses `active_speaker_selector` when configured. If neither explicit intent
95    /// nor active-speaker selection applies, `visible_selector` is used.
96    #[must_use]
97    pub fn resolve(
98        self,
99        preference: Option<VideoLayoutIntent>,
100        active_speaker: bool,
101    ) -> SourceRoomPolicySelector {
102        match preference {
103            Some(VideoLayoutIntent::Pinned) => SourceRoomPolicySelector::Pinned,
104            Some(VideoLayoutIntent::Featured) => SourceRoomPolicySelector::Featured,
105            Some(VideoLayoutIntent::Hidden) => SourceRoomPolicySelector::Hidden,
106            Some(VideoLayoutIntent::Overflow) => SourceRoomPolicySelector::Overflow,
107            None if active_speaker => self
108                .active_speaker_selector
109                .unwrap_or(self.visible_selector),
110            Some(VideoLayoutIntent::VisibleThumbnail) | None => self.visible_selector,
111        }
112    }
113}
114
115/// receiver bandwidth behavior for one published source
116///
117/// set by [`SourcePublishIntent`](crate::prelude::SourcePublishIntent) to decide
118/// whether the source participates in receiver-video layer selection, route
119/// pausing and over-budget diagnostics
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum SourceAdaptationPolicy {
122    /// keep this source out of receiver-video BWE control
123    ///
124    /// useful for audio or metadata-like sources that can route through normal
125    /// subscriptions without spending visible-video budget
126    None,
127    /// let the receiver-video planner budget the source and choose an encoding
128    ///
129    /// useful for sources where thumbnail routes may downswitch or pause under
130    /// receiver budget pressure
131    ScalableVideo,
132    /// keep readable detail ahead of normal thumbnail adaptation
133    ///
134    /// useful for text-heavy visual sources that stay on the highest advertised
135    /// encoding until lower-priority routes are exhausted then pause if needed
136    ReadableDetail,
137}
138
139/// active-speaker relationship declared for one source
140///
141/// publish intent decides which sources participate in transport-observed
142/// speech relationships
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct ActiveSpeakerPolicy {
145    group: ActiveSpeakerGroup,
146    role: ActiveSpeakerSourceRole,
147}
148
149impl ActiveSpeakerPolicy {
150    #[must_use]
151    pub const fn new(group: ActiveSpeakerGroup, role: ActiveSpeakerSourceRole) -> Self {
152        Self { group, role }
153    }
154
155    #[must_use]
156    pub const fn group(self) -> ActiveSpeakerGroup {
157        self.group
158    }
159
160    #[must_use]
161    pub const fn role(self) -> ActiveSpeakerSourceRole {
162        self.role
163    }
164}
165
166/// active-speaker group id used to separate speech relationships
167///
168/// groups keep unrelated speech domains separate without teaching core about
169/// application stream names
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct ActiveSpeakerGroup(u16);
172
173impl ActiveSpeakerGroup {
174    pub const MAIN: Self = Self(0);
175}
176
177/// role one source plays inside an active-speaker group
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum ActiveSpeakerSourceRole {
180    /// transport observations from this source can mark its owner active
181    ///
182    /// detectors are usually audio-like and do not receive video layout
183    /// treatment by themselves
184    Detector,
185    /// this source can receive active-speaker video treatment for its owner
186    ///
187    /// core promotes it only when a detector in the same group marks the same
188    /// owner as active
189    Promotable,
190}
191
192/// receiver-specific layout role before a concrete encoding is chosen
193///
194/// the budget planner reads this role to decide quality targets and pause order
195/// transport code only sees the final packet gate after the role resolves
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum SourceRoomPolicySelector {
198    /// the receiver explicitly pinned this source
199    ///
200    /// pinned routes use featured quality and share the highest budget priority
201    /// with featured routes
202    Pinned,
203    /// the receiver explicitly requested featured treatment for this source
204    ///
205    /// featured routes use featured quality and share the highest budget
206    /// priority with pinned routes
207    Featured,
208    /// source policy says readable detail matters for this route
209    ///
210    /// explicit pinned or featured receiver intent outranks this role
211    ReadableDetail,
212    /// active-speaker policy promoted this source for the current receiver
213    ///
214    /// explicit receiver intent and readable detail outrank this role
215    ActiveSpeaker,
216    /// the source is visible as a secondary tile
217    ///
218    /// visible thumbnails downswitch and pause before higher-priority routes
219    VisibleThumbnail,
220    /// the receiver is subscribed but the source is not visible right now
221    ///
222    /// hidden routes skip visible-video budget and are first candidates for
223    /// overload pause
224    Hidden,
225    /// the source is outside the receiver's visible tile set
226    ///
227    /// overflow routes behave like hidden routes but keep a distinct pause
228    /// reason for diagnostics
229    Overflow,
230}
231
232impl SourceRoomPolicySelector {
233    #[must_use]
234    pub const fn priority(self) -> SourceRoutePriority {
235        match self {
236            Self::Pinned | Self::Featured => SourceRoutePriority::PinnedOrFeatured,
237            Self::ReadableDetail => SourceRoutePriority::ReadableDetail,
238            Self::ActiveSpeaker => SourceRoutePriority::ActiveSpeaker,
239            Self::VisibleThumbnail => SourceRoutePriority::VisibleThumbnail,
240            Self::Hidden | Self::Overflow => SourceRoutePriority::HiddenOrOverflow,
241        }
242    }
243
244    #[must_use]
245    pub const fn uses_featured_quality(self) -> bool {
246        matches!(
247            self,
248            Self::Pinned | Self::Featured | Self::ReadableDetail | Self::ActiveSpeaker
249        )
250    }
251
252    #[must_use]
253    pub const fn counts_toward_visible_budget(self) -> bool {
254        !matches!(self, Self::Hidden | Self::Overflow)
255    }
256}
257
258/// overload priority derived from a route's room-policy selector
259///
260/// lower-priority buckets exhaust their cheaper encodings and pause before
261/// overload handling changes a higher-priority bucket
262#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
263pub enum SourceRoutePriority {
264    /// explicit receiver intent
265    ///
266    /// pinned and featured routes outrank every other route and pause last
267    PinnedOrFeatured,
268    /// detail-preserving source policy
269    ///
270    /// readable-detail routes outrank active-speaker routes but stay below
271    /// explicit pinned or featured receiver intent
272    ReadableDetail,
273    /// server-promoted active-speaker route
274    ///
275    /// explicit receiver intent and readable-detail source policy outrank this
276    /// role
277    ActiveSpeaker,
278    /// visible secondary route
279    ///
280    /// visible thumbnails downswitch and then pause before higher-priority routes
281    VisibleThumbnail,
282    /// route that is not currently visible
283    ///
284    /// hidden and overflow routes are first to pause under receiver budget
285    /// pressure
286    HiddenOrOverflow,
287}
288
289/// reason why room policy withholds media for a subscribed route
290///
291/// subscription state can remain active while the packet gate is closed for
292/// budget, layout or activation-cap reasons
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum PolicyPauseReason {
295    /// the receiver budget cannot fit this route after cheaper layers were tried
296    BudgetPressure,
297    /// the receiver layout explicitly hides this source
298    HiddenTile,
299    /// the receiver layout puts this source outside the visible tile set
300    OverflowTile,
301    /// no negotiated encoding can be forwarded usefully
302    MissingUsableLayer,
303    /// the active-audio-speaker cap withheld this route
304    AudioSpeakerLimit,
305    /// the receiver deafened itself so no audio is delivered to it
306    ReceiverDeafened,
307    /// the per-receiver live-video cap withheld this route
308    VideoDownloadLimit,
309    /// the source bitrate cap withheld this route
310    SourceBitrateLimit,
311}
312
313/// server-defined role for one published source encoding
314///
315/// the role lets the budget planner choose an encoding without reading
316/// application stream names
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub enum UploadLayerPolicyRole {
319    /// highest useful quality for high-priority or detail-focused routes
320    ///
321    /// the planner avoids this as the first cheap fallback when a lower-cost
322    /// encoding exists
323    Featured,
324    /// normal quality target for visible secondary video
325    ///
326    /// expected low-cost encoding before the planner considers pausing the route
327    Thumbnail,
328    /// lower-cost thumbnail rung below the normal thumbnail target
329    ///
330    /// reserved for upload ladders with more than two useful video encodings
331    DegradedThumbnail,
332}
333
334impl UploadLayerPolicyRole {
335    #[must_use]
336    pub const fn as_wire_value(self) -> &'static str {
337        match self {
338            Self::Featured => "featured",
339            Self::Thumbnail => "thumbnail",
340            Self::DegradedThumbnail => "degradedThumbnail",
341        }
342    }
343}