1mod build;
21mod config;
22mod policy_invalidation;
23mod route_control;
24mod rtc;
25mod teardown;
26#[cfg(any(test, feature = "testing-transport", feature = "internal-benchmarks"))]
27#[path = "TESTS/test_support/mod.rs"]
28pub mod test_support;
29mod types;
30mod workers;
31
32#[cfg(any(test, feature = "testing-transport"))]
33use std::sync::atomic::AtomicUsize;
34use std::{ptr, sync::Arc};
35
36pub use build::MediaTransportBuildError;
37pub use config::{MediaTransportConfig, MediaTransportDeps};
38use o_sfu_router::{
39 MediaKind,
40 rtp::{MediaCapabilities, MediaStream as RouterRtpParameters},
41};
42pub use policy_invalidation::{SourcePolicySignal, SourcePolicyUpdateSubscription};
43pub(crate) use route_control::{
44 ConsumerRouteControl, ConsumerRouteControlOutcome, MediaControlPlan,
45 TransportSourceActivityEffect,
46};
47pub(in crate::engine::media_transport) use route_control::{
48 ConsumerRouteControlFailure, ProducerRouteControl,
49};
50pub(crate) use teardown::TransportTeardown;
51#[cfg(feature = "internal-benchmarks")]
52pub mod benchmark_support {
53 pub use super::rtc::benchmark_support::*;
54}
55#[cfg(any(test, fuzzing))]
56pub mod fuzz_support {
57 pub use super::rtc::{
58 client_rtp_capabilities_from_answer, fuzz_support::route_packet_loop_ingress_demux,
59 };
60}
61use rtc::{
62 ParsedSessionAnswer, RtcSessionOffer, RtcWorker, RtcWorkerCommand, RtcWorkerResponse,
63 RtpProfile,
64};
65use tracing::warn;
66pub use types::{
67 ActiveSpeakerActivityReason, ActiveSpeakerActivityState, ActiveSpeakerSource,
68 ActiveSpeakerSourceDiagnostic, AppliedProducer, AppliedSessionAnswer, ConsumerActivity,
69 ProducerActivity, ReceiverBandwidthSnapshot, ReceiverBweTargetUpdate, RelayRouteActivity,
70 SessionOffer, SessionUploadEncoding, SessionUploadSlot, SourcePacketGate,
71 TransportAdapterError, TransportBitrateSnapshot, TransportConsumerRoute,
72 TransportHealthSnapshot, TransportMediaId, TransportQualitySample, TransportQualitySnapshot,
73 TransportRelayRouteAction, TransportRelayRouteEffect, TransportResult, TransportRidActivity,
74 TransportSessionHealth, TransportSessionKey, TransportSourceActivity,
75 TransportSourceDiagnosticsSnapshot, TransportSourceKey, TransportWorkerPressureSnapshot,
76};
77pub(crate) use types::{SourceActivityRevision, SourceActivityUpdate};
78
79pub(crate) use self::workers::WorkerPlacementState;
80use self::workers::signaling_to_str0m_media_kind;
81use crate::engine::{
82 media_transport::rtc::TcpAcceptor, metrics::RuntimeMetrics, source_model::SourceSyncPolicy,
83};
84
85#[derive(Debug, Clone)]
96pub struct MediaTransport {
97 workers: Arc<[RtcWorker]>,
98 profile: Arc<RtpProfile>,
99 metrics: Arc<RuntimeMetrics>,
100 #[cfg(test)]
101 media_control_batches: test_support::MediaControlBatchLog,
102 #[cfg(any(test, feature = "testing-transport"))]
103 source_diagnostics_requests: Arc<AtomicUsize>,
104 source_policy_signal: SourcePolicySignal,
106 _tcp_acceptor: Option<Arc<TcpAcceptor>>,
107}
108
109#[derive(Debug, Clone, Copy)]
110enum TransportCommandOp {
111 CreateInitialSessionOffer,
112 CreateSessionRenegotiationOffer,
113 ApplySessionAnswer,
114 PublishMedia,
115 ConsumeMedia,
116}
117
118fn warn_session_command_failed(
119 session_key: &TransportSessionKey,
120 op: TransportCommandOp,
121 error: TransportAdapterError,
122) {
123 warn!(
124 ?session_key,
125 ?op,
126 ?error,
127 "media transport worker command failed"
128 );
129}
130
131impl MediaTransport {
132 pub fn cancel(&self) {
134 for worker in self.workers.iter() {
135 worker.cancel();
136 }
137 }
138
139 pub async fn shutdown(&self) {
141 self.cancel();
142 for worker in self.workers.iter() {
143 worker.wait_for_shutdown().await;
144 }
145 }
146
147 #[must_use]
149 pub fn router_rtp_capabilities(&self) -> MediaCapabilities {
150 self.profile.router_capabilities()
151 }
152
153 async fn request_session_command<T>(
154 &self,
155 session_key: &TransportSessionKey,
156 build: impl FnOnce(RtcWorkerResponse<T>) -> RtcWorkerCommand,
157 log_error: impl FnOnce(TransportAdapterError),
158 ) -> Result<T, TransportAdapterError> {
159 let result = match self.require_worker_for_user(session_key) {
160 Ok(worker) => worker.request_worker(build).await,
161 Err(error) => Err(error),
162 };
163 if let Err(error) = &result {
164 log_error(*error);
165 }
166 result
167 }
168
169 pub async fn create_initial_session_offer(
180 &self,
181 room_id: &str,
182 session_key: &TransportSessionKey,
183 ) -> Result<SessionOffer, TransportAdapterError> {
184 let room_id: Arc<str> = Arc::from(room_id);
185 self.request_session_command(
186 session_key,
187 move |response| RtcWorkerCommand::CreateInitialSessionOffer {
188 room_id,
189 session_key: session_key.clone(),
190 response,
191 },
192 |error| {
193 warn_session_command_failed(
194 session_key,
195 TransportCommandOp::CreateInitialSessionOffer,
196 error,
197 );
198 },
199 )
200 .await
201 .map(RtcSessionOffer::into_session_offer)
202 }
203
204 pub async fn create_session_renegotiation_offer(
215 &self,
216 session_key: &TransportSessionKey,
217 ) -> Result<SessionOffer, TransportAdapterError> {
218 self.request_session_command(
219 session_key,
220 |response| RtcWorkerCommand::CreateSessionRenegotiationOffer {
221 session_key: session_key.clone(),
222 response,
223 },
224 |error| {
225 warn_session_command_failed(
226 session_key,
227 TransportCommandOp::CreateSessionRenegotiationOffer,
228 error,
229 );
230 },
231 )
232 .await
233 .map(RtcSessionOffer::into_session_offer)
234 }
235
236 pub async fn apply_session_answer(
247 &self,
248 session_key: &TransportSessionKey,
249 answer_sdp: &str,
250 ) -> Result<AppliedSessionAnswer, TransportAdapterError> {
251 async {
252 let worker = self.require_worker_for_user(session_key)?;
253 let answer = ParsedSessionAnswer::parse(answer_sdp)?;
254 worker
255 .request_worker(|response| RtcWorkerCommand::ApplySessionAnswer {
256 session_key: session_key.clone(),
257 answer,
258 response,
259 })
260 .await
261 }
262 .await
263 .inspect_err(|error| {
264 warn!(
265 ?session_key,
266 op = ?TransportCommandOp::ApplySessionAnswer,
267 answer_len = answer_sdp.len(),
268 ?error,
269 "media transport failed to apply session answer"
270 );
271 })
272 }
273
274 pub async fn publish_media(
286 &self,
287 session_key: &TransportSessionKey,
288 media_kind: MediaKind,
289 rtp_parameters: &RouterRtpParameters,
290 ) -> Result<TransportMediaId, TransportAdapterError> {
291 self.request_session_command(
292 session_key,
293 |response| RtcWorkerCommand::AddRecvMedia {
294 session_key: session_key.clone(),
295 media_kind: signaling_to_str0m_media_kind(media_kind),
296 rtp_parameters: rtp_parameters.clone(),
297 response,
298 },
299 |error| {
300 warn!(
301 ?session_key,
302 op = ?TransportCommandOp::PublishMedia,
303 ?media_kind,
304 mid = rtp_parameters.mid(),
305 ?error,
306 "media transport worker command failed"
307 );
308 },
309 )
310 .await
311 }
312
313 pub async fn consume_media(
326 &self,
327 consumer_session_key: &TransportSessionKey,
328 media_kind: MediaKind,
329 source_session_key: &TransportSessionKey,
330 source_media_id: TransportMediaId,
331 consumer_rtp_parameters: &RouterRtpParameters,
332 initial_activity: ConsumerActivity,
333 ) -> Result<TransportMediaId, TransportAdapterError> {
334 self.consume_media_with_mid(
335 consumer_session_key,
336 media_kind,
337 &TransportSourceKey::new(source_session_key.clone(), source_media_id),
338 SourceSyncPolicy::Independent,
339 consumer_rtp_parameters,
340 initial_activity,
341 )
342 .await
343 .map(|(transport_media_id, _mid)| transport_media_id)
344 }
345
346 pub(in crate::engine) async fn consume_media_with_mid(
353 &self,
354 consumer_session_key: &TransportSessionKey,
355 media_kind: MediaKind,
356 source: &TransportSourceKey,
357 sync: SourceSyncPolicy,
358 consumer_rtp_parameters: &RouterRtpParameters,
359 initial_activity: ConsumerActivity,
360 ) -> Result<(TransportMediaId, String), TransportAdapterError> {
361 let source_session_key = source.session_key();
362 let source_media_id = source.transport_media_id();
363 let result = async {
364 Self::ensure_same_room(consumer_session_key, source_session_key)?;
365 let consumer_worker = self.require_worker_for_user(consumer_session_key)?;
366 let source_worker = self.require_worker_for_user(source_session_key)?;
367 let remote_source_control = (!ptr::eq(consumer_worker, source_worker))
368 .then(|| source_worker.remote_source_control(consumer_worker));
369 consumer_worker
370 .request_worker(|response| RtcWorkerCommand::AddSendMedia {
371 consumer_key: consumer_session_key.clone(),
372 media_kind: signaling_to_str0m_media_kind(media_kind),
373 source: source.clone(),
374 sync,
375 remote_source_control,
376 consumer_rtp_parameters: consumer_rtp_parameters.clone(),
377 active: initial_activity.is_active(),
378 response,
379 })
380 .await
381 }
382 .await;
383 if let Err(error) = &result {
384 warn!(
385 ?consumer_session_key,
386 ?source_session_key,
387 ?source_media_id,
388 op = ?TransportCommandOp::ConsumeMedia,
389 ?media_kind,
390 mid = consumer_rtp_parameters.mid(),
391 initial_active = initial_activity.is_active(),
392 ?error,
393 "media transport worker command failed"
394 );
395 }
396 result
397 }
398
399 pub(crate) async fn apply_relay_route_effect(
409 &self,
410 effect: &TransportRelayRouteEffect,
411 ) -> Result<(), TransportAdapterError> {
412 self.execute_relay_route_effect(effect)
413 .await
414 .inspect_err(|error| {
415 warn!(
416 source = ?effect.source,
417 target_media_worker_id = effect.target_media_worker_id.as_usize(),
418 action = ?effect.action,
419 ?error,
420 "media transport failed to apply relay route effect"
421 );
422 })
423 }
424
425 pub(crate) async fn apply_remote_source_activity_effect(
432 &self,
433 effect: &TransportSourceActivityEffect,
434 ) -> Result<(), TransportAdapterError> {
435 self.execute_remote_source_activity_effect(effect)
436 .await
437 .inspect_err(|error| {
438 warn!(
439 source = ?effect.source,
440 target_media_worker_id = effect.target_media_worker_id.as_usize(),
441 update = ?effect.update,
442 ?error,
443 "media transport failed to apply remote source activity"
444 );
445 })
446 }
447
448 #[must_use]
451 pub fn source_policy_subscription(&self) -> SourcePolicyUpdateSubscription {
452 self.source_policy_signal.subscribe()
453 }
454}
455
456#[cfg(test)]
457#[expect(non_snake_case, reason = "test modules map to local TESTS directories")]
458mod TESTS;