Skip to main content

o_sfu_core/engine/room/source_policy/video/
solver.rs

1//! receiver-video policy turn
2//!
3//! [`SourcePolicyTransaction`]: filter -> budget -> plan -> admit -> fit -> hysteresis -> projection
4//! packet-gate changes stay behind [`projection`] so planning never builds transport gates directly
5
6use std::{cmp::Reverse, collections::BTreeMap};
7
8use itertools::Itertools;
9
10use super::{
11    super::{input::SourcePolicySnapshot, turn::SourcePolicyTransaction},
12    input::{ReceiverVideoRouteInput, receiver_video_routes},
13    projection,
14};
15use crate::{
16    Bitrate, VideoAdaptationTuning,
17    engine::{
18        UserId,
19        media_transport::ReceiverBweTargetUpdate,
20        room::state::RoomState,
21        source_model::{
22            ConsumerSourceSelection, PolicyPauseReason, PublishedSourceDescriptor,
23            PublishedSourceId, ReceiverVideoBudgetDiagnostics, SourceAdaptationPolicy,
24            SourceEncodingDescriptor, SourceRoomPolicySelector, SourceRoutePriority,
25            SourceSelector, UploadLayerPolicyRole,
26        },
27    },
28};
29
30/// ordering key shared by policy refresh and pre-setup receiver admission
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
32pub struct VideoAdmissionRank {
33    /// lower values admit more important layout roles first
34    priority: u8,
35    /// active-speaker order, or `usize::MAX` for non-speakers
36    active_speaker_rank: usize,
37    /// deterministic tie-breaker after priority and speaker rank
38    source_id: u64,
39}
40
41impl VideoAdmissionRank {
42    pub const fn new(
43        priority: SourceRoutePriority,
44        active_speaker_rank: Option<usize>,
45        source_id: PublishedSourceId,
46    ) -> Self {
47        Self {
48            priority: match priority {
49                SourceRoutePriority::PinnedOrFeatured => 0,
50                SourceRoutePriority::ReadableDetail => 1,
51                SourceRoutePriority::ActiveSpeaker => 2,
52                SourceRoutePriority::VisibleThumbnail => 3,
53                SourceRoutePriority::HiddenOrOverflow => 4,
54            },
55            active_speaker_rank: match active_speaker_rank {
56                Some(rank) => rank,
57                None => usize::MAX,
58            },
59            source_id: source_id.as_u64(),
60        }
61    }
62}
63
64/// hysteresis counters persisted in [`ConsumerSourceSelection`] between policy turns
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
66pub(super) struct AdaptationCounts {
67    /// consecutive pressure observations toward a soft downswitch or pause
68    pub(super) pressure: u8,
69    /// consecutive stable observations toward a soft resume or upswitch
70    pub(super) upgrade: u8,
71}
72
73impl AdaptationCounts {
74    const fn reset() -> Self {
75        Self {
76            pressure: 0,
77            upgrade: 0,
78        }
79    }
80
81    pub(super) fn from_current(selection: ConsumerSourceSelection) -> Self {
82        Self {
83            pressure: selection.pressure_observations(),
84            upgrade: selection.upgrade_observations(),
85        }
86    }
87
88    fn next_pressure(selection: ConsumerSourceSelection, limit: u8) -> Self {
89        Self {
90            pressure: selection
91                .pressure_observations()
92                .saturating_add(1)
93                .min(limit),
94            upgrade: 0,
95        }
96    }
97
98    fn next_upgrade(selection: ConsumerSourceSelection, limit: u8) -> Self {
99        Self {
100            pressure: 0,
101            upgrade: selection
102                .upgrade_observations()
103                .saturating_add(1)
104                .min(limit),
105        }
106    }
107}
108
109/// candidate selector and policy pause state before projection
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub(super) struct ReceiverRouteSelection {
112    /// packet selector to commit if the route stays deliverable
113    pub(super) selector: SourceSelector,
114    /// policy reason that keeps receiver intent while blocking delivery
115    pub(super) policy_pause_reason: Option<PolicyPauseReason>,
116    /// hysteresis state carried into the next policy turn
117    pub(super) counts: AdaptationCounts,
118    /// decoder refresh requested after selector changes or delivery resumes
119    pub(super) request_keyframe: bool,
120}
121
122impl ReceiverRouteSelection {
123    const fn send(
124        selector: SourceSelector,
125        counts: AdaptationCounts,
126        request_keyframe: bool,
127    ) -> Self {
128        Self {
129            selector,
130            policy_pause_reason: None,
131            counts,
132            request_keyframe,
133        }
134    }
135
136    const fn pause(
137        current: ConsumerSourceSelection,
138        reason: PolicyPauseReason,
139        counts: AdaptationCounts,
140    ) -> Self {
141        Self {
142            selector: current.selector(),
143            policy_pause_reason: Some(reason),
144            counts,
145            request_keyframe: false,
146        }
147    }
148
149    const fn hold(
150        current: ConsumerSourceSelection,
151        policy_pause_reason: Option<PolicyPauseReason>,
152        counts: AdaptationCounts,
153    ) -> Self {
154        Self {
155            selector: current.selector(),
156            policy_pause_reason,
157            counts,
158            request_keyframe: false,
159        }
160    }
161}
162
163/// metric outcome produced by budget and admission decisions
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub(super) enum RouteOutcome {
166    /// no downgrade or pause recorded by the video solver
167    Neutral,
168    /// selected layer is lower than the previous committed selector
169    Degraded,
170    /// route is paused by video policy
171    Paused,
172}
173
174/// planned receiver route passed to [`projection`] after admission and budget pressure
175///
176/// `selected_bitrate`, `selection` and `outcome` must change together through
177/// [`Self::send`] or [`Self::pause`]
178#[derive(Debug, Clone, Copy)]
179pub(super) struct PlannedReceiverRoute<'a> {
180    /// immutable route facts captured before policy mutation
181    pub(super) input: &'a ReceiverVideoRouteInput<'a>,
182    /// bitrate counted against receiver budget after this policy step
183    pub(super) selected_bitrate: Bitrate,
184    /// candidate selector, pause state and hysteresis state
185    pub(super) selection: ReceiverRouteSelection,
186    /// diagnostic outcome attached to projection
187    pub(super) outcome: RouteOutcome,
188}
189
190impl<'a> PlannedReceiverRoute<'a> {
191    fn new(input: &'a ReceiverVideoRouteInput<'a>, selection: ReceiverRouteSelection) -> Self {
192        let selected_bitrate = if selection.policy_pause_reason.is_some() {
193            Bitrate::zero()
194        } else {
195            selector_bitrate(input, selection.selector).unwrap_or_default()
196        };
197        let current_bitrate =
198            selector_bitrate(input, input.current_selection.selector()).unwrap_or_default();
199        let outcome = if selected_bitrate < current_bitrate {
200            RouteOutcome::Degraded
201        } else {
202            RouteOutcome::Neutral
203        };
204        Self {
205            input,
206            selected_bitrate,
207            selection,
208            outcome,
209        }
210    }
211
212    fn send(&mut self, selector: SourceSelector, selected_bitrate: Bitrate, outcome: RouteOutcome) {
213        self.selected_bitrate = selected_bitrate;
214        self.selection = ReceiverRouteSelection::send(
215            selector,
216            self.selection.counts,
217            self.selection.request_keyframe,
218        );
219        self.outcome = outcome;
220    }
221
222    fn pause(&mut self, reason: PolicyPauseReason, outcome: RouteOutcome) {
223        self.selected_bitrate = Bitrate::zero();
224        self.selection = ReceiverRouteSelection::pause(
225            self.input.current_selection,
226            reason,
227            self.selection.counts,
228        );
229        self.outcome = outcome;
230    }
231}
232
233pub(in crate::engine::room::source_policy) fn append_receiver_video_policy(
234    tx: &mut SourcePolicyTransaction,
235    state: &RoomState,
236    input: &SourcePolicySnapshot<'_>,
237    mut receiver_bwe_targets: BTreeMap<UserId, ReceiverBweTargetUpdate>,
238) {
239    let routes = receiver_video_routes(state, input);
240    let max_video_downloads_per_receiver = input.media_limits.max_video_downloads_per_receiver();
241    let tuning = input.video_adaptation_tuning;
242    // `committed_consumer_routes` is ordered by `SubscriptionKey` with
243    // `receiver` first. Preserve that order so `chunk_by` sees each complete
244    // receiver allocation.
245    for receiver_routes in routes.chunk_by(|left, right| left.key.receiver == right.key.receiver) {
246        let Some(first_route) = receiver_routes.first() else {
247            continue;
248        };
249        let consumer_user_id = &first_route.key.receiver;
250        let selected_video = append_receiver_policy_updates(
251            tx,
252            receiver_routes,
253            max_video_downloads_per_receiver,
254            tuning,
255        );
256        // The target is seeded with this receiver's audio reserve, so add the
257        // selected video to report the full send allocation to str0m's BWE.
258        if let Some(update) = receiver_bwe_targets.get_mut(consumer_user_id) {
259            update.set_target(update.target().saturating_add(selected_video));
260        }
261    }
262    tx.set_receiver_bwe_targets(receiver_bwe_targets.into_values().collect());
263}
264
265/// Solves video subscription quality for a receiver based on layout intent and network capacity.
266///
267/// [`receiver_video_routes`] first retains video routes with an adaptation policy or source bitrate
268/// cap. This function computes the optional video budget before calling `route_plan` for each
269/// retained route.
270///
271/// Balances visual experience against available bandwidth through a multi-pass allocation:
272///
273/// 1. **Desired Quality Target**: Builds each route's selection from its policy-specific facts.
274///    An unpaused selector with no bitrate is paused as
275///    [`PolicyPauseReason::MissingUsableLayer`].
276/// 2. **Stream Count Capping**: Enforces the receiver download limit across planned routes. Lowest-priority
277///    streams are paused before bandwidth sharing. Equal ranks use active-route position.
278/// 3. **Bandwidth Fitting**: When aggregate bitrate exceeds optional downlink headroom after the
279///    audio reserve, secondary streams are stepped down layer-by-layer. If demand still exceeds
280///    budget after eligible step-downs, the lowest-priority streams are paused. Each pass stops once
281///    demand fits.
282/// 4. **Hysteresis Smoothing**: `scalable_plan` applies selector hysteresis. `resolve_hysteresis`
283///    delays soft pauses and most resumes. New download-limit and source-cap pauses apply
284///    immediately. A route resuming from [`PolicyPauseReason::VideoDownloadLimit`] also resumes
285///    immediately.
286/// 5. **Decision Emission**: Projection returns an optional packet-selection update. The caller
287///    stages it as a transport effect or state-only update.
288///
289/// ```text
290///      Policy-Managed Video Routes                Downlink Bandwidth Estimate
291///                    \                                   /
292///                     v                                 v
293///          +-------------------------------------------------------+
294///          | 1. Desired Quality Target                             |
295///          |    - Multiparty featured --> featured-quality layer   |
296///          |    - Multiparty secondary --> thumbnail-biased layer  |
297///          +-------------------------------------------------------+
298///                                     |
299///                                     v
300///          +-------------------------------------------------------+
301///          | 2. Stream Count Capping (Top-N)                       |
302///          |    - Active planned streams > receiver limit?         |
303///          |      yes -> pause lowest-ranked excess positions      |
304///          |      no  -> keep planned streams active               |
305///          +-------------------------------------------------------+
306///                                     |
307///                                     v
308///          +-------------------------------------------------------+
309///          | 3. Bandwidth Fitting & Degradation                    |
310///          |    - Total bitrate <= bandwidth budget?               |
311///          |      yes -> fit as-is                                 |
312///          |      no  -> step down eligible layers one-by-one      |
313///          |             pause lowest-priority tiles if congested  |
314///          +-------------------------------------------------------+
315///                                     |
316///                                     v
317///          +-------------------------------------------------------+
318///          | 4. Hysteresis Smoothing                               |
319///          |    - Smooth selector changes and route oscillation    |
320///          |    - Delay soft pauses and most resumes               |
321///          +-------------------------------------------------------+
322///                                     |
323///                                     v
324///              Selection Updates + Allocated Bandwidth
325/// ```
326fn append_receiver_policy_updates<'a>(
327    tx: &mut SourcePolicyTransaction,
328    receiver_routes: &'a [ReceiverVideoRouteInput<'a>],
329    max_video_downloads_per_receiver: usize,
330    tuning: VideoAdaptationTuning,
331) -> Bitrate {
332    let receiver_bandwidth = receiver_routes
333        .iter()
334        .find_map(|route| route.receiver_bandwidth);
335    let audio_reserve = receiver_routes
336        .first()
337        .map_or_else(Bitrate::zero, |route| route.audio_budget_reserve);
338    let video_budget = receiver_bandwidth
339        .map(|bandwidth| effective_video_budget(bandwidth, tuning, audio_reserve));
340    let mut planned_routes = receiver_routes
341        .iter()
342        .filter_map(|route| {
343            route_plan(route, tuning).map(|selection| PlannedReceiverRoute::new(route, selection))
344        })
345        .collect::<Vec<_>>();
346    // Apply the hard route count before sharing bandwidth. Rejected routes must
347    // not consume receiver budget or force admitted routes down.
348    apply_video_download_limit(&mut planned_routes, max_video_downloads_per_receiver);
349    if let Some(video_budget) = video_budget {
350        apply_overload_policy(&mut planned_routes, video_budget);
351    }
352    let budget_diagnostics =
353        receiver_video_budget_diagnostics(&planned_routes, receiver_bandwidth, video_budget);
354    for planned_route in planned_routes {
355        let selection = resolve_hysteresis(&planned_route, tuning);
356        let Some(update) = projection::consumer_packet_selection_update(
357            &planned_route,
358            selection,
359            budget_diagnostics,
360        ) else {
361            continue;
362        };
363        if update.requires_media_transport_effect() {
364            tx.push_route_update(update);
365        } else {
366            tx.push_state_update(update);
367        }
368    }
369    budget_diagnostics.selected_video_bitrate()
370}
371
372fn route_plan(
373    route: &ReceiverVideoRouteInput<'_>,
374    tuning: VideoAdaptationTuning,
375) -> Option<ReceiverRouteSelection> {
376    let policy = route.source.policy().adaptation();
377    let source_cap = route.source.policy().video_bitrate_cap();
378    if source_cap.is_some_and(|cap| source_exceeds_bitrate_cap(route, cap)) {
379        return Some(ReceiverRouteSelection::pause(
380            route.current_selection,
381            PolicyPauseReason::SourceBitrateLimit,
382            AdaptationCounts::reset(),
383        ));
384    }
385    let selection = match policy {
386        SourceAdaptationPolicy::ReadableDetail
387            if route.source.selectable_encoding_count() < 2 && source_cap.is_none() =>
388        {
389            None
390        }
391        SourceAdaptationPolicy::ReadableDetail => highest_allowed_plan(route),
392        SourceAdaptationPolicy::ScalableVideo => scalable_plan(route, tuning),
393        SourceAdaptationPolicy::None if source_cap.is_some() => highest_allowed_plan(route),
394        SourceAdaptationPolicy::None => None,
395    }
396    .or_else(|| match policy {
397        SourceAdaptationPolicy::None if source_cap.is_none() => None,
398        _ if source_cap.is_some() => Some(ReceiverRouteSelection::pause(
399            route.current_selection,
400            PolicyPauseReason::SourceBitrateLimit,
401            AdaptationCounts::reset(),
402        )),
403        _ => Some(adaptation_hold(
404            route.current_selection,
405            AdaptationCounts::from_current(route.current_selection),
406        )),
407    })?;
408    if selection.policy_pause_reason.is_none()
409        && selector_bitrate(route, selection.selector).is_none()
410    {
411        return Some(ReceiverRouteSelection::pause(
412            route.current_selection,
413            PolicyPauseReason::MissingUsableLayer,
414            AdaptationCounts::reset(),
415        ));
416    }
417    Some(selection)
418}
419
420fn source_exceeds_bitrate_cap(route: &ReceiverVideoRouteInput<'_>, cap: Bitrate) -> bool {
421    if route.source.selectable_encoding_count() > 1 {
422        return false;
423    }
424    // Missing data cannot prove recovery from a cap violation. Keep the route
425    // paused until a measured source rate is at or below the cap.
426    route.source_bitrate.map_or_else(
427        || {
428            route.current_selection.policy_pause_reason()
429                == Some(PolicyPauseReason::SourceBitrateLimit)
430        },
431        |observed| observed > cap,
432    )
433}
434
435fn selector_bitrate(
436    route: &ReceiverVideoRouteInput<'_>,
437    selector: SourceSelector,
438) -> Option<Bitrate> {
439    let source = route.source;
440    let declared = selector.selected_encoding().map_or_else(
441        || {
442            source
443                .selectable_encodings()
444                .filter_map(SourceEncodingDescriptor::max_bitrate)
445                .max()
446        },
447        |encoding_id| {
448            source
449                .selectable_encodings()
450                .find(|encoding| encoding.encoding_id() == encoding_id)
451                .and_then(SourceEncodingDescriptor::max_bitrate)
452        },
453    );
454    // A per-media observation aggregates every RID. Use it only when policy has
455    // at most one selectable encoding and needs no per-RID estimate.
456    let observed = route
457        .source_bitrate
458        .filter(|_| source.selectable_encoding_count() <= 1);
459    declared.max(observed)
460}
461
462fn highest_allowed_plan(route: &ReceiverVideoRouteInput<'_>) -> Option<ReceiverRouteSelection> {
463    let source_cap = route.source.policy().video_bitrate_cap();
464    if route.source.selectable_encoding_count() == 0 {
465        return source_cap
466            .is_none_or(|cap| route.source_bitrate.is_some_and(|rate| rate <= cap))
467            .then(|| adaptation_send(SourceSelector::Open, false));
468    }
469    let target_index = allowed_encoding_indices(route.source, source_cap)
470        .next_back()
471        .or_else(|| {
472            (route.source.selectable_encoding_count() == 1
473                && source_cap
474                    .is_some_and(|cap| route.source_bitrate.is_some_and(|rate| rate <= cap)))
475            .then_some(0)
476        })?;
477    let target_selector = SourceSelector::Encoding(
478        route
479            .source
480            .selectable_encoding_by_rank(target_index)?
481            .encoding_id(),
482    );
483    Some(adaptation_send(
484        target_selector,
485        target_selector != route.current_selection.selector(),
486    ))
487}
488
489fn cheapest_useful_selector(
490    route: &ReceiverVideoRouteInput<'_>,
491) -> Option<(SourceSelector, Bitrate)> {
492    let source_cap = route.source.policy().video_bitrate_cap();
493    route
494        .source
495        .selectable_encodings()
496        .filter(|encoding| {
497            !matches!(
498                encoding.policy_role(),
499                Some(UploadLayerPolicyRole::Featured)
500            )
501        })
502        .chain(route.source.selectable_encodings())
503        .find_map(|encoding| {
504            let bitrate = encoding.max_bitrate()?;
505            source_cap
506                .is_none_or(|source_cap| bitrate <= source_cap)
507                .then_some((SourceSelector::Encoding(encoding.encoding_id()), bitrate))
508        })
509}
510
511/// Separates configured downlink headroom from audio consumption so receivers
512/// reserve audio only for admitted routes they can hear.
513fn effective_video_budget(
514    receiver_bandwidth: Bitrate,
515    tuning: VideoAdaptationTuning,
516    audio_reserve: Bitrate,
517) -> Bitrate {
518    let usable_percent = 100u64.saturating_sub(u64::from(tuning.receiver_budget_headroom_percent));
519    let after_overhead =
520        Bitrate::from_bps(receiver_bandwidth.as_bps().saturating_mul(usable_percent) / 100);
521    after_overhead.saturating_sub(audio_reserve)
522}
523
524/// next allowed, non-featured layer one step below `current`, with its bitrate
525///
526/// returns `None` when the route is already at its lowest usable layer, letting
527/// the overload pass stop degrading a route before it bottoms out
528fn step_down_selector(
529    route: &ReceiverVideoRouteInput<'_>,
530    current: SourceSelector,
531) -> Option<(SourceSelector, Bitrate)> {
532    let source = route.source;
533    let source_cap = source.policy().video_bitrate_cap();
534    let current_index = selector_index(source, current);
535    (0..current_index).rev().find_map(|index| {
536        let encoding = source.selectable_encoding_by_rank(index)?;
537        if matches!(
538            encoding.policy_role(),
539            Some(UploadLayerPolicyRole::Featured)
540        ) {
541            return None;
542        }
543        let bitrate = encoding.max_bitrate()?;
544        source_cap
545            .is_none_or(|cap| bitrate <= cap)
546            .then_some((SourceSelector::Encoding(encoding.encoding_id()), bitrate))
547    })
548}
549
550fn scalable_plan(
551    route: &ReceiverVideoRouteInput<'_>,
552    tuning: VideoAdaptationTuning,
553) -> Option<ReceiverRouteSelection> {
554    let source_cap = route.source.policy().video_bitrate_cap();
555    if route.source.selectable_encoding_count() < 2 && source_cap.is_none() {
556        return None;
557    }
558    let current = route.current_selection;
559    let current_index = selector_index(route.source, current.selector());
560    let target_index = desired_encoding_index(route, source_cap, tuning)?;
561    let target_selector = SourceSelector::Encoding(
562        route
563            .source
564            .selectable_encoding_by_rank(target_index)?
565            .encoding_id(),
566    );
567    let request_keyframe = target_selector != current.selector();
568    if target_index == current_index || route.receiver_bandwidth.is_none() {
569        return Some(adaptation_send(target_selector, request_keyframe));
570    }
571    if target_index < current_index {
572        if source_cap.is_some_and(|cap| {
573            selector_bitrate(route, current.selector()).is_some_and(|bitrate| bitrate > cap)
574        }) {
575            return Some(adaptation_send(target_selector, request_keyframe));
576        }
577        let pressure_limit = tuning.downswitch_pressure_observations;
578        let counts = AdaptationCounts::next_pressure(current, pressure_limit);
579        if counts.pressure >= pressure_limit {
580            return Some(adaptation_send(target_selector, request_keyframe));
581        }
582        return Some(adaptation_hold(current, counts));
583    }
584    let stable_limit = tuning.upswitch_stable_observations;
585    let counts = AdaptationCounts::next_upgrade(current, stable_limit);
586    if counts.upgrade >= stable_limit {
587        return Some(adaptation_send(target_selector, true));
588    }
589    Some(adaptation_hold(current, counts))
590}
591
592const fn adaptation_send(
593    selector: SourceSelector,
594    request_keyframe: bool,
595) -> ReceiverRouteSelection {
596    ReceiverRouteSelection::send(selector, AdaptationCounts::reset(), request_keyframe)
597}
598
599const fn adaptation_hold(
600    current: ConsumerSourceSelection,
601    counts: AdaptationCounts,
602) -> ReceiverRouteSelection {
603    ReceiverRouteSelection::send(current.selector(), counts, false)
604}
605
606fn desired_encoding_index(
607    route: &ReceiverVideoRouteInput<'_>,
608    source_cap: Option<Bitrate>,
609    tuning: VideoAdaptationTuning,
610) -> Option<usize> {
611    if route.user_count < tuning.multiparty_scalable_video_threshold {
612        return allowed_encoding_indices(route.source, source_cap).next_back();
613    }
614    let uses_featured_quality = route.layout_role.uses_featured_quality();
615    let Some(receiver_bandwidth) = route.receiver_bandwidth else {
616        return if uses_featured_quality {
617            allowed_encoding_indices(route.source, source_cap).next_back()
618        } else {
619            allowed_encoding_indices(route.source, source_cap).next()
620        };
621    };
622    let receiver_bandwidth =
623        effective_video_budget(receiver_bandwidth, tuning, route.audio_budget_reserve);
624    let budget = if uses_featured_quality || route.visible_scalable_route_count <= 1 {
625        receiver_bandwidth
626    } else {
627        let divisor = u64::try_from(route.visible_scalable_route_count)
628            .unwrap_or(u64::MAX)
629            // Bias secondary routes toward thumbnail quality before aggregate overload handling.
630            .saturating_mul(tuning.thumbnail_budget_divisor);
631        receiver_bandwidth.divided_by(divisor)
632    };
633    highest_affordable_encoding_index(route.source, budget, uses_featured_quality, source_cap)
634}
635
636fn highest_affordable_encoding_index(
637    source: &PublishedSourceDescriptor,
638    budget: Bitrate,
639    uses_featured_quality: bool,
640    source_cap: Option<Bitrate>,
641) -> Option<usize> {
642    if source
643        .selectable_encodings()
644        .all(|encoding| encoding.max_bitrate().is_none())
645    {
646        return source_cap.is_none().then_some(if uses_featured_quality {
647            source.selectable_encoding_count().saturating_sub(1)
648        } else {
649            0
650        });
651    }
652    allowed_encoding_indices(source, source_cap)
653        .rev()
654        .find_or_last(|index| {
655            source
656                .selectable_encoding_by_rank(*index)
657                .and_then(SourceEncodingDescriptor::max_bitrate)
658                .is_some_and(|bitrate| bitrate <= budget)
659        })
660}
661
662fn selector_index(source: &PublishedSourceDescriptor, selector: SourceSelector) -> usize {
663    selector
664        .selected_encoding()
665        .and_then(|encoding_id| {
666            source
667                .selectable_encodings()
668                .position(|encoding| encoding.encoding_id() == encoding_id)
669        })
670        .unwrap_or_else(|| source.selectable_encoding_count().saturating_sub(1))
671}
672
673fn allowed_encoding_indices(
674    source: &PublishedSourceDescriptor,
675    source_cap: Option<Bitrate>,
676) -> impl DoubleEndedIterator<Item = usize> + '_ {
677    (0..source.selectable_encoding_count()).filter(move |index| {
678        source_cap.is_none_or(|source_cap| {
679            source
680                .selectable_encoding_by_rank(*index)
681                .and_then(SourceEncodingDescriptor::max_bitrate)
682                .is_some_and(|bitrate| bitrate <= source_cap)
683        })
684    })
685}
686
687/// enforces receiver download limits by pausing the lowest-ranked routes using top-k selection
688///
689/// ```text
690/// all active routes (N)
691///   [ R0: rank 2, R1: rank 5, R2: rank 1, R3: rank 8, R4: rank 4 ]
692///   limit = 3  ==>  routes_to_pause (K) = 5 - 3 = 2
693///                              |
694///                              v  .k_largest_by_key(K = 2)
695///   +-------------------------------------------------------------+
696///   | min-heap of size K=2:                                       |
697///   | retains only the 2 highest ranks: [ R1: rank 5, R3: rank 8 ]|
698///   +-------------------------------------------------------------+
699///                              |
700///                              v  .rev()
701///   route.pause(VideoDownloadLimit) applied only to [ R3, R1 ]
702/// ```
703fn apply_video_download_limit(
704    routes: &mut [PlannedReceiverRoute<'_>],
705    max_video_downloads_per_receiver: usize,
706) {
707    let routes_to_pause =
708        active_route_count(routes).saturating_sub(max_video_downloads_per_receiver);
709    if routes_to_pause == 0 {
710        return;
711    }
712    for (_rank_key, route) in routes
713        .iter_mut()
714        .filter(|route| route.selection.policy_pause_reason.is_none())
715        .enumerate()
716        .map(|(position, route)| ((video_download_rank(route), position), route))
717        .k_largest_by_key(routes_to_pause, |(rank_key, _route)| *rank_key)
718        .rev()
719    {
720        route.pause(PolicyPauseReason::VideoDownloadLimit, RouteOutcome::Paused);
721    }
722}
723
724fn active_route_count(routes: &[PlannedReceiverRoute<'_>]) -> usize {
725    routes
726        .iter()
727        .filter(|route| route.selection.policy_pause_reason.is_none())
728        .count()
729}
730
731fn video_download_rank(route: &PlannedReceiverRoute<'_>) -> VideoAdmissionRank {
732    let input = route.input;
733    VideoAdmissionRank::new(
734        input.layout_role.priority(),
735        input.active_speaker_rank,
736        input.source.source_id(),
737    )
738}
739
740fn apply_overload_policy(routes: &mut [PlannedReceiverRoute<'_>], video_budget: Bitrate) {
741    let mut total_bitrate = selected_active_video_bitrate(routes);
742    if total_bitrate <= video_budget {
743        return;
744    }
745    // Drop downgradable routes that have no usable layer to fall back to.
746    let mut missing_ladders = routes
747        .iter_mut()
748        .filter(|route| {
749            route_can_downgrade(route) && cheapest_useful_selector(route.input).is_none()
750        })
751        .map(|route| (video_download_rank(route), route))
752        .collect::<Vec<_>>();
753    missing_ladders.sort_by_key(|(rank, _route)| Reverse(*rank));
754    for (_rank, route) in missing_ladders {
755        if total_bitrate <= video_budget {
756            break;
757        }
758        let selected_bitrate = route.selected_bitrate;
759        route.pause(PolicyPauseReason::MissingUsableLayer, RouteOutcome::Neutral);
760        total_bitrate = total_bitrate.saturating_sub(selected_bitrate);
761    }
762    // Step the least important downgradable route down one layer at a time.
763    // Preserve intermediate layers and stop as soon as aggregate demand fits.
764    while total_bitrate > video_budget {
765        let Some((route, selector, bitrate)) = routes
766            .iter_mut()
767            .filter(|route| route_can_downgrade(route))
768            .filter_map(|route| {
769                step_down_selector(route.input, route.selection.selector)
770                    .map(|(selector, bitrate)| (route, selector, bitrate))
771            })
772            .max_by_key(|(route, _selector, _bitrate)| {
773                (video_download_rank(route), route.selected_bitrate)
774            })
775        else {
776            break;
777        };
778        let selected_bitrate = route.selected_bitrate;
779        total_bitrate = total_bitrate
780            .saturating_sub(selected_bitrate)
781            .saturating_add(bitrate);
782        route.send(selector, bitrate, RouteOutcome::Degraded);
783    }
784    if total_bitrate <= video_budget {
785        return;
786    }
787    let mut pause_order = Vec::with_capacity(routes.len());
788    for route in routes.iter_mut() {
789        if route.selection.policy_pause_reason.is_some() {
790            continue;
791        }
792        pause_order.push((video_download_rank(route), route));
793    }
794    pause_order.sort_by_key(|(rank, _)| Reverse(*rank));
795    for (_rank, route) in pause_order {
796        if total_bitrate <= video_budget {
797            break;
798        }
799        let selected_bitrate = route.selected_bitrate;
800        let pause_reason = pause_reason_for_route(route);
801        route.pause(pause_reason, RouteOutcome::Paused);
802        total_bitrate = total_bitrate.saturating_sub(selected_bitrate);
803    }
804}
805
806fn receiver_video_budget_diagnostics(
807    routes: &[PlannedReceiverRoute<'_>],
808    receiver_bandwidth: Option<Bitrate>,
809    video_budget: Option<Bitrate>,
810) -> ReceiverVideoBudgetDiagnostics {
811    let selected_video_bitrate = selected_active_video_bitrate(routes);
812    ReceiverVideoBudgetDiagnostics::new(
813        receiver_bandwidth,
814        video_budget,
815        active_route_count(routes),
816        selected_video_bitrate,
817    )
818}
819
820fn selected_active_video_bitrate(routes: &[PlannedReceiverRoute<'_>]) -> Bitrate {
821    routes
822        .iter()
823        .filter(|route| route.selection.policy_pause_reason.is_none())
824        .fold(Bitrate::zero(), |total, route| {
825            total.saturating_add(route.selected_bitrate)
826        })
827}
828
829fn route_can_downgrade(route: &PlannedReceiverRoute<'_>) -> bool {
830    let input = route.input;
831    route.selection.policy_pause_reason.is_none()
832        && input.source.policy().adaptation() == SourceAdaptationPolicy::ScalableVideo
833        && matches!(
834            input.layout_role.priority(),
835            SourceRoutePriority::VisibleThumbnail | SourceRoutePriority::HiddenOrOverflow
836        )
837}
838
839fn pause_reason_for_route(route: &PlannedReceiverRoute<'_>) -> PolicyPauseReason {
840    let role = route.input.layout_role;
841    match role.priority() {
842        SourceRoutePriority::HiddenOrOverflow => match role {
843            SourceRoomPolicySelector::Hidden => PolicyPauseReason::HiddenTile,
844            SourceRoomPolicySelector::Overflow => PolicyPauseReason::OverflowTile,
845            _ => PolicyPauseReason::BudgetPressure,
846        },
847        _ => PolicyPauseReason::BudgetPressure,
848    }
849}
850
851fn resolve_hysteresis(
852    route: &PlannedReceiverRoute<'_>,
853    tuning: VideoAdaptationTuning,
854) -> ReceiverRouteSelection {
855    let current = route.input.current_selection;
856    let current_pause_reason = current.policy_pause_reason();
857    let selection = route.selection;
858    match (selection.policy_pause_reason, current_pause_reason) {
859        // Resume a route newly admitted under the download limit immediately
860        // because recovery hysteresis would leave an available slot unused.
861        (None, Some(PolicyPauseReason::VideoDownloadLimit)) => {
862            ReceiverRouteSelection::send(selection.selector, AdaptationCounts::reset(), true)
863        }
864        (None, Some(reason)) => {
865            let stable_limit = tuning.upswitch_stable_observations;
866            let counts = AdaptationCounts::next_upgrade(current, stable_limit);
867            if counts.upgrade >= stable_limit {
868                ReceiverRouteSelection::send(selection.selector, AdaptationCounts::reset(), true)
869            } else {
870                ReceiverRouteSelection::hold(current, Some(reason), counts)
871            }
872        }
873        (Some(reason), pause_reason) if pause_reason != Some(reason) => {
874            // Per-receiver download count and source bitrate caps are hard
875            // constraints. Holding current state would exceed the admission
876            // limit or source ceiling.
877            if matches!(
878                reason,
879                PolicyPauseReason::VideoDownloadLimit | PolicyPauseReason::SourceBitrateLimit
880            ) {
881                return ReceiverRouteSelection::pause(current, reason, AdaptationCounts::reset());
882            }
883            let pressure_limit = tuning.downswitch_pressure_observations;
884            let counts = AdaptationCounts::next_pressure(current, pressure_limit);
885            if counts.pressure >= pressure_limit {
886                ReceiverRouteSelection::pause(current, reason, AdaptationCounts::reset())
887            } else {
888                ReceiverRouteSelection::hold(current, None, counts)
889            }
890        }
891        _ => selection,
892    }
893}
894
895#[cfg(test)]
896#[path = "TESTS/solver.rs"]
897mod tests;