diff --git a/src/routes/rtc/handlers.rs b/src/routes/rtc/handlers.rs index 2c21771..ccdd276 100644 --- a/src/routes/rtc/handlers.rs +++ b/src/routes/rtc/handlers.rs @@ -18,5 +18,7 @@ pub async fn ws_handler( .get_by_id(channel_id) .await? .ok_or(HTTPError::NotFound)?; + // todo : check des permissions + // aiinsi que vérifier qu'on est sur un chan vocal OU un chan de message privé Ok(ws.on_upgrade(move |socket| ws_entrypoint_handler(socket, state, channel, user))) } diff --git a/src/rtc/README.md b/src/rtc/README.md new file mode 100644 index 0000000..2a221a5 --- /dev/null +++ b/src/rtc/README.md @@ -0,0 +1,11 @@ +### Step 1 - handshake WebSocket + +Le client doit effectuer une connexion HTTP (websocket) sur /rtc/{channel_id} +Le server vérifiera avant d'accepter la connexion si : + +- Le channel existe +- Vérifiera les permissions sur le canal (check_perm ou si c'est un canal privé) + +### Step 2 - SDP (négociation du type de media) + +### Step 3 - ICE (Négociation du flux réseaux) diff --git a/src/rtc/client.rs b/src/rtc/client.rs index 9cdea44..d085275 100644 --- a/src/rtc/client.rs +++ b/src/rtc/client.rs @@ -1,3 +1,4 @@ +use super::messages::VoiceClientMessage; use crate::models::{channel, user}; use axum::extract::ws::Message; use rustrtc::peer_connection::PeerConnection; @@ -7,8 +8,8 @@ use tokio::sync::mpsc::UnboundedSender; /// Client connecté à un canal RTC. #[derive(Clone)] pub struct RtcClient { - pub user: user::Model, - pub channel: channel::Model, + pub user: Arc, + pub channel: Arc, pub peer_connection: Arc, pub websocket_sender: UnboundedSender, } @@ -21,13 +22,33 @@ impl RtcClient { websocket_sender: UnboundedSender, ) -> Self { Self { - user, - channel, + user: Arc::new(user), + channel: Arc::new(channel), peer_connection, websocket_sender, } } + pub fn on_message(&self, raw_message: Message) { + let Message::Text(raw_message) = raw_message else { + return; + }; + + let parsed = match serde_json::from_str::(&raw_message) { + Ok(message) => message, + Err(_) => return, + }; + + match parsed { + VoiceClientMessage::SDPOffer { channel_id, sdp } => {} + VoiceClientMessage::IceCandidate { + channel_id, + candidate, + } => {} + VoiceClientMessage::Leave { channel_id } => {} + } + } + /// Ferme proprement la connexion WebRTC. pub fn close(&self) { self.peer_connection.close(); diff --git a/src/rtc/manager.rs b/src/rtc/manager.rs new file mode 100644 index 0000000..604bf25 --- /dev/null +++ b/src/rtc/manager.rs @@ -0,0 +1,36 @@ +use crate::config::NetworkConfig; +use rustrtc::{RtcConfiguration, RtcConfigurationBuilder}; + +// 1. Client crée une RTCPeerConnection +// 2. Client crée une SDP offer +// 3. Client envoie l'offer via WebSocket +// 4. Serveur crée sa PeerConnection +// 5. Serveur applique l'offer +// 6. Serveur crée une SDP answer +// 7. Serveur renvoie l'answer via WebSocket +// 8. Client et serveur échangent les candidats ICE +// 9. ICE sélectionne un chemin réseau +// 10. La connexion WebRTC devient active + +#[derive(Debug, Clone)] +struct RTCManager { + pub config: RtcConfiguration, +} + +impl RTCManager { + pub fn new(network: &NetworkConfig) -> Self { + let builder = RtcConfigurationBuilder::new() + .ice_udp_mux(true) + .ice_udp_mux_port(network.udp_port) + .bind_ip(network.host.to_string()); + let config = builder.build(); + + Self { config } + } + + /// Permet de se mettre d'accord sur les paramètres media + pub async fn handle_sdp_offer(&self) {} + + /// Interactive Connectivity Establishment - Permet de négocier un chemin réseau + pub async fn handle_ice_candidate(&self) {} +} diff --git a/src/rtc/messages.rs b/src/rtc/messages.rs new file mode 100644 index 0000000..c3512c1 --- /dev/null +++ b/src/rtc/messages.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Deserialize)] +#[serde(tag = "action", rename_all = "kebab-case")] +pub enum VoiceClientMessage { + SDPOffer { channel_id: Uuid, sdp: String }, + IceCandidate { channel_id: Uuid, candidate: String }, + Leave { channel_id: Uuid }, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "action", rename_all = "kebab-case")] +pub enum VoiceServerMessage { + Answer { channel_id: Uuid, sdp: String }, + IceCandidate { channel_id: Uuid, candidate: String }, + Error { message: String }, +} diff --git a/src/rtc/mod.rs b/src/rtc/mod.rs index 5ad232e..2590deb 100644 --- a/src/rtc/mod.rs +++ b/src/rtc/mod.rs @@ -1,4 +1,6 @@ mod client; +mod manager; +mod messages; mod metrics; pub mod ws_entrypoint; diff --git a/src/rtc/ws_entrypoint.rs b/src/rtc/ws_entrypoint.rs index 649656d..0bf4c90 100644 --- a/src/rtc/ws_entrypoint.rs +++ b/src/rtc/ws_entrypoint.rs @@ -5,8 +5,23 @@ use crate::core::AppState; use crate::models::{channel, user}; use axum::extract::ws::{Message, WebSocket}; use futures_util::{SinkExt, StreamExt}; +use rustrtc::PeerConnection; +use std::sync::Arc; use tokio::sync::mpsc; +// 1. Le frontend ouvre /rtc/{channel_id} +// 2. Axum extrait channel_id +// 3. CurrentUser identifie l'utilisateur +// 4. Le serveur vérifie le canal et les permissions +// 5. Le serveur accepte le WebSocket +// 6. Le client envoie son SDP Offer +// 7. Le serveur crée la PeerConnection +// 8. Le serveur applique l'Offer +// 9. Le serveur crée une SDP Answer +// 10. Le serveur renvoie l'Answer +// 11. Les candidats ICE sont échangés +// 12. À la déconnexion, la PeerConnection est fermée + pub async fn ws_entrypoint_handler( socket: WebSocket, state: AppState, @@ -15,7 +30,8 @@ pub async fn ws_entrypoint_handler( ) { let (mut sender, mut receiver) = socket.split(); let (tx, mut rx) = mpsc::unbounded_channel::(); - // todo : faire le peer connexion. + + let peer_connection = Arc::new(PeerConnection::new(state.rtc.config.clone())); let mut rtc_client = RtcClient::new(user, channel, peer_connection, tx); let send_task = tokio::spawn(async move {