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}