Skip to main content

o_sfu_core/engine/media_transport/
mod.rs

1//! Runtime media transport boundary used by core.
2//!
3//! This module contains the service surface that room, server and
4//! [`crate::prelude::SfuCore`] code use when media work must happen outside the
5//! pure router. Callers ask the media transport to create SDP offers, apply
6//! answers, publish or consume media, close sessions, read diagnostics
7//! snapshots and subscribe to source-policy wakeups.
8//!
9//! The boundary exposes the opaque [`MediaTransport`] handle, narrow
10//! construction inputs for the server runtime and transport operations that let
11//! higher layers express intent without knowing about RTC workers, Str0m state,
12//! UDP sockets or worker-local relay routing
13//!
14//! Code above this module should depend on [`MediaTransport`]. Code below this
15//! module, especially the RTC engine, may deal with worker-local state machines
16//! and packet-loop details. Keeping that split explicit prevents room and
17//! signaling code from growing knowledge of the concrete WebRTC
18//! implementation.
19
20mod 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/// Opaque runtime media transport handle.
86///
87/// [`MediaTransport`] is the handle server code and [`crate::prelude::SfuCore`]
88/// should hold.
89/// It hides the production RTC worker topology. Callers express intent through
90/// inherent methods and must not branch on concrete worker internals.
91///
92/// The handle also centralizes warning logs for failed transport effects. Inner
93/// backends return typed errors, while this boundary adds stable diagnostic
94/// context such as session keys, media ids and SDP lengths.
95#[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    /// Coalesces transport observations for room policy.
105    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    /// Cancels every RTC worker without waiting for termination.
133    pub fn cancel(&self) {
134        for worker in self.workers.iter() {
135            worker.cancel();
136        }
137    }
138
139    /// Cancels every RTC worker and waits for its thread to terminate.
140    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    /// returns the router capability snapshot compiled from the RTC wire profile
148    #[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    /// Creates the first SDP offer for a transport session.
170    ///
171    /// The session must already be assigned to a transport worker by its
172    /// [`TransportSessionKey`]. The returned offer is transport state that
173    /// callers should send to the browser unchanged.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`TransportAdapterError`] when the session cannot be addressed or
178    /// the active backend cannot create the offer.
179    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    /// Creates a new SDP offer after transport media state changed.
205    ///
206    /// Backends reject sessions that cannot renegotiate in their current state.
207    /// Callers should treat the returned offer as replacing any older pending
208    /// transport offer for the same session.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`TransportAdapterError`] when the session cannot be addressed or
213    /// the active backend cannot create a renegotiation offer.
214    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    /// Applies a browser SDP answer to a pending transport offer.
237    ///
238    /// The answer can reveal transport-derived producer facts such as mapped
239    /// RTP parameters. Those facts are returned so room code can commit staged
240    /// media using values observed by the transport.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`TransportAdapterError`] when the session cannot be addressed,
245    /// the answer is invalid or the active backend cannot apply it.
246    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    /// Declares a new producer on a transport session.
275    ///
276    /// `rtp_parameters` must come from router media state accepted by the core.
277    /// The returned [`TransportMediaId`] addresses the backend-local producer
278    /// for later route, activity and cleanup operations.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`TransportAdapterError`] when the session cannot be addressed,
283    /// the RTP parameters are invalid or the active backend cannot create the
284    /// producer.
285    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    /// Declares a new consumer route from a source session to a consumer session.
314    ///
315    /// The source and consumer sessions must belong to the same room instance.
316    /// Cross-worker routing is an implementation detail hidden behind this
317    /// method. The initial activity controls whether the packet loop can
318    /// forward packets before later room policy updates arrive.
319    ///
320    /// # Errors
321    ///
322    /// Returns [`TransportAdapterError`] when either session cannot be
323    /// addressed, the source media id is unknown or the active backend cannot
324    /// create the consumer.
325    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    /// Declares consumer media and returns the MID from the same worker turn.
347    ///
348    /// # Errors
349    ///
350    /// Returns [`TransportAdapterError`] when either session is unavailable,
351    /// the source is unknown or the worker cannot create the consumer.
352    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    /// Applies a room relay route mutation.
400    ///
401    /// The transport may install, release or gate the packet-loop relay target.
402    /// Room state remains the lifecycle owner.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`TransportAdapterError`] when the referenced route cannot be
407    /// addressed or the active backend cannot apply the relay mutation.
408    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    /// Applies one activity revision to a remote source.
426    ///
427    /// # Errors
428    ///
429    /// Returns [`TransportAdapterError`] when the target worker is unavailable
430    /// or rejects the update.
431    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    /// Subscribes to source-policy invalidation signals emitted by transport
449    /// workers.
450    #[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;