use crate::distributed::PeerId;
use futures_util::{SinkExt, StreamExt};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio_tungstenite::tungstenite::Message;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ClientMessage {
Join {
peer: PeerId,
#[serde(skip_serializing_if = "Option::is_none", default)]
capabilities: Option<Vec<String>>,
},
Offer { to: PeerId, sdp: String },
Answer { to: PeerId, sdp: String },
Ice { to: PeerId, candidate: String },
Relay { to: PeerId, payload: Value },
Leave,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ServerMessage {
Welcome { peer: PeerId, peers: Vec<PeerId> },
PeerJoined { peer: PeerId },
PeerLeft { peer: PeerId },
Offer { from: PeerId, sdp: String },
Answer { from: PeerId, sdp: String },
Ice { from: PeerId, candidate: String },
Relay { from: PeerId, payload: Value },
Error { code: String, message: String },
}
#[derive(Debug)]
pub enum SignalingError {
WebSocket(tokio_tungstenite::tungstenite::Error),
Protocol(serde_json::Error),
Closed,
}
impl std::fmt::Display for SignalingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SignalingError::WebSocket(e) => write!(f, "signaling websocket error: {e}"),
SignalingError::Protocol(e) => write!(f, "signaling protocol error: {e}"),
SignalingError::Closed => write!(f, "signaling connection closed"),
}
}
}
impl std::error::Error for SignalingError {}
impl From<tokio_tungstenite::tungstenite::Error> for SignalingError {
fn from(e: tokio_tungstenite::tungstenite::Error) -> Self {
SignalingError::WebSocket(e)
}
}
impl From<serde_json::Error> for SignalingError {
fn from(e: serde_json::Error) -> Self {
SignalingError::Protocol(e)
}
}
type WsStream =
tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
pub struct SignalingClient {
ws: WsStream,
peer: PeerId,
}
impl SignalingClient {
pub async fn connect(
base_url: &str,
session: &str,
peer: PeerId,
) -> Result<Self, SignalingError> {
Self::connect_with_capabilities(base_url, session, peer, None).await
}
pub async fn connect_with_capabilities(
base_url: &str,
session: &str,
peer: PeerId,
capabilities: Option<Vec<String>>,
) -> Result<Self, SignalingError> {
let url = format!("{}/session/{}", base_url.trim_end_matches('/'), session);
let (ws, _response) = tokio_tungstenite::connect_async(&url).await?;
let mut client = Self { ws, peer };
client
.send(&ClientMessage::Join { peer, capabilities })
.await?;
Ok(client)
}
pub fn peer(&self) -> PeerId {
self.peer
}
pub async fn send(&mut self, message: &ClientMessage) -> Result<(), SignalingError> {
let json = serde_json::to_string(message)?;
self.ws.send(Message::Text(json)).await?;
Ok(())
}
pub async fn offer(
&mut self,
to: PeerId,
sdp: impl Into<String>,
) -> Result<(), SignalingError> {
self.send(&ClientMessage::Offer {
to,
sdp: sdp.into(),
})
.await
}
pub async fn answer(
&mut self,
to: PeerId,
sdp: impl Into<String>,
) -> Result<(), SignalingError> {
self.send(&ClientMessage::Answer {
to,
sdp: sdp.into(),
})
.await
}
pub async fn ice(
&mut self,
to: PeerId,
candidate: impl Into<String>,
) -> Result<(), SignalingError> {
self.send(&ClientMessage::Ice {
to,
candidate: candidate.into(),
})
.await
}
pub async fn relay(&mut self, to: PeerId, payload: Value) -> Result<(), SignalingError> {
self.send(&ClientMessage::Relay { to, payload }).await
}
pub async fn leave(&mut self) -> Result<(), SignalingError> {
self.send(&ClientMessage::Leave).await?;
self.ws.close(None).await?;
Ok(())
}
pub async fn recv(&mut self) -> Option<Result<ServerMessage, SignalingError>> {
loop {
match self.ws.next().await? {
Ok(Message::Text(text)) => {
return Some(serde_json::from_str(text.as_str()).map_err(SignalingError::from));
}
Ok(Message::Binary(bytes)) => {
return Some(serde_json::from_slice(&bytes).map_err(SignalingError::from));
}
Ok(Message::Close(_)) => return None,
Ok(_) => continue,
Err(e) => return Some(Err(SignalingError::WebSocket(e))),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn client_messages_match_wire_format() {
assert_eq!(
serde_json::to_value(ClientMessage::Join {
peer: PeerId(1),
capabilities: None
})
.unwrap(),
json!({ "type": "join", "peer": 1 })
);
assert_eq!(
serde_json::to_value(ClientMessage::Join {
peer: PeerId(1),
capabilities: Some(vec!["crdt".into()]),
})
.unwrap(),
json!({ "type": "join", "peer": 1, "capabilities": ["crdt"] })
);
assert_eq!(
serde_json::to_value(ClientMessage::Offer {
to: PeerId(2),
sdp: "x".into()
})
.unwrap(),
json!({ "type": "offer", "to": 2, "sdp": "x" })
);
assert_eq!(
serde_json::to_value(ClientMessage::Ice {
to: PeerId(2),
candidate: "c".into()
})
.unwrap(),
json!({ "type": "ice", "to": 2, "candidate": "c" })
);
assert_eq!(
serde_json::to_value(ClientMessage::Relay {
to: PeerId(2),
payload: json!({"d": 1})
})
.unwrap(),
json!({ "type": "relay", "to": 2, "payload": { "d": 1 } })
);
assert_eq!(
serde_json::to_value(ClientMessage::Leave).unwrap(),
json!({ "type": "leave" })
);
}
#[test]
fn server_messages_match_wire_format() {
assert_eq!(
serde_json::from_value::<ServerMessage>(
json!({ "type": "welcome", "peer": 1, "peers": [2, 3] })
)
.unwrap(),
ServerMessage::Welcome {
peer: PeerId(1),
peers: vec![PeerId(2), PeerId(3)]
}
);
assert_eq!(
serde_json::from_value::<ServerMessage>(json!({ "type": "peer-joined", "peer": 5 }))
.unwrap(),
ServerMessage::PeerJoined { peer: PeerId(5) }
);
assert_eq!(
serde_json::from_value::<ServerMessage>(json!({ "type": "peer-left", "peer": 5 }))
.unwrap(),
ServerMessage::PeerLeft { peer: PeerId(5) }
);
assert_eq!(
serde_json::from_value::<ServerMessage>(
json!({ "type": "relay", "from": 1, "payload": [1, 2] })
)
.unwrap(),
ServerMessage::Relay {
from: PeerId(1),
payload: json!([1, 2])
}
);
assert_eq!(
serde_json::from_value::<ServerMessage>(
json!({ "type": "error", "code": "permission_denied", "message": "no" })
)
.unwrap(),
ServerMessage::Error {
code: "permission_denied".into(),
message: "no".into()
}
);
}
#[test]
fn server_message_round_trips() {
for msg in [
ServerMessage::Welcome {
peer: PeerId(1),
peers: vec![],
},
ServerMessage::Offer {
from: PeerId(2),
sdp: "s".into(),
},
ServerMessage::Answer {
from: PeerId(2),
sdp: "s".into(),
},
ServerMessage::Ice {
from: PeerId(2),
candidate: "c".into(),
},
] {
let s = serde_json::to_string(&msg).unwrap();
assert_eq!(serde_json::from_str::<ServerMessage>(&s).unwrap(), msg);
}
}
}