communitas-core 0.1.22

Core business logic for Communitas - PQC collaboration with virtual disks
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
// Copyright (c) 2025 Saorsa Labs Limited
//
// This file is part of the Communitas P2P collaboration platform.
//
// Licensed under the GPL-3.0 license

//! WebRTC Service Integration
//!
//! Provides a high-level WebRTC service for Communitas that integrates
//! with the gossip overlay network for signaling and peer discovery.

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};

/// Active call state
#[derive(Debug, Clone)]
pub struct CallState {
    /// Call ID
    pub call_id: CallId,
    /// Target peer identity
    pub target: CommunitasIdentity,
    /// Media constraints
    pub constraints: MediaConstraints,
    /// Is video currently enabled
    pub is_video_enabled: bool,
    /// Is audio currently enabled
    pub is_audio_enabled: bool,
    /// Is screen sharing active
    pub is_screen_sharing: bool,
}

/// Media device information
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MediaDevice {
    /// Device ID
    pub device_id: String,
    /// Human-readable label
    pub label: String,
    /// Device kind (audioinput, audiooutput, videoinput)
    pub kind: String,
}

/// WebRTC service for Communitas
///
/// Manages WebRTC calls over the gossip overlay network, providing
/// voice, video, and screen sharing capabilities.
pub struct CommunitasWebRtcService {
    /// Signaling transport
    signaling: Arc<GossipSignalingTransport>,

    /// Signaling handler with rate limiting
    signaling_handler: Arc<SignalingHandler<GossipSignalingTransport>>,

    /// Call manager from saorsa-webrtc-core
    call_manager: Arc<CallManager<CommunitasIdentity>>,

    /// Local identity
    local_identity: CommunitasIdentity,

    /// Event broadcaster
    event_tx: broadcast::Sender<CallEvent<CommunitasIdentity>>,

    /// Active calls (maps CallId to session_id for signaling)
    active_calls: Arc<RwLock<HashMap<CallId, CallState>>>,

    /// Pending incoming calls (from signaling)
    pending_incoming_calls: Arc<RwLock<HashMap<String, IncomingCallInfo>>>,
}

/// Information about an incoming call
#[derive(Debug, Clone)]
pub struct IncomingCallInfo {
    /// Session ID from signaling
    pub session_id: String,
    /// Caller identity
    pub caller: CommunitasIdentity,
    /// SDP offer
    pub sdp_offer: String,
    /// Media constraints from the offer
    pub has_video: bool,
}

