// client/src/handshake.rs #![forbid(unsafe_code)] use lesavka_common::lesavka::{self as pb, handshake_client::HandshakeClient}; use std::time::Duration; use tokio::time::timeout; use tonic::{Code, transport::Endpoint}; use tracing::{info, warn}; #[derive(Default, Clone, Debug)] pub struct PeerCaps { pub camera: bool, pub microphone: bool, pub camera_output: Option, pub camera_codec: Option, pub camera_width: Option, pub camera_height: Option, pub camera_fps: Option, } pub async fn negotiate(uri: &str) -> PeerCaps { info!(%uri, "🤝 dial handshake"); let ep = Endpoint::from_shared(uri.to_owned()) .expect("handshake endpoint") .tcp_nodelay(true) .http2_keep_alive_interval(Duration::from_secs(15)) .connect_timeout(Duration::from_secs(5)); let channel = timeout(Duration::from_secs(8), ep.connect()) .await .expect("handshake connect timeout") .expect("handshake connect failed"); info!("🤝 handshake channel connected"); let mut cli = HandshakeClient::new(channel); info!("🤝 fetching capabilities…"); match timeout(Duration::from_secs(5), cli.get_capabilities(pb::Empty {})).await { Ok(Ok(rsp)) => { let rsp = rsp.get_ref(); let caps = PeerCaps { camera: rsp.camera, microphone: rsp.microphone, camera_output: if rsp.camera_output.is_empty() { None } else { Some(rsp.camera_output.clone()) }, camera_codec: if rsp.camera_codec.is_empty() { None } else { Some(rsp.camera_codec.clone()) }, camera_width: if rsp.camera_width == 0 { None } else { Some(rsp.camera_width) }, camera_height: if rsp.camera_height == 0 { None } else { Some(rsp.camera_height) }, camera_fps: if rsp.camera_fps == 0 { None } else { Some(rsp.camera_fps) }, }; info!(?caps, "🤝 handshake ok"); caps } Ok(Err(e)) if e.code() == Code::Unimplemented => { warn!("🤝 handshake not implemented on server – assuming defaults"); PeerCaps::default() } Ok(Err(e)) => { warn!("🤝 handshake failed: {e} – assuming defaults"); PeerCaps::default() } Err(_) => { warn!("🤝 handshake timed out – assuming defaults"); PeerCaps::default() } } }