Skip to main content

o_sfu_core/engine/room/source_policy/
audio.rs

1use o_sfu_router::MediaKind;
2
3use super::{
4    action::ConsumerPacketSelectionUpdate, input::SourcePolicySnapshot,
5    turn::SourcePolicyTransaction,
6};
7use crate::engine::{room::media_graph::ConsumerRouteView, source_model::PolicyPauseReason};
8
9const AUDIO_PAUSE_REASONS: [PolicyPauseReason; 2] = [
10    PolicyPauseReason::AudioSpeakerLimit,
11    PolicyPauseReason::ReceiverDeafened,
12];
13
14/// Evaluates and stages audio forwarding activity for active room audio routes.
15///
16/// The policy computes one room-wide admitted active-speaker set and applies it
17/// to every considered receiver route.
18///
19/// # Policy Invariants
20///
21/// 1. **Deafness Dominance**: If a receiver is deafened, each considered audio route is paused
22///    with [`PolicyPauseReason::ReceiverDeafened`]. When undeafened, routes are not blindly
23///    resumed; they are re-evaluated against the current active speaker quota.
24/// 2. **Low-Latency Voice Onset**: For a receiver that is not deafened, sources outside the
25///    room-wide active-source set remain unpaused by audio route policy. Worker-local VAD gating
26///    is separate and opens before destination planning on a VAD-true packet, so that packet does
27///    not wait for a policy turn.
28/// 3. **Active Speaker Admission**: When the room has more active speakers than
29///    `max_active_audio_speakers`, screen-sharers are admitted first. Sources otherwise retain
30///    room ranking by newest observation, audio level and media ID. Sources beyond the limit are
31///    paused with [`PolicyPauseReason::AudioSpeakerLimit`].
32/// 4. **Pause Reason Ownership**: Audio policy sets only reasons in [`AUDIO_PAUSE_REASONS`].
33///    It clears a pause only when the current reason belongs to that set.
34///
35/// ```text
36///                     Incoming Audio Route (Receiver, Source)
37///                                         |
38///                                         v
39///                         +-------------------------------+
40///                         |   Is Receiver Deafened?       | -- yes --> [ Pause: ReceiverDeafened ]
41///                         +-------------------------------+
42///                                         | no
43///                                         v
44///                         +-------------------------------+
45///                         |   Is Source in Room Active    | -- no  --> [ No Audio-Owned Pause ]
46///                         |   Set?                        |             (VAD gate is separate)
47///                         +-------------------------------+
48///                                         | yes
49///                                         v
50///                         +-------------------------------+
51///                         |   Within Active Speaker Cap?  | -- yes --> [ No Audio-Owned Pause ]
52///                         |  (Screen-sharers prioritized) |
53///                         +-------------------------------+
54///                                         | no
55///                                         v
56///                             [ Pause: AudioSpeakerLimit ]
57/// ```
58pub(super) fn append_audio_route_activity(
59    tx: &mut SourcePolicyTransaction,
60    input: &SourcePolicySnapshot<'_>,
61) {
62    for route in &input.routes {
63        if route.source.descriptor.media_kind() != MediaKind::Audio {
64            continue;
65        }
66        let next_reason = audio_pause_reason(input, route);
67        // Clear only pause reasons owned by audio policy. A refresh must not
68        // resume a route still withheld by another policy.
69        if next_reason.is_none() && !owns_pause_reason(route.selection.policy_pause_reason()) {
70            continue;
71        }
72        if let Some(update) = ConsumerPacketSelectionUpdate::route_activity(
73            route.key.clone(),
74            route.source.descriptor.source_id(),
75            route.route.clone(),
76            route.selection,
77            next_reason,
78        ) {
79            tx.push_route_update(update);
80        }
81    }
82}
83
84fn audio_pause_reason(
85    input: &SourcePolicySnapshot<'_>,
86    route: &ConsumerRouteView<'_>,
87) -> Option<PolicyPauseReason> {
88    // Deafness dominates speaker admission. Undeafening recomputes the cap
89    // instead of blindly resuming every audio route.
90    if input
91        .deaf_receiver_connection_ids
92        .contains(&route.route.consumer_session_key().connection_id())
93    {
94        return Some(PolicyPauseReason::ReceiverDeafened);
95    }
96    let source_media_id = route.route.source_transport_media_id();
97    let active_speaker = input.active_speaker_media_ids.contains(&source_media_id);
98    let admitted = input.admitted_audio_media_ids.contains(&source_media_id);
99    (active_speaker && !admitted).then_some(PolicyPauseReason::AudioSpeakerLimit)
100}
101
102fn owns_pause_reason(current_reason: Option<PolicyPauseReason>) -> bool {
103    current_reason.is_some_and(|reason| AUDIO_PAUSE_REASONS.contains(&reason))
104}