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}