1use std::{collections::BTreeMap, mem::replace, sync::Arc};
20
21pub use crate::engine::media_transport::{
22 SessionOffer as NegotiationOffer, SessionUploadEncoding as UploadEncoding,
23 SessionUploadSlot as UploadSlot,
24};
25use crate::{
26 ConnectionId,
27 engine::{
28 AvailableFeatures, JsonPayload, PeerSnapshot, RecordingOptions, RecordingState, UserId,
29 UserInfo,
30 media_transport::{
31 MediaTransport, TransportAdapterError, TransportSessionHealth, TransportSessionKey,
32 },
33 room::{
34 BroadcastPayloadError, DeactivateIntentOutcome, JoinUserRequest, PublishIntentOutcome,
35 Room, RoomManager, RoomManagerJoinError, RoomUserOperation,
36 },
37 source_model::{
38 SourceDeactivateIntent, SourcePublishIntent, SourceSubscriptionIntent, UserStreamId,
39 },
40 },
41};
42
43#[derive(Debug, Default)]
49enum SessionPhase {
50 #[default]
52 BeforeInitialOffer,
53 Stable,
55 WaitingForAnswer(InFlightOffer),
57}
58
59#[derive(Debug)]
61struct InFlightOffer {
62 purpose: SessionOfferPurpose,
63 queued_publishes: BTreeMap<UserStreamId, SourcePublishIntent>,
64 follow_up_renegotiation: bool,
65}
66
67impl SessionPhase {
68 fn can_stage_publish(&self) -> bool {
69 !matches!(self, Self::WaitingForAnswer(_))
70 }
71
72 fn has_queued_publish(&self, stream_id: &UserStreamId) -> bool {
73 matches!(
74 self,
75 Self::WaitingForAnswer(pending)
76 if pending.queued_publishes.contains_key(stream_id)
77 )
78 }
79
80 fn queue_publish(&mut self, intent: SourcePublishIntent) {
81 if let Self::WaitingForAnswer(pending) = self {
82 let stream_id = intent.stream_id().clone();
83 pending.queued_publishes.insert(stream_id, intent);
84 }
85 }
86
87 fn remove_queued_publish(&mut self, stream_id: &UserStreamId) -> bool {
88 let Self::WaitingForAnswer(pending) = self else {
89 return false;
90 };
91 pending.queued_publishes.remove(stream_id).is_some()
92 }
93
94 fn clear_queued_publishes(&mut self) {
95 if let Self::WaitingForAnswer(pending) = self {
96 pending.queued_publishes.clear();
97 }
98 }
99
100 fn request_renegotiation(&mut self) -> bool {
101 match self {
102 Self::BeforeInitialOffer => false,
103 Self::Stable => true,
104 Self::WaitingForAnswer(pending) => {
105 pending.follow_up_renegotiation = true;
106 false
107 }
108 }
109 }
110
111 fn mark_follow_up_renegotiation(&mut self) {
112 if let Self::WaitingForAnswer(pending) = self {
113 pending.follow_up_renegotiation = true;
114 }
115 }
116
117 fn wait_for_answer(&mut self, purpose: SessionOfferPurpose) {
118 *self = Self::WaitingForAnswer(InFlightOffer {
119 purpose,
120 queued_publishes: BTreeMap::new(),
121 follow_up_renegotiation: false,
122 });
123 }
124
125 #[expect(
126 clippy::unreachable,
127 reason = "answer validates the phase before awaiting with exclusive session access"
128 )]
129 fn complete_answer(&mut self) -> InFlightOffer {
130 match replace(self, Self::Stable) {
131 Self::WaitingForAnswer(pending) => pending,
132 _ => unreachable!("answer completion requires an in-flight offer"),
133 }
134 }
135}
136
137#[derive(Debug)]
139enum SessionOfferPurpose {
140 EstablishSession,
141 RefreshSession,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
146pub enum SessionError {
147 #[error("no pending media request")]
149 NoPendingRequest,
150 #[error(transparent)]
152 Core(#[from] SfuCoreError),
153}
154
155impl SessionError {
156 #[must_use]
161 pub const fn is_client_error(self) -> bool {
162 match self {
163 Self::NoPendingRequest => true,
164 Self::Core(error) => error.is_client_error(),
165 }
166 }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
170pub enum SfuCoreError {
171 #[error("transport operation failed")]
173 Transport(#[source] TransportAdapterError),
174 #[error("capability projection failed")]
176 CapabilityProjection(#[source] TransportAdapterError),
177 #[error("session negotiation rejected")]
179 SessionNegotiationRejected,
180 #[error("session refresh rejected")]
182 SessionRefreshRejected,
183 #[error("subscription update rejected")]
185 SubscriptionUpdateRejected,
186}
187
188impl SfuCoreError {
189 #[must_use]
191 pub const fn is_client_error(self) -> bool {
192 matches!(
193 self,
194 Self::Transport(TransportAdapterError::InvalidInput)
195 | Self::CapabilityProjection(_)
196 | Self::SessionNegotiationRejected
197 | Self::SessionRefreshRejected
198 | Self::SubscriptionUpdateRejected
199 )
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct SfuCore {
206 media_transport: MediaTransport,
207 rooms: Arc<RoomManager>,
208}
209
210#[derive(Debug)]
229pub struct MediaSession {
230 core: SfuCore,
231 room: Arc<Room>,
232 transport_user_key: TransportSessionKey,
233 phase: SessionPhase,
234 closed: bool,
235}
236
237impl SfuCore {
238 #[must_use]
239 pub fn new(media_transport: MediaTransport, rooms: Arc<RoomManager>) -> Self {
240 Self {
241 media_transport,
242 rooms,
243 }
244 }
245
246 pub async fn admit_user(
266 &self,
267 room_id: &str,
268 request: JoinUserRequest,
269 ) -> Result<MediaSession, RoomManagerJoinError> {
270 let admission = self
271 .rooms
272 .join_user(room_id, request, &self.media_transport)
273 .await?;
274 Ok(MediaSession {
275 core: self.clone(),
276 room: admission.room,
277 transport_user_key: admission.transport_session_key,
278 phase: SessionPhase::default(),
279 closed: false,
280 })
281 }
282}
283
284impl MediaSession {
285 pub async fn establish(&mut self) -> Result<Option<NegotiationOffer>, SessionError> {
296 if !matches!(self.phase, SessionPhase::BeforeInitialOffer) {
297 return Ok(None);
298 }
299 let offer = self
300 .core
301 .media_transport
302 .create_initial_session_offer(self.room.uuid(), &self.transport_user_key)
303 .await
304 .map_err(SfuCoreError::Transport)?;
305 self.phase
306 .wait_for_answer(SessionOfferPurpose::EstablishSession);
307 Ok(Some(offer))
308 }
309
310 pub async fn answer(&mut self, sdp: &str) -> Result<Option<NegotiationOffer>, SessionError> {
325 if !matches!(self.phase, SessionPhase::WaitingForAnswer(_)) {
326 return Err(SessionError::NoPendingRequest);
327 }
328 let applied_answer = self
329 .core
330 .media_transport
331 .apply_session_answer(&self.transport_user_key, sdp)
332 .await
333 .map_err(SfuCoreError::Transport)?;
334 let InFlightOffer {
335 purpose,
336 queued_publishes,
337 follow_up_renegotiation,
338 } = self.phase.complete_answer();
339 match purpose {
340 SessionOfferPurpose::EstablishSession => {
341 let client_capabilities = applied_answer.client_capabilities().cloned().ok_or(
342 SfuCoreError::CapabilityProjection(TransportAdapterError::InvalidInput),
343 )?;
344 self.room_operation()
345 .apply_session_negotiated(
346 client_capabilities,
347 applied_answer.declined_consumers(),
348 )
349 .await
350 .ok_or(SfuCoreError::SessionNegotiationRejected)?;
351 }
352 SessionOfferPurpose::RefreshSession => {
353 self.room_operation()
354 .apply_session_refreshed(applied_answer.declined_consumers())
355 .await
356 .ok_or(SfuCoreError::SessionRefreshRejected)?;
357 }
358 }
359 self.room_operation()
360 .commit_staged_publishes(&applied_answer)
361 .await;
362 let staged = self.stage_queued_publishes(queued_publishes).await?;
363 if staged || follow_up_renegotiation {
364 return self.renegotiate().await;
365 }
366 Ok(None)
367 }
368
369 pub async fn publish(
381 &mut self,
382 intent: SourcePublishIntent,
383 ) -> Result<Option<NegotiationOffer>, SessionError> {
384 if self.phase.has_queued_publish(intent.stream_id()) {
385 return Ok(None);
386 }
387 match self
388 .start_publish(&intent, self.phase.can_stage_publish())
389 .await?
390 {
391 PublishIntentOutcome::Noop | PublishIntentOutcome::Activated => Ok(None),
392 PublishIntentOutcome::Queue => {
393 self.phase.queue_publish(intent);
394 Ok(None)
395 }
396 PublishIntentOutcome::Staged => self.renegotiate().await,
397 }
398 }
399
400 pub async fn deactivate_publication(&mut self, intent: SourceDeactivateIntent) {
408 if self.phase.remove_queued_publish(intent.stream_id()) {
409 return;
410 }
411 match self.room_operation().deactivate_publication(&intent).await {
412 DeactivateIntentOutcome::RolledBack => {
413 self.phase.mark_follow_up_renegotiation();
414 }
415 DeactivateIntentOutcome::Deactivated | DeactivateIntentOutcome::Noop => {}
416 }
417 }
418
419 pub async fn close(&mut self) -> bool {
428 if self.closed {
429 return false;
430 }
431 self.phase.clear_queued_publishes();
432 let did_close = self
433 .core
434 .rooms
435 .close_session(
436 self.room_id(),
437 self.user_id(),
438 self.connection_id(),
439 &self.core.media_transport,
440 )
441 .await;
442 self.closed = true;
443 did_close
444 }
445
446 pub async fn renegotiate(&mut self) -> Result<Option<NegotiationOffer>, SessionError> {
458 if !self.phase.request_renegotiation() {
459 return Ok(None);
460 }
461 let offer = match self
462 .core
463 .media_transport
464 .create_session_renegotiation_offer(&self.transport_user_key)
465 .await
466 {
467 Ok(offer) => offer,
468 Err(TransportAdapterError::UnsupportedFeature) => return Ok(None),
469 Err(error) => return Err(SfuCoreError::Transport(error).into()),
470 };
471 self.phase
472 .wait_for_answer(SessionOfferPurpose::RefreshSession);
473 Ok(Some(offer))
474 }
475
476 pub async fn subscribe(
488 &self,
489 target_user_id: &UserId,
490 intents: &BTreeMap<UserStreamId, SourceSubscriptionIntent>,
491 ) -> Result<(), SessionError> {
492 self.room_operation()
493 .apply_receiver_intent(target_user_id, intents)
494 .await
495 .ok_or(SfuCoreError::SubscriptionUpdateRejected)?;
496 Ok(())
497 }
498
499 #[must_use]
501 pub fn endpoint_health(&self) -> Option<TransportSessionHealth> {
502 self.core
503 .media_transport
504 .session_transport_health(&self.transport_user_key)
505 }
506
507 #[must_use]
508 pub fn user_id(&self) -> &UserId {
509 self.transport_user_key.user_id()
510 }
511
512 #[must_use]
513 pub const fn connection_id(&self) -> ConnectionId {
514 self.transport_user_key.connection_id()
515 }
516
517 #[must_use]
518 pub fn room_id(&self) -> &str {
519 self.room.uuid()
520 }
521
522 pub async fn is_current_connection(&self) -> bool {
523 self.room
524 .has_connection(self.user_id(), self.connection_id())
525 .await
526 }
527
528 #[must_use]
529 pub fn available_features(&self) -> AvailableFeatures {
530 self.room.available_features()
531 }
532
533 pub async fn recording_state(&self) -> RecordingState {
534 self.room.recording_state().await
535 }
536
537 pub async fn peer_snapshots(&self) -> Vec<PeerSnapshot> {
539 self.room.user_snapshots_except(self.user_id()).await
540 }
541
542 fn room_operation(&self) -> RoomUserOperation<'_> {
543 self.room.user_operation(
544 self.user_id(),
545 self.connection_id(),
546 &self.core.media_transport,
547 )
548 }
549
550 async fn start_publish(
551 &self,
552 intent: &SourcePublishIntent,
553 can_stage: bool,
554 ) -> Result<PublishIntentOutcome, SfuCoreError> {
555 self.room_operation()
556 .start_publish(intent, can_stage)
557 .await
558 .map_err(SfuCoreError::Transport)
559 }
560
561 pub async fn update_info(&self, info: UserInfo) {
567 self.room
568 .update_user_info(
569 self.user_id(),
570 self.connection_id(),
571 &self.core.media_transport,
572 info,
573 )
574 .await;
575 }
576
577 pub async fn broadcast(&self, message: JsonPayload) -> Result<(), BroadcastPayloadError> {
588 self.room
589 .broadcast(self.user_id(), self.connection_id(), message)
590 .await
591 }
592
593 #[must_use]
598 #[expect(
599 clippy::unused_async,
600 reason = "keeps the public MediaSession recording facade async while disabled recording is synchronous"
601 )]
602 pub async fn start_recording(&self, options: RecordingOptions) -> bool {
603 self.room
604 .apply_recording_start(self.user_id(), self.connection_id(), options)
605 }
606
607 #[must_use]
611 #[expect(
612 clippy::unused_async,
613 reason = "keeps the public MediaSession recording facade async while disabled recording is synchronous"
614 )]
615 pub async fn stop_recording(&self) -> bool {
616 self.room
617 .apply_recording_stop(self.user_id(), self.connection_id())
618 }
619
620 async fn stage_queued_publishes(
621 &self,
622 queued: BTreeMap<UserStreamId, SourcePublishIntent>,
623 ) -> Result<bool, SessionError> {
624 let mut staged = false;
625 for intent in queued.into_values() {
626 if self.start_publish(&intent, true).await? == PublishIntentOutcome::Staged {
627 staged = true;
628 }
629 }
630 Ok(staged)
631 }
632}