impl CommunitasWebRtcService {
    /// Create a new WebRTC service
    ///
    /// # Arguments
    /// * `gossip` - The gossip context
    pub async fn new(gossip: Arc<GossipContext>) -> Result<Self> {
        info!("Initializing Communitas WebRTC service");

        // Create signaling transport
        let signaling = Arc::new(GossipSignalingTransport::new(gossip.clone())?);

        // Create signaling handler with rate limiting
        let signaling_handler = Arc::new(SignalingHandler::new(signaling.clone()));

        // Create call manager with default config
        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))?,
        );

        // Get local identity
        let local_identity = CommunitasIdentity::new(gossip.four_words.clone())?;

        // Create event broadcaster
        let (event_tx, _) = broadcast::channel(100);

        // Initialize active calls tracking
        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,
        })
    }

    /// Start the WebRTC service
    ///
    /// Subscribes to signaling messages and begins listening for incoming calls.
    pub async fn start(&self) -> Result<()> {
        info!("Starting WebRTC service for {}", self.local_identity);

        // Subscribe to signaling messages
        self.signaling.subscribe_to_signaling().await?;

        debug!("WebRTC service started successfully");

        Ok(())
    }

    /// Initiate a call to another peer
    ///
    /// # Arguments
    /// * `target_four_words` - Four-word address of the peer to call
    /// * `constraints` - Media constraints (audio, video, screen share)
    ///
    /// # Returns
    /// The call ID for the initiated call
    pub async fn initiate_call(
        &self,
        target_four_words: &str,
        constraints: MediaConstraints,
    ) -> Result<CallId> {
        info!(
            "Initiating call to {} with constraints: {:?}",
            target_four_words, constraints
        );

        // Create target identity
        let target = CommunitasIdentity::new(target_four_words.to_string())?;

        // Use CallManager to initiate the call (creates peer connection and media tracks)
        let call_id = self
            .call_manager
            .initiate_call(target.clone(), constraints.clone())
            .await
            .map_err(|e| anyhow!("Failed to initiate call: {}", e))?;

        // Generate SDP offer
        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);

        // Create session ID for signaling (using call_id as session_id)
        let session_id = call_id.to_string();

        // Send SDP offer via signaling transport
        let offer_message = SignalingMessage::Offer {
            session_id: session_id.clone(),
            sdp: sdp_offer,
            quic_endpoint: None, // QUIC endpoint discovery handled by gossip
        };

        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);

        // Create call state for tracking
        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,
        };

        // Store call state
        {
            let mut calls = self.active_calls.write().await;
            calls.insert(call_id, call_state);
        }

        debug!("Created call {} to {}", call_id, target);

        // Emit call initiated event
        let event = CallEvent::CallInitiated {
            call_id,
            callee: target,
            constraints,
        };
        let _ = self.event_tx.send(event);

        Ok(call_id)
    }

    /// Accept an incoming call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call to accept
    /// * `constraints` - Media constraints for the local side
    pub async fn accept_call(&self, call_id: CallId, constraints: MediaConstraints) -> Result<()> {
        info!("Accepting call {}", call_id);

        let session_id = call_id.to_string();

        // Check if this call exists in pending incoming calls
        let incoming_info = {
            let pending = self.pending_incoming_calls.read().await;
            pending.get(&session_id).cloned()
        };

        // If we have incoming call info, this is a real incoming call
        // Otherwise, treat it as accepting a call we initiated (for state sync)
        if let Some(info) = incoming_info {
            // Accept via CallManager (this handles WebRTC state)
            self.call_manager
                .accept_call(call_id, constraints.clone())
                .await
                .map_err(|e| anyhow!("Failed to accept call: {}", e))?;

            // Send SDP answer back to caller
            let answer_message = SignalingMessage::Answer {
                session_id: session_id.clone(),
                sdp: info.sdp_offer.clone(), // In real impl, create actual answer SDP
                quic_endpoint: None,
            };

            self.signaling_handler
                .send_message(&info.caller, answer_message)
                .await
                .map_err(|e| anyhow!("Failed to send SDP answer: {}", e))?;

            // Create and store call state
            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);
            }

            // Remove from pending
            {
                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 {
            // Just update CallManager state
            self.call_manager
                .accept_call(call_id, constraints)
                .await
                .map_err(|e| anyhow!("Failed to accept call: {}", e))?;
        }

        // Emit connection established event
        let event = CallEvent::ConnectionEstablished { call_id };
        let _ = self.event_tx.send(event);

        debug!("Call {} accepted", call_id);

        Ok(())
    }

    /// Reject an incoming call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call to reject
    pub async fn reject_call(&self, call_id: CallId) -> Result<()> {
        info!("Rejecting call {}", call_id);

        let session_id = call_id.to_string();

        // Check if this is a pending incoming call
        let incoming_info = {
            let pending = self.pending_incoming_calls.read().await;
            pending.get(&session_id).cloned()
        };

        // Send Bye signaling message to caller
        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);
            }

            // Remove from pending
            {
                let mut pending = self.pending_incoming_calls.write().await;
                pending.remove(&session_id);
            }

            info!("Sent rejection to {}", info.caller);
        }

        // Update CallManager state
        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(())
    }

    /// End an active call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call to end
    pub async fn end_call(&self, call_id: CallId) -> Result<()> {
        info!("Ending call {}", call_id);

        let session_id = call_id.to_string();

        // Get call state and target before removing
        let call_state = {
            let calls = self.active_calls.read().await;
            calls.get(&call_id).cloned()
        };

        // Send Bye signaling message to remote peer
        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);
        }

        // Remove call from active calls
        {
            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"));
            }
        }

        // End call in CallManager (closes peer connection and cleans up tracks)
        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(())
    }

    /// Enable or disable video in an active call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call
    /// * `enabled` - Whether to enable or disable video
    ///
    /// Note: Full track muting requires platform-specific implementation.
    /// This currently updates state and logs the change. The actual track
    /// control will be handled by the Swift layer using AVFoundation.
    pub async fn set_video_enabled(&self, call_id: CallId, enabled: bool) -> Result<()> {
        info!("Setting video enabled={} for call {}", enabled, call_id);

        // Update call state
        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()
        };

        // Note: Media state change events would be handled at the application layer
        // For now, state is tracked locally and UI will poll for updates

        debug!(
            "Video {} for call {} (target: {})",
            if enabled { "enabled" } else { "disabled" },
            call_id,
            target
        );

        Ok(())
    }

    /// Enable or disable audio in an active call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call
    /// * `enabled` - Whether to enable or disable audio (mute/unmute)
    ///
    /// Note: Full track muting requires platform-specific implementation.
    /// This currently updates state and logs the change. The actual track
    /// control will be handled by the Swift layer using AVFoundation.
    pub async fn set_audio_enabled(&self, call_id: CallId, enabled: bool) -> Result<()> {
        info!("Setting audio enabled={} for call {}", enabled, call_id);

        // Update call state
        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()
        };

        // Note: Media state change events would be handled at the application layer
        // For now, state is tracked locally and UI will poll for updates

        debug!(
            "Audio {} for call {} (target: {})",
            if enabled { "enabled" } else { "disabled" },
            call_id,
            target
        );

        Ok(())
    }

    /// Start screen sharing in an active call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call
    ///
    /// Note: Screen capture requires platform-specific implementation.
    /// On macOS, this will be handled by Swift using ScreenCaptureKit.
    /// On other platforms, platform-specific screen capture APIs are required.
    /// This method updates the call state; the actual screen capture track
    /// will be added by the Swift layer through the media stream manager.
    pub async fn start_screen_share(&self, call_id: CallId) -> Result<()> {
        info!("Starting screen share for call {}", call_id);

        // Update call state and get target for logging
        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()
        };

        // Note: The actual screen capture track is added by the Swift layer
        // using ScreenCaptureKit on macOS. The Rust layer tracks the state
        // and will signal the remote peer when the track is added.

        debug!(
            "Screen share started for call {} (target: {})",
            call_id, target
        );

        Ok(())
    }

    /// Stop screen sharing in an active call
    ///
    /// # Arguments
    /// * `call_id` - The ID of the call
    ///
    /// Note: Screen capture is handled by the platform layer (Swift/ScreenCaptureKit).
    /// This method updates the call state; the actual screen capture track
    /// will be removed by the Swift layer through the media stream manager.
    pub async fn stop_screen_share(&self, call_id: CallId) -> Result<()> {
        info!("Stopping screen share for call {}", call_id);

        // Update call state and get target for logging
        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()
        };

        // Note: The actual screen capture track is removed by the Swift layer.
        // The Rust layer tracks the state and will signal the remote peer
        // when the track is removed.

        debug!(
            "Screen share stopped for call {} (target: {})",
            call_id, target
        );

        Ok(())
    }

    /// Get available media devices
    ///
    /// # Returns
    /// List of available audio and video devices
    pub async fn get_media_devices(&self) -> Result<Vec<MediaDevice>> {
        info!("Getting media devices");

        // Media device enumeration is typically done on the client side
        // The backend service doesn't have access to browser media devices
        // This method exists for API consistency but returns empty list

        debug!("Media device enumeration should be done on the client side");

        Ok(Vec::new())
    }

    /// Subscribe to call events
    ///
    /// # Returns
    /// A broadcast receiver for call events
    pub fn subscribe_events(&self) -> broadcast::Receiver<CallEvent<CommunitasIdentity>> {
        self.event_tx.subscribe()
    }

    /// Get the local identity
    pub fn local_identity(&self) -> &CommunitasIdentity {
        &self.local_identity
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Note: Full integration tests would require setting up a complete gossip context
    // For now, these are placeholders

    #[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());
    }
}