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