use super::gossip_signaling::GossipSignalingTransport;
use super::identity::CommunitasIdentity;
use crate::gossip::GossipContext;
use anyhow::{Result, anyhow};
use saorsa_webrtc::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>,
local_identity: CommunitasIdentity,
event_tx: broadcast::Sender<CallEvent<CommunitasIdentity>>,
active_calls: Arc<RwLock<HashMap<CallId, CallState>>>,
}
impl CommunitasWebRtcService {
pub fn new(gossip: Arc<GossipContext>) -> Result<Self> {
info!("Initializing Communitas WebRTC service");
let signaling = Arc::new(GossipSignalingTransport::new(gossip.clone())?);
let local_identity = CommunitasIdentity::new(gossip.four_words.clone())?;
let (event_tx, _) = broadcast::channel(100);
let active_calls = Arc::new(RwLock::new(HashMap::new()));
Ok(Self {
signaling,
local_identity,
event_tx,
active_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 = CallId::new();
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) -> Result<()> {
info!("Accepting call {}", call_id);
debug!("Call {} accepted", call_id);
Ok(())
}
pub async fn reject_call(&self, call_id: CallId) -> Result<()> {
info!("Rejecting call {}", call_id);
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 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"));
}
}
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 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;
}
debug!(
"Video {} for call {}",
if enabled { "enabled" } else { "disabled" },
call_id
);
Ok(())
}
pub async fn set_audio_enabled(&self, call_id: CallId, enabled: bool) -> Result<()> {
info!("Setting audio enabled={} for call {}", enabled, call_id);
{
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;
}
debug!(
"Audio {} for call {}",
if enabled { "enabled" } else { "disabled" },
call_id
);
Ok(())
}
pub async fn start_screen_share(&self, call_id: CallId) -> Result<()> {
info!("Starting screen share for call {}", call_id);
{
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
call.is_screen_sharing = true;
}
debug!("Screen share started for call {}", call_id);
Ok(())
}
pub async fn stop_screen_share(&self, call_id: CallId) -> Result<()> {
info!("Stopping screen share for call {}", call_id);
{
let mut calls = self.active_calls.write().await;
let call = calls
.get_mut(&call_id)
.ok_or_else(|| anyhow!("Call not found"))?;
call.is_screen_sharing = false;
}
debug!("Screen share stopped for call {}", call_id);
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());
}
}