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}