use super::gossip_signaling::GossipSignalingTransport;
use super::identity::CommunitasIdentity;
use crate::gossip::GossipContext;
use anyhow::{Result, anyhow};
use saorsa_webrtc_core::call::{CallManager, CallManagerConfig};
use saorsa_webrtc_core::signaling::{SignalingHandler, SignalingMessage};
use saorsa_webrtc_core::types::{CallEvent, CallId, MediaConstraints};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{RwLock, broadcast};
use tracing::{debug, info, warn};
#[derive(Debug, Clone)]
pub struct CallState {
pub call_id: CallId,
pub target: CommunitasIdentity,
pub constraints: MediaConstraints,
pub is_video_enabled: bool,
pub is_audio_enabled: bool,
pub is_screen_sharing: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MediaDevice {
pub device_id: String,
pub label: String,
pub kind: String,
}
pub struct CommunitasWebRtcService {
signaling: Arc<GossipSignalingTransport>,
signaling_handler: Arc<SignalingHandler<GossipSignalingTransport>>,
call_manager: Arc<CallManager<CommunitasIdentity>>,
local_identity: CommunitasIdentity,
event_tx: broadcast::Sender<CallEvent<CommunitasIdentity>>,
active_calls: Arc<RwLock<HashMap<CallId, CallState>>>,
pending_incoming_calls: Arc<RwLock<HashMap<String, IncomingCallInfo>>>,
}
#[derive(Debug, Clone)]
pub struct IncomingCallInfo {
pub session_id: String,
pub caller: CommunitasIdentity,
pub sdp_offer: String,
pub has_video: bool,
}
impl CommunitasWebRtcService {
pub async fn new(gossip: Arc<GossipContext>) -> Result<Self> {
info!("Initializing Communitas WebRTC service");
let signaling = Arc::new(GossipSignalingTransport::new(gossip.clone())?);
let signaling_handler = Arc::new(SignalingHandler::new(signaling.clone()));
let call_config = CallManagerConfig::default();
let call_manager = Arc::new(
CallManager::new(call_config)
.await
.map_err(|e| anyhow!("Failed to create call manager: {}", e))?,
);
let local_identity = CommunitasIdentity::new(gossip.four_words.clone())?;
let (event_tx, _) = broadcast::channel(100);
let active_calls = Arc::new(RwLock::new(HashMap::new()));
let pending_incoming_calls = Arc::new(RwLock::new(HashMap::new()));
Ok(Self {
signaling,
signaling_handler,
call_manager,
local_identity,
event_tx,
active_calls,
pending_incoming_calls,
})
}
pub async fn start(&self) -> Result<()> {
info!("Starting WebRTC service for {}", self.local_identity);
self.signaling.subscribe_to_signaling().await?;
debug!("WebRTC service started successfully");
Ok(())
}
pub async fn initiate_call(
&self,
target_four_words: &str,
constraints: MediaConstraints,
) -> Result<CallId> {
info!(
"Initiating call to {} with constraints: {:?}",
target_four_words, constraints
);
let target = CommunitasIdentity::new(target_four_words.to_string())?;
let call_id = self
.call_manager
.initiate_call(target.clone(), constraints.clone())
.await
.map_err(|e| anyhow!("Failed to initiate call: {}", e))?;
let sdp_offer = self
.call_manager
.create_offer(call_id)
.await
.map_err(|e| anyhow!("Failed to create SDP offer: {}", e))?;
debug!("Created SDP offer for call {}", call_id);
let session_id = call_id.to_string();
let offer_message = SignalingMessage::Offer {
session_id: session_id.clone(),
sdp: sdp_offer,
quic_endpoint: None, };
self.signaling_handler
.send_message(&target, offer_message)
.await
.map_err(|e| anyhow!("Failed to send SDP offer: {}", e))?;
info!("Sent SDP offer to {} for call {}", target, call_id);
let call_state = CallState {
call_id,
target: target.clone(),
constraints: constraints.clone(),
is_video_enabled: constraints.has_video(),
is_audio_enabled: constraints.has_audio(),
is_screen_sharing: false,
};
{
let mut calls = self.active_calls.write().await;
calls.insert(call_id, call_state);
}
debug!("Created call {} to {}", call_id, target);
let event = CallEvent::CallInitiated {
call_id,
callee: target,
constraints,
};
let _ = self.event_tx.send(event);
Ok(call_id)
}
pub async fn accept_call(&self, call_id: CallId, constraints: MediaConstraints) -> Result<()> {
info!("Accepting call {}", call_id);
let session_id = call_id.to_string();
let incoming_info = {
let pending = self.pending_incoming_calls.read().await;
pending.get(&session_id).cloned()
};
if let Some(info) = incoming_info {
self.call_manager
.accept_call(call_id, constraints.clone())
.await
.map_err(|e| anyhow!("Failed to accept call: {}", e))?;
let answer_message = SignalingMessage::Answer {
session_id: session_id.clone(),
sdp: info.sdp_offer.clone(), quic_endpoint: None,
};
self.signaling_handler
.send_message(&info.caller, answer_message)
.await
.map_err(|e| anyhow!("Failed to send SDP answer: {}", e))?;
let call_state = CallState {
call_id,
target: info.caller.clone(),
constraints: constraints.clone(),
is_video_enabled: constraints.has_video(),
is_audio_enabled: constraints.has_audio(),
is_screen_sharing: false,
};
{
let mut calls = self.active_calls.write().await;
calls.insert(call_id, call_state);
}
{
let mut pending = self.pending_incoming_calls.write().await;
pending.remove(&session_id);
}
info!(
"Call {} accepted, sent SDP answer to {}",
call_id, info.caller
);
} else {
self.call_manager
.accept_call(call_id, constraints)
.await
.map_err(|e| anyhow!("Failed to accept call: {}", e))?;
}
let event = CallEvent::ConnectionEstablished { call_id };
let _ = self.event_tx.send(event);
debug!("Call {} accepted", call_id);
Ok(())
}
pub async fn reject_call(&self, call_id: CallId) -> Result<()> {
info!("Rejecting call {}", call_id);
let session_id = call_id.to_string();
let incoming_info = {
let pending = self.pending_incoming_calls.read().await;
pending.get(&session_id).cloned()
};
if let Some(info) = &incoming_info {
let bye_message = SignalingMessage::Bye {
session_id: session_id.clone(),
reason: Some("rejected".to_string()),
};
if let Err(e) = self
.signaling_handler
.send_message(&info.caller, bye_message)
.await
{
warn!("Failed to send rejection signaling: {}", e);
}
{
let mut pending = self.pending_incoming_calls.write().await;
pending.remove(&session_id);
}
info!("Sent rejection to {}", info.caller);
}
if let Err(e) = self.call_manager.reject_call(call_id).await {
debug!("CallManager reject_call error (may not exist yet): {}", e);
}
debug!("Call {} rejected", call_id);
let event = CallEvent::CallRejected { call_id };
let _ = self.event_tx.send(event);
Ok(())
}
pub async fn end_call(&self, call_id: CallId) -> Result<()> {
info!("Ending call {}", call_id);
let session_id = call_id.to_string();
let call_state = {
let calls = self.active_calls.read().await;
calls.get(&call_id).cloned()
};
if let Some(state) = &call_state {
let bye_message = SignalingMessage::Bye {
session_id: session_id.clone(),
reason: Some("ended".to_string()),
};
if let Err(e) = self
.signaling_handler
.send_message(&state.target, bye_message)
.await
{
warn!("Failed to send call end signaling: {}", e);
}
info!("Sent call end to {}", state.target);
}
{
let mut calls = self.active_calls.write().await;
if calls.remove(&call_id).is_none() {
warn!("Attempted to end non-existent call {}", call_id);
return Err(anyhow!("Call not found"));
}
}
if let Err(e) = self.call_manager.end_call(call_id).await {
debug!("CallManager end_call error: {}", e);
}
debug!("Call {} ended", call_id);
let event = CallEvent::CallEnded { call_id };
let _ = self.event_tx.send(event);
Ok(())
}
pub async fn set_video_enabled(&self, call_id: CallId, enabled: bool) -> Result<()> {
info!("Setting video enabled={} for call {}", enabled, call_id);
let target = {
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
call.is_video_enabled = enabled;
call.target.clone()
};
debug!(
"Video {} for call {} (target: {})",
if enabled { "enabled" } else { "disabled" },
call_id,
target
);
Ok(())
}
pub async fn set_audio_enabled(&self, call_id: CallId, enabled: bool) -> Result<()> {
info!("Setting audio enabled={} for call {}", enabled, call_id);
let target = {
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
call.is_audio_enabled = enabled;
call.target.clone()
};
debug!(
"Audio {} for call {} (target: {})",
if enabled { "enabled" } else { "disabled" },
call_id,
target
);
Ok(())
}
pub async fn start_screen_share(&self, call_id: CallId) -> Result<()> {
info!("Starting screen share for call {}", call_id);
let target = {
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
if call.is_screen_sharing {
debug!("Screen sharing already active for call {}", call_id);
return Ok(());
}
call.is_screen_sharing = true;
call.target.clone()
};
debug!(
"Screen share started for call {} (target: {})",
call_id, target
);
Ok(())
}
pub async fn stop_screen_share(&self, call_id: CallId) -> Result<()> {
info!("Stopping screen share for call {}", call_id);
let target = {
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
if !call.is_screen_sharing {
debug!("Screen sharing not active for call {}", call_id);
return Ok(());
}
call.is_screen_sharing = false;
call.target.clone()
};
debug!(
"Screen share stopped for call {} (target: {})",
call_id, target
);
Ok(())
}
pub async fn get_media_devices(&self) -> Result<Vec<MediaDevice>> {
info!("Getting media devices");
debug!("Media device enumeration should be done on the client side");
Ok(Vec::new())
}
pub fn subscribe_events(&self) -> broadcast::Receiver<CallEvent<CommunitasIdentity>> {
self.event_tx.subscribe()
}
pub fn local_identity(&self) -> &CommunitasIdentity {
&self.local_identity
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_call_id_generation() {
let id1 = CallId::new();
let id2 = CallId::new();
assert_ne!(id1, id2);
}
#[test]
fn test_media_constraints() {
let audio = MediaConstraints::audio_only();
assert!(audio.has_audio());
assert!(!audio.has_video());
let video = MediaConstraints::video_call();
assert!(video.has_audio());
assert!(video.has_video());
}
}