Skip to main content

citadel_sdk/media/
endpoint.rs

1use super::config::MediaTransportConfig;
2use super::negotiation::agree_on_udp;
3use super::receiver::MediaReceiver;
4use super::sender::MediaSender;
5use super::transport::{BoxedSink, BoxedSource, MediaTransportKind, ReliableSink};
6use crate::prelude::{CitadelClientServerConnection, PeerChannel, UdpChannel};
7use crate::remote_ext::remote_specialization::PeerRemote;
8use crate::remote_ext::results::PeerConnectSuccess;
9use citadel_io::time::{timeout, Instant};
10use citadel_io::tokio::sync::oneshot::Receiver;
11use citadel_io::ErrorCode;
12use citadel_proto::prelude::{NetworkError, OutboundUdpSender, PeerChannelRecvHalf, Ratchet};
13
14/// A media session bound to one connection. Build with
15/// [`MediaEndpoint::from_peer_connection`] or [`MediaEndpoint::from_c2s`],
16/// then [`MediaEndpoint::split`].
17pub struct MediaEndpoint {
18    kind: MediaTransportKind,
19    sender: MediaSender,
20    receiver: MediaReceiver,
21}
22
23impl std::fmt::Debug for MediaEndpoint {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("MediaEndpoint")
26            .field("kind", &self.kind)
27            .finish()
28    }
29}
30
31/// The two halves of a [`MediaEndpoint`].
32#[derive(Debug)]
33pub struct MediaEndpointParts {
34    pub kind: MediaTransportKind,
35    pub sender: MediaSender,
36    pub receiver: MediaReceiver,
37}
38
39impl MediaEndpoint {
40    /// Consumes a P2P connection's channels and returns the endpoint together
41    /// with the connection's [`PeerRemote`] (take any file-transfer handle
42    /// receiver with `get_incoming_file_transfer_handle` beforehand).
43    /// Waits `cfg.udp_wait` for UDP, then exchanges transport offers with the
44    /// peer over the reliable channel: UDP is used only if both sides hold
45    /// it, otherwise both fall back to reliable mode.
46    ///
47    /// The peer must build its endpoint too. The wait for its offer has no
48    /// timer: it lasts until the offer arrives or the channel closes
49    /// (`MediaTransportClosed`), however long the peer takes to build. A
50    /// timer here could not be made safe, because this side's offer is
51    /// already sent: a late peer would act on it while this side had
52    /// already fallen back. Bound the call yourself if the peer may never
53    /// build; dropping the future drops the channel, which the peer then
54    /// sees as closed.
55    ///
56    /// A peer whose protocol version predates transport offers
57    /// ([`citadel_proto::constants::MEDIA_TRANSPORT_OFFER_SINCE`], or unknown because the peer or the
58    /// server relaying its key exchange predates carrying the version) is
59    /// sent none and never waited on: this side uses reliable mode.
60    pub async fn from_peer_connection<R: Ratchet>(
61        conn: PeerConnectSuccess<R>,
62        cfg: MediaTransportConfig,
63    ) -> Result<(Self, PeerRemote<R>), NetworkError> {
64        let endpoint = Self::from_channels(conn.channel, conn.udp_channel_rx, cfg).await?;
65        Ok((endpoint, conn.remote))
66    }
67
68    /// Builds directly from a reliable channel and the optional pending UDP
69    /// channel receiver (as found on every connection-success type).
70    pub async fn from_channels<R: Ratchet>(
71        channel: PeerChannel<R>,
72        udp_channel_rx: Option<Receiver<UdpChannel<R>>>,
73        cfg: MediaTransportConfig,
74    ) -> Result<Self, NetworkError> {
75        Self::build(channel, udp_channel_rx, cfg).await
76    }
77
78    /// Same as [`Self::from_peer_connection`] for a client↔server connection.
79    pub async fn from_c2s<R: Ratchet>(
80        conn: &mut CitadelClientServerConnection<R>,
81        cfg: MediaTransportConfig,
82    ) -> Result<Self, NetworkError> {
83        let channel = conn.take_channel().ok_or_else(|| {
84            citadel_io::error!(
85                ErrorCode::MediaTransportClosed,
86                "reliable channel already taken"
87            )
88        })?;
89        Self::from_channels(channel, conn.udp_channel_rx.take(), cfg).await
90    }
91
92    async fn build<R: Ratchet>(
93        channel: PeerChannel<R>,
94        udp_rx: Option<Receiver<UdpChannel<R>>>,
95        cfg: MediaTransportConfig,
96    ) -> Result<Self, NetworkError> {
97        cfg.validate()?;
98        let start = Instant::now();
99        let peer_protocol_version = channel.peer_protocol_version();
100        let (reliable_tx, reliable_rx) = channel.split();
101        let mut control_sink = ReliableSink::spawn(reliable_tx);
102        let mut control_src: BoxedSource = Box::pin(reliable_rx);
103
104        let local_udp = await_udp(udp_rx, &cfg).await;
105        if let Some((udp_tx, _)) = &local_udp {
106            if cfg.udp_payload_budget > udp_tx.max_payload_len() {
107                return Err(citadel_io::error!(
108                    ErrorCode::MediaConfigInvalid,
109                    format!(
110                        "udp_payload_budget {} exceeds the UDP channel's max payload {}",
111                        cfg.udp_payload_budget,
112                        udp_tx.max_payload_len()
113                    )
114                ));
115            }
116        }
117        let peer_udp = agree_on_udp(
118            peer_protocol_version,
119            local_udp.is_some(),
120            &mut control_sink,
121            &mut control_src,
122        )
123        .await?;
124
125        match local_udp.filter(|_| peer_udp) {
126            Some((udp_tx, udp_rx)) => Self::assemble(
127                MediaTransportKind::Unreliable,
128                Box::new(udp_tx),
129                Box::new(control_sink),
130                Box::pin(udp_rx),
131                Some(control_src),
132                cfg,
133                start,
134            ),
135            None => Self::assemble(
136                MediaTransportKind::Reliable,
137                Box::new(control_sink.clone()),
138                Box::new(control_sink),
139                control_src,
140                None,
141                cfg,
142                start,
143            ),
144        }
145    }
146
147    pub(crate) fn assemble(
148        kind: MediaTransportKind,
149        media_sink: BoxedSink,
150        control_sink: BoxedSink,
151        media_src: BoxedSource,
152        control_src: Option<BoxedSource>,
153        cfg: MediaTransportConfig,
154        start: Instant,
155    ) -> Result<Self, NetworkError> {
156        Ok(Self {
157            kind,
158            sender: MediaSender::new(
159                kind,
160                media_sink,
161                control_sink,
162                cfg.media,
163                cfg.send_queue_frames,
164            )?,
165            receiver: MediaReceiver::new(kind, media_src, control_src, cfg.media, start)?,
166        })
167    }
168
169    pub fn kind(&self) -> MediaTransportKind {
170        self.kind
171    }
172
173    /// Splits into independent send/receive halves. Dropping the receiver in
174    /// unreliable mode drops the UDP receive half (⇒ `DisconnectUDP`).
175    pub fn split(self) -> (MediaSender, MediaReceiver) {
176        (self.sender, self.receiver)
177    }
178
179    pub fn into_parts(self) -> MediaEndpointParts {
180        MediaEndpointParts {
181            kind: self.kind,
182            sender: self.sender,
183            receiver: self.receiver,
184        }
185    }
186}
187
188/// Resolves the UDP channel within `cfg.udp_wait`, or `None` to fall back.
189/// A missing receiver (`UdpMode::Disabled`), a timeout, or a dropped sender
190/// all mean "no UDP" — logged, never fatal.
191async fn await_udp<R: Ratchet>(
192    udp_rx: Option<Receiver<UdpChannel<R>>>,
193    cfg: &MediaTransportConfig,
194) -> Option<(OutboundUdpSender, PeerChannelRecvHalf<R>)> {
195    let rx = udp_rx?;
196    match timeout(cfg.udp_wait, rx).await {
197        Ok(Ok(chan)) => Some(chan.split()),
198        Ok(Err(_)) => {
199            log::warn!(target: "citadel", "media: UDP channel sender dropped; using reliable transport");
200            None
201        }
202        Err(_) => {
203            log::warn!(target: "citadel", "media: UDP channel not ready within {:?}; using reliable transport", cfg.udp_wait);
204            None
205        }
206    }
207}