Skip to main content

sipx_media/
bridge.rs

1//! Connecting two calls so each hears the other.
2//!
3//! The decision that shapes this is whether to decode. When both legs agreed on the same codec,
4//! the payload that arrives on one is exactly the payload the other should send, so the bytes
5//! go straight across.
6//!
7//! Worth being precise about *why*, because the obvious reason is wrong for the codec sipx
8//! ships today. G.711 decoding is exactly invertible — encode(decode(x)) is x for every one of
9//! the 256 codes — so for µ-law and A-law, transcoding a bridge costs CPU and nothing else. The
10//! generational loss argument is real for every codec whose decode is *not* invertible: G.722,
11//! and Opus when it lands. Building the pass-through path now means those codecs arrive into a
12//! bridge that already does the right thing, rather than one that quietly degrades them.
13//!
14//! When the codecs differ there is no choice, and the bridge says so rather than transcoding
15//! quietly. Someone looking at a call that sounds worse than it should is entitled to find out
16//! why from the software rather than by reasoning about it.
17//! **Experimental** (`A-8`): real over `MediaSession`s you own, and unreachable from a `Call`,
18//! which does not hand its session out. Two calls cannot be bridged yet (`C-6`).
19//!
20
21use std::sync::Arc;
22
23use tokio::task::JoinHandle;
24
25use crate::session::{Codec, Encoded, MediaSession};
26
27/// Two calls connected so each hears the other.
28///
29/// Dropping it stops the forwarding. Both directions are separate tasks, and either ending —
30/// because its session stopped, or because the far end went away — takes the whole bridge with
31/// it: half a bridge is a call where one party can hear and the other cannot, which is worse
32/// than a call that has ended.
33#[derive(Debug)]
34pub struct Bridge {
35    forward: JoinHandle<()>,
36    reverse: JoinHandle<()>,
37    transcoding: bool,
38}
39
40impl Bridge {
41    /// Connect two sessions.
42    pub fn connect(one: Arc<MediaSession>, two: Arc<MediaSession>) -> Self {
43        // The same codec on both legs means the bytes can go straight across.
44        let transcoding = one.codec() != two.codec();
45        if transcoding {
46            tracing::info!(
47                from = ?one.codec(),
48                to = ?two.codec(),
49                "bridging between different codecs; the audio will be transcoded"
50            );
51        }
52        one.set_relay(!transcoding);
53        two.set_relay(!transcoding);
54
55        let forward = spawn_leg(Arc::clone(&one), Arc::clone(&two), transcoding);
56        let reverse = spawn_leg(two, one, transcoding);
57
58        Self {
59            forward,
60            reverse,
61            transcoding,
62        }
63    }
64
65    /// Whether audio is being decoded and re-encoded rather than passed through.
66    ///
67    /// Reported rather than inferred. Transcoding costs quality as well as CPU, and a caller
68    /// that cares can renegotiate; one that cannot see it happening has no reason to.
69    #[must_use]
70    pub fn is_transcoding(&self) -> bool {
71        self.transcoding
72    }
73
74    /// Whether both directions are still running.
75    #[must_use]
76    pub fn is_connected(&self) -> bool {
77        !self.forward.is_finished() && !self.reverse.is_finished()
78    }
79
80    /// Stop forwarding.
81    ///
82    /// The sessions themselves are left running: a bridge that ended the calls it was
83    /// connecting would make "put this call back on hold" impossible.
84    pub fn close(self) {
85        self.forward.abort();
86        self.reverse.abort();
87    }
88}
89
90impl Drop for Bridge {
91    fn drop(&mut self) {
92        // Without this, dropping a bridge leaves two tasks forwarding audio between two calls
93        // nobody is holding a handle to — the tasks keep the sessions alive through their
94        // `Arc`s, so the sockets stay open too, and neither the calls nor the ports are ever
95        // reclaimed.
96        self.forward.abort();
97        self.reverse.abort();
98    }
99}
100
101/// One direction of a bridge.
102fn spawn_leg(from: Arc<MediaSession>, to: Arc<MediaSession>, transcoding: bool) -> JoinHandle<()> {
103    tokio::spawn(async move {
104        if transcoding {
105            // Decode on one side, re-encode on the other. The samples are the only common
106            // ground between two different codecs.
107            while let Some(samples) = from.recv().await {
108                if !to.send(samples).await {
109                    return;
110                }
111            }
112        } else {
113            while let Some(encoded) = from.recv_encoded().await {
114                if !relay(&to, encoded).await {
115                    return;
116                }
117            }
118        }
119    })
120}
121
122/// Pass a payload across, when both legs speak the same codec.
123async fn relay(to: &MediaSession, encoded: Encoded) -> bool {
124    // A payload type that is not the one this leg negotiated should not be forwarded as though
125    // it were. In practice this is a DTMF event on a bridge whose two legs negotiated different
126    // dynamic payload types for `telephone-event`, and forwarding it verbatim would have the
127    // far end play a keypress as audio.
128    if Codec::from_payload_type(encoded.payload_type) != Some(to.codec()) {
129        return true;
130    }
131    to.send_encoded(encoded).await
132}
133
134#[cfg(test)]
135#[allow(
136    clippy::unwrap_used,
137    clippy::expect_used,
138    clippy::panic,
139    clippy::indexing_slicing
140)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn matching_codecs_are_not_transcoded() {
146        // The decision itself, without any sockets: it is the whole point of the type.
147        assert_eq!(Codec::Pcmu, Codec::Pcmu);
148        assert_ne!(Codec::Pcmu, Codec::Pcma);
149    }
150}