rvoip_core/
virtual_publisher.rs1use std::fmt;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Weak};
12
13use tokio::task::JoinHandle;
14
15use crate::ids::{ConnectionId, SessionId, StreamId};
16use crate::media_graph::{ManagedMediaRoute, MediaGraphRouteStatus};
17use crate::orchestrator::Orchestrator;
18use crate::subscriptions::{PublisherRegistrationId, PublisherRegistry};
19use crate::Result;
20
21pub const DEFAULT_VIRTUAL_PUBLISHER_QUEUE_CAPACITY: usize = 10;
25
26#[derive(Clone, Eq, PartialEq)]
28pub struct VirtualPublisherDescriptor {
29 pub session_id: SessionId,
30 pub stream_id: StreamId,
31 pub participant: String,
32}
33
34impl fmt::Debug for VirtualPublisherDescriptor {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter
37 .debug_struct("VirtualPublisherDescriptor")
38 .field("session_id", &self.session_id)
39 .field("stream_id", &self.stream_id)
40 .field("participant_present", &!self.participant.is_empty())
41 .field("participant_bytes", &self.participant.len())
42 .finish()
43 }
44}
45
46impl VirtualPublisherDescriptor {
47 pub fn new(session_id: SessionId, stream_id: StreamId, participant: impl Into<String>) -> Self {
48 Self {
49 session_id,
50 stream_id,
51 participant: participant.into(),
52 }
53 }
54}
55
56struct VirtualPublisherCleanup {
57 orchestrator: Weak<Orchestrator>,
58 registry: Arc<PublisherRegistry>,
59 session_id: SessionId,
60 stream_id: StreamId,
61 publisher_id: ConnectionId,
62 registration_id: PublisherRegistrationId,
63 active: AtomicBool,
64}
65
66impl VirtualPublisherCleanup {
67 fn new(
68 orchestrator: Weak<Orchestrator>,
69 registry: Arc<PublisherRegistry>,
70 publisher_id: ConnectionId,
71 descriptor: &VirtualPublisherDescriptor,
72 registration_id: PublisherRegistrationId,
73 ) -> Self {
74 metrics::gauge!("rvoip_virtual_publishers_active").increment(1.0);
75 Self {
76 orchestrator,
77 registry,
78 session_id: descriptor.session_id.clone(),
79 stream_id: descriptor.stream_id.clone(),
80 publisher_id,
81 registration_id,
82 active: AtomicBool::new(true),
83 }
84 }
85
86 fn is_current(&self) -> bool {
87 self.active.load(Ordering::Acquire)
88 && self.registry.registration_is_current(
89 &self.session_id,
90 self.stream_id.as_str(),
91 self.registration_id,
92 )
93 }
94
95 fn unregister(&self) {
96 if !self.active.swap(false, Ordering::AcqRel) {
97 return;
98 }
99 let removed = self.registry.remove_registration(
100 &self.session_id,
101 self.stream_id.as_str(),
102 self.registration_id,
103 );
104 if removed {
105 if let Some(orchestrator) = self.orchestrator.upgrade() {
106 orchestrator.drop_publisher_stream_subscriptions(
107 &self.session_id,
108 &self.publisher_id,
109 &self.stream_id,
110 );
111 }
112 }
113 metrics::gauge!("rvoip_virtual_publishers_active").decrement(1.0);
114 }
115}
116
117struct TaskCleanup(Arc<VirtualPublisherCleanup>);
118
119impl Drop for TaskCleanup {
120 fn drop(&mut self) {
121 self.0.unregister();
122 }
123}
124
125#[must_use = "dropping the managed publisher immediately unregisters it"]
132pub struct ManagedVirtualPublisher {
133 source_connection_id: ConnectionId,
134 descriptor: VirtualPublisherDescriptor,
135 route: Option<ManagedMediaRoute>,
136 task: Option<JoinHandle<()>>,
137 cleanup: Arc<VirtualPublisherCleanup>,
138}
139
140impl ManagedVirtualPublisher {
141 pub(crate) fn start(
142 orchestrator: Weak<Orchestrator>,
143 source_connection_id: ConnectionId,
144 descriptor: VirtualPublisherDescriptor,
145 route: ManagedMediaRoute,
146 mut frames: tokio::sync::mpsc::Receiver<crate::stream::MediaFrame>,
147 registry: Arc<PublisherRegistry>,
148 registration_id: PublisherRegistrationId,
149 ) -> Self {
150 let cleanup = Arc::new(VirtualPublisherCleanup::new(
151 orchestrator.clone(),
152 registry,
153 source_connection_id.clone(),
154 &descriptor,
155 registration_id,
156 ));
157 let task_cleanup = TaskCleanup(Arc::clone(&cleanup));
158 let task_session_id = descriptor.session_id.clone();
159 let task_stream_id = descriptor.stream_id.clone();
160 let task_publisher_id = source_connection_id.clone();
161 let task = tokio::spawn(async move {
162 let cleanup = task_cleanup;
163 while let Some(mut frame) = frames.recv().await {
164 if !cleanup.0.is_current() {
169 break;
170 }
171 let Some(orchestrator) = orchestrator.upgrade() else {
172 break;
173 };
174 frame.stream_id = task_stream_id.clone();
175 let delivered = orchestrator
176 .fanout_frame(&task_session_id, &task_publisher_id, &task_stream_id, frame)
177 .await;
178 metrics::counter!("rvoip_virtual_publisher_frames_total").increment(1);
179 metrics::counter!("rvoip_virtual_publisher_deliveries_total")
180 .increment(delivered as u64);
181 }
182 });
183
184 Self {
185 source_connection_id,
186 descriptor,
187 route: Some(route),
188 task: Some(task),
189 cleanup,
190 }
191 }
192
193 pub fn source_connection_id(&self) -> &ConnectionId {
194 &self.source_connection_id
195 }
196
197 pub fn descriptor(&self) -> &VirtualPublisherDescriptor {
198 &self.descriptor
199 }
200
201 pub fn route_status(&self) -> MediaGraphRouteStatus {
202 self.route
203 .as_ref()
204 .expect("managed virtual publisher route is present until close")
205 .status()
206 }
207
208 pub async fn close(mut self) -> Result<()> {
211 if let Some(task) = self.task.take() {
212 task.abort();
213 let _ = task.await;
214 }
215 self.cleanup.unregister();
216 if let Some(route) = self.route.take() {
217 if matches!(
221 route.state(),
222 crate::media_graph::MediaGraphRouteState::Terminal(_)
223 ) {
224 drop(route);
225 } else {
226 let _ = route.remove().await?;
227 }
228 }
229 Ok(())
230 }
231}
232
233impl Drop for ManagedVirtualPublisher {
234 fn drop(&mut self) {
235 if let Some(task) = self.task.take() {
236 task.abort();
237 }
238 self.cleanup.unregister();
239 self.route.take();
240 }
241}
242
243#[cfg(test)]
244mod diagnostic_tests {
245 use super::*;
246
247 #[test]
248 fn virtual_publisher_descriptor_debug_redacts_all_identity_values() {
249 const CANARY: &str = "virtual-publisher-canary\r\nAuthorization: exposed";
250 let descriptor = VirtualPublisherDescriptor::new(
251 SessionId::from_string(CANARY),
252 StreamId::from_string(CANARY),
253 CANARY,
254 );
255 let debug = format!("{descriptor:?}");
256 assert!(!debug.contains(CANARY));
257 assert!(debug.contains("participant_present: true"));
258 }
259}