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}