Skip to main content

ruma_client_api/
rtc.rs

1//! [MatrixRTC] endpoints.
2//!
3//! [MatrixRTC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4143
4
5use std::borrow::Cow;
6
7use ruma_common::serde::JsonObject;
8use serde::{Deserialize, Deserializer, Serialize};
9
10#[cfg(feature = "unstable-msc4195")]
11pub mod livekit;
12pub mod transports;
13
14/// Information about a specific MatrixRTC transport.
15#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
16#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
17#[serde(tag = "type")]
18pub enum RtcTransport {
19    /// A LiveKit RTC transport.
20    #[cfg(feature = "unstable-msc4195")]
21    #[serde(rename = "livekit")]
22    LiveKit(LiveKitRtcTransport),
23
24    /// A custom RTC transport.
25    #[doc(hidden)]
26    #[serde(untagged)]
27    _Custom(CustomRtcTransport),
28}
29
30impl<'de> Deserialize<'de> for RtcTransport {
31    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
32    where
33        D: Deserializer<'de>,
34    {
35        use serde::de::Error;
36
37        let mut obj = JsonObject::deserialize(deserializer)?;
38        let transport_type = match obj.remove("type") {
39            Some(serde_json::Value::String(s)) => s,
40            Some(_) => return Err(D::Error::custom("`type` must be a string")),
41            None => return Err(D::Error::missing_field("type")),
42        };
43
44        Ok(match transport_type.as_str() {
45            #[cfg(feature = "unstable-msc4195")]
46            "livekit" => Self::LiveKit(
47                serde_json::from_value(serde_json::Value::Object(obj)).map_err(D::Error::custom)?,
48            ),
49            _ => Self::_Custom(CustomRtcTransport { transport_type, data: obj }),
50        })
51    }
52}
53
54impl RtcTransport {
55    /// A constructor to create a custom RTC transport.
56    ///
57    /// Prefer to use the public variants of `RtcTransport` where possible; this constructor is
58    /// meant to be used for unsupported transport types only and does not allow setting arbitrary
59    /// data for supported ones.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the `transport_type` is known and serialization of `data` to the
64    /// corresponding `RtcTransport` variant fails.
65    pub fn new(transport_type: &str, data: JsonObject) -> serde_json::Result<Self> {
66        Ok(match transport_type {
67            #[cfg(feature = "unstable-msc4195")]
68            "livekit" => Self::LiveKit(serde_json::from_value(serde_json::Value::Object(data))?),
69            _ => Self::_Custom(CustomRtcTransport {
70                transport_type: transport_type.to_owned(),
71                data,
72            }),
73        })
74    }
75
76    #[cfg(feature = "unstable-msc4195")]
77    /// Creates a new `RtcTransport::LiveKit` with the given SFU URL.
78    pub fn livekit(url: String) -> Self {
79        Self::LiveKit(LiveKitRtcTransport::new(url))
80    }
81
82    /// Returns a reference to the transport type of this RTC transport.
83    pub fn transport_type(&self) -> &str {
84        match self {
85            #[cfg(feature = "unstable-msc4195")]
86            Self::LiveKit(_) => "livekit",
87            Self::_Custom(custom) => &custom.transport_type,
88        }
89    }
90
91    /// Returns the associated data.
92    ///
93    /// The returned JSON object won't contain the `type` field, please use
94    /// [`.transport_type()`][Self::transport_type] to access that.
95    ///
96    /// Prefer to use the public variants of `RtcTransport` where possible; this method is meant
97    /// to be used for custom transport types only.
98    pub fn data(&self) -> Cow<'_, JsonObject> {
99        #[cfg(feature = "unstable-msc4195")]
100        fn serialize<T: Serialize>(object: &T) -> JsonObject {
101            use serde_json::Value as JsonValue;
102
103            match serde_json::to_value(object).expect("rtc transport type serialization to succeed")
104            {
105                JsonValue::Object(object) => object,
106                _ => panic!("all rtc transport types must serialize to objects"),
107            }
108        }
109
110        match self {
111            #[cfg(feature = "unstable-msc4195")]
112            Self::LiveKit(info) => Cow::Owned(serialize(info)),
113            Self::_Custom(info) => Cow::Borrowed(&info.data),
114        }
115    }
116}
117
118/// Information about a LiveKit RTC transport.
119#[cfg(feature = "unstable-msc4195")]
120#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
121#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
122pub struct LiveKitRtcTransport {
123    /// The WebSocket URL of the LiveKit SFU.
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub url: Option<String>,
126
127    /// The URL for the LiveKit JWT service.
128    #[serde(rename = "livekit_service_url", skip_serializing_if = "Option::is_none")]
129    #[deprecated = "Use `url` instead unless you require backwards compatibility with older versions of MSC4195."]
130    pub service_url: Option<String>,
131}
132
133#[cfg(feature = "unstable-msc4195")]
134impl LiveKitRtcTransport {
135    /// Creates a new `LiveKitRtcTransport` with the given SFU URL.
136    #[allow(deprecated)]
137    pub fn new(url: String) -> Self {
138        Self { url: Some(url), service_url: None }
139    }
140
141    /// Creates a new `LiveKitRtcTransport` with the given JWT service URL.
142    #[deprecated = "Use `LiveKitRtcTransport::new()` instead unless you require backwards compatibility with older versions of MSC4195."]
143    #[allow(deprecated)]
144    pub fn with_service_url(service_url: String) -> Self {
145        Self { url: None, service_url: Some(service_url) }
146    }
147}
148
149#[cfg(feature = "unstable-msc4195")]
150impl From<LiveKitRtcTransport> for RtcTransport {
151    fn from(value: LiveKitRtcTransport) -> Self {
152        Self::LiveKit(value)
153    }
154}
155
156/// Information about a custom RTC transport.
157///
158/// This type does not implement `Deserialize` to prevent users from
159/// constructing the `_Custom` variant of [`RtcTransport`] for a known `type`.
160/// Deserialize through [`RtcTransport`] instead.
161#[doc(hidden)]
162#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
163pub struct CustomRtcTransport {
164    /// The type of RTC transport.
165    #[serde(rename = "type")]
166    transport_type: String,
167
168    /// Remaining RTC transport data.
169    #[serde(flatten)]
170    data: JsonObject,
171}
172
173#[cfg(test)]
174mod tests {
175    use serde_json::{
176        Value as JsonValue, from_value as from_json_value, json, to_value as to_json_value,
177    };
178    use strass::assert_let;
179
180    use super::{LiveKitRtcTransport, RtcTransport};
181
182    #[test]
183    fn serialize_roundtrip_custom_rtc_transport() {
184        let transport_type = "local.custom.transport";
185        assert_let!(
186            JsonValue::Object(transport_data) = json!({
187                "foo": "bar",
188                "baz": true,
189            })
190        );
191        let transport = RtcTransport::new(transport_type, transport_data.clone()).unwrap();
192        let json = json!({
193            "type": transport_type,
194            "foo": "bar",
195            "baz": true,
196        });
197
198        assert_eq!(transport.transport_type(), transport_type);
199        assert_eq!(*transport.data().as_ref(), transport_data);
200        assert_eq!(to_json_value(&transport).unwrap(), json);
201        assert_eq!(from_json_value::<RtcTransport>(json).unwrap(), transport);
202    }
203
204    #[cfg(feature = "unstable-msc4195")]
205    #[test]
206    fn livekit_transport_new_and_from_impl() {
207        use super::LiveKitRtcTransport;
208
209        let url = "wss://livekit.local/".to_owned();
210        let inner = LiveKitRtcTransport::new(url.clone());
211        let transport = RtcTransport::from(inner);
212        assert_eq!(transport.transport_type(), "livekit");
213        assert_eq!(transport, RtcTransport::livekit(url));
214    }
215
216    #[cfg(feature = "unstable-msc4195")]
217    #[test]
218    fn serialize_roundtrip_livekit_sfu_transport() {
219        let transport_type = "livekit";
220        let url = "wss://livekit.local/";
221        let transport = RtcTransport::livekit(url.to_owned());
222        let json = json!({
223            "type": transport_type,
224            "url": url,
225        });
226
227        assert_eq!(transport.transport_type(), transport_type);
228        assert_eq!(to_json_value(&transport).unwrap(), json);
229        assert_eq!(from_json_value::<RtcTransport>(json).unwrap(), transport);
230    }
231
232    #[cfg(feature = "unstable-msc4195")]
233    #[test]
234    #[allow(deprecated)]
235    fn serialize_roundtrip_legacy_livekit_sfu_transport() {
236        let livekit_service_url = "https://livekit-jwt.local/";
237        let json = json!({
238            "type": "livekit",
239            "livekit_service_url": livekit_service_url,
240        });
241
242        assert_let!(Ok(RtcTransport::LiveKit(livekit)) = from_json_value(json.clone()));
243        assert_eq!(livekit.url, None);
244        assert_eq!(livekit.service_url.as_deref(), Some(livekit_service_url));
245        assert_eq!(livekit, LiveKitRtcTransport::with_service_url(livekit_service_url.to_owned()));
246        assert_eq!(to_json_value(RtcTransport::LiveKit(livekit)).unwrap(), json);
247    }
248
249    #[cfg(feature = "unstable-msc4195")]
250    #[test]
251    #[allow(deprecated)]
252    fn serialize_roundtrip_livekit_sfu_transport_with_legacy_field() {
253        let url = "wss://livekit.local/";
254        let livekit_service_url = "https://livekit-jwt.local/";
255        let json = json!({
256            "type": "livekit",
257            "url": url,
258            "livekit_service_url": livekit_service_url,
259        });
260
261        assert_let!(Ok(RtcTransport::LiveKit(livekit)) = from_json_value(json.clone()));
262        assert_eq!(livekit.url.as_deref(), Some(url));
263        assert_eq!(livekit.service_url.as_deref(), Some(livekit_service_url));
264        assert_eq!(to_json_value(RtcTransport::LiveKit(livekit)).unwrap(), json);
265    }
266}