rvoip_client_core/client/events.rs
1//! Event handling for the client-core library
2//!
3//! This module contains the event handler that bridges session-core events
4//! to client-core events, providing a clean abstraction for applications.
5
6use std::sync::Arc;
7use std::collections::HashMap;
8use tokio::sync::RwLock;
9use dashmap::DashMap;
10use chrono::Utc;
11
12// Import session-core types
13use rvoip_session_core::{
14 api::{
15 types::{SessionId, CallSession, CallState, IncomingCall, CallDecision},
16 handlers::CallHandler,
17 },
18};
19
20// Import client-core types
21use crate::{
22 call::{CallId, CallInfo, CallDirection},
23 events::{ClientEventHandler, IncomingCallInfo, CallStatusInfo},
24};
25
26// All types are re-exported from the main events module
27
28/// Internal call handler that bridges session-core events to client-core events
29///
30/// This handler receives events from the session-core layer and translates them
31/// into client-core events that applications can consume. It manages mappings
32/// between session IDs and call IDs, tracks call state, and forwards events
33/// to registered event handlers.
34///
35/// # Architecture
36///
37/// The handler maintains several mappings:
38/// - Session ID ↔ Call ID mapping for event translation
39/// - Call information storage with extended metadata
40/// - Incoming call storage for deferred acceptance/rejection
41/// - Event broadcasting through multiple channels
42///
43/// # Examples
44///
45/// ```rust
46/// use rvoip_client_core::client::events::ClientCallHandler;
47/// use rvoip_session_core::CallHandler;
48/// use std::sync::Arc;
49/// use dashmap::DashMap;
50///
51/// let handler = ClientCallHandler::new(
52/// Arc::new(DashMap::new()), // call_mapping
53/// Arc::new(DashMap::new()), // session_mapping
54/// Arc::new(DashMap::new()), // call_info
55/// Arc::new(DashMap::new()), // incoming_calls
56/// );
57/// ```
58pub struct ClientCallHandler {
59 /// Client event handler for forwarding processed events to applications
60 ///
61 /// This optional handler receives high-level client events after they have been
62 /// processed and enriched by this bridge. Applications can register handlers
63 /// to receive notifications about incoming calls, state changes, etc.
64 pub client_event_handler: Arc<RwLock<Option<Arc<dyn ClientEventHandler>>>>,
65
66 /// Mapping from session-core SessionId to client-core CallId
67 ///
68 /// This bidirectional mapping allows the handler to translate between
69 /// session-core's internal session identifiers and the client-facing call IDs
70 /// that applications use to reference calls.
71 pub call_mapping: Arc<DashMap<SessionId, CallId>>,
72
73 /// Reverse mapping from client-core CallId to session-core SessionId
74 ///
75 /// Provides efficient lookup in the opposite direction from call_mapping,
76 /// allowing quick translation from client call IDs to session IDs when
77 /// making session-core API calls.
78 pub session_mapping: Arc<DashMap<CallId, SessionId>>,
79
80 /// Enhanced call information storage with extended metadata
81 ///
82 /// Stores comprehensive call information including state, timing data,
83 /// participant details, and custom metadata. This information persists
84 /// throughout the call lifecycle and can be used for history and reporting.
85 pub call_info: Arc<DashMap<CallId, CallInfo>>,
86
87 /// Storage for incoming calls awaiting acceptance or rejection
88 ///
89 /// When incoming calls arrive, they are stored here until the application
90 /// decides whether to accept or reject them. This allows for deferred
91 /// call handling and provides access to full call details for decision making.
92 pub incoming_calls: Arc<DashMap<CallId, IncomingCall>>,
93
94 /// Optional broadcast channel for real-time event streaming
95 ///
96 /// If configured, events are broadcast through this channel in addition to
97 /// being sent to the registered event handler. This allows multiple consumers
98 /// to receive events independently.
99 pub event_tx: Option<tokio::sync::broadcast::Sender<crate::events::ClientEvent>>,
100
101 /// Channel for notifying when calls become established
102 ///
103 /// This channel is used to notify ClientManager when a call transitions to
104 /// the Connected state, allowing it to set up audio frame subscription.
105 pub(crate) call_established_tx: Option<tokio::sync::mpsc::UnboundedSender<CallId>>,
106}
107
108impl std::fmt::Debug for ClientCallHandler {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 f.debug_struct("ClientCallHandler")
111 .field("client_event_handler", &"<event handler>")
112 .field("call_mapping", &self.call_mapping)
113 .field("session_mapping", &self.session_mapping)
114 .field("call_info", &self.call_info)
115 .field("incoming_calls", &self.incoming_calls)
116 .finish()
117 }
118}
119
120impl ClientCallHandler {
121 /// Create a new ClientCallHandler with required mappings and storage
122 ///
123 /// This constructor initializes the handler with the necessary data structures
124 /// for managing call state and event translation between session-core and client-core.
125 ///
126 /// # Arguments
127 ///
128 /// * `call_mapping` - Bidirectional mapping between session IDs and call IDs
129 /// * `session_mapping` - Reverse mapping for efficient lookups
130 /// * `call_info` - Storage for comprehensive call information and metadata
131 /// * `incoming_calls` - Storage for pending incoming calls
132 ///
133 /// # Examples
134 ///
135 /// ```rust
136 /// use rvoip_client_core::client::events::ClientCallHandler;
137 /// use std::sync::Arc;
138 /// use dashmap::DashMap;
139 ///
140 /// let handler = ClientCallHandler::new(
141 /// Arc::new(DashMap::new()),
142 /// Arc::new(DashMap::new()),
143 /// Arc::new(DashMap::new()),
144 /// Arc::new(DashMap::new()),
145 /// );
146 /// ```
147 pub fn new(
148 call_mapping: Arc<DashMap<SessionId, CallId>>,
149 session_mapping: Arc<DashMap<CallId, SessionId>>,
150 call_info: Arc<DashMap<CallId, CallInfo>>,
151 incoming_calls: Arc<DashMap<CallId, IncomingCall>>,
152 ) -> Self {
153 Self {
154 client_event_handler: Arc::new(RwLock::new(None)),
155 call_mapping,
156 session_mapping,
157 call_info,
158 incoming_calls,
159 event_tx: None,
160 call_established_tx: None,
161 }
162 }
163
164 /// Configure the handler with an event broadcast channel
165 ///
166 /// This method adds broadcast capability to the handler, allowing events
167 /// to be sent to multiple consumers through a tokio broadcast channel.
168 /// Events will be sent to both the registered event handler and the broadcast channel.
169 ///
170 /// # Arguments
171 ///
172 /// * `event_tx` - Broadcast sender for streaming events to multiple consumers
173 ///
174 /// # Examples
175 ///
176 /// ```rust
177 /// use rvoip_client_core::client::events::ClientCallHandler;
178 /// use std::sync::Arc;
179 /// use dashmap::DashMap;
180 ///
181 /// let (tx, _rx) = tokio::sync::broadcast::channel(100);
182 /// let handler = ClientCallHandler::new(
183 /// Arc::new(DashMap::new()),
184 /// Arc::new(DashMap::new()),
185 /// Arc::new(DashMap::new()),
186 /// Arc::new(DashMap::new()),
187 /// ).with_event_tx(tx);
188 /// ```
189 pub fn with_event_tx(mut self, event_tx: tokio::sync::broadcast::Sender<crate::events::ClientEvent>) -> Self {
190 self.event_tx = Some(event_tx);
191 self
192 }
193
194 /// Configure the handler with a call established notification channel
195 ///
196 /// This internal method is used by ClientManager to provide a channel
197 /// for receiving notifications when calls transition to the Connected state.
198 pub(crate) fn with_call_established_tx(mut self, tx: tokio::sync::mpsc::UnboundedSender<CallId>) -> Self {
199 self.call_established_tx = Some(tx);
200 self
201 }
202
203 /// Register an event handler to receive processed client events
204 ///
205 /// This method sets the application-level event handler that will receive
206 /// high-level client events after they have been processed and enriched by this bridge.
207 /// The handler will be called for incoming calls, state changes, media events, etc.
208 ///
209 /// # Arguments
210 ///
211 /// * `handler` - The event handler implementation to register
212 ///
213 /// # Examples
214 ///
215 /// ```rust
216 /// # use rvoip_client_core::client::events::ClientCallHandler;
217 /// # use rvoip_client_core::events::ClientEventHandler;
218 /// # use std::sync::Arc;
219 /// # use dashmap::DashMap;
220 /// # struct MyEventHandler;
221 /// # #[async_trait::async_trait]
222 /// # impl ClientEventHandler for MyEventHandler {
223 /// # async fn on_incoming_call(&self, _info: rvoip_client_core::events::IncomingCallInfo) -> rvoip_client_core::events::CallAction {
224 /// # rvoip_client_core::events::CallAction::Accept
225 /// # }
226 /// # async fn on_call_state_changed(&self, _info: rvoip_client_core::events::CallStatusInfo) {}
227 /// # async fn on_media_event(&self, _info: rvoip_client_core::events::MediaEventInfo) {}
228 /// # async fn on_registration_status_changed(&self, _info: rvoip_client_core::events::RegistrationStatusInfo) {}
229 /// # }
230 /// # #[tokio::main]
231 /// # async fn main() {
232 /// let handler = ClientCallHandler::new(
233 /// Arc::new(DashMap::new()),
234 /// Arc::new(DashMap::new()),
235 /// Arc::new(DashMap::new()),
236 /// Arc::new(DashMap::new()),
237 /// );
238 ///
239 /// let event_handler = Arc::new(MyEventHandler);
240 /// handler.set_event_handler(event_handler).await;
241 /// # }
242 /// ```
243 pub async fn set_event_handler(&self, handler: Arc<dyn ClientEventHandler>) {
244 *self.client_event_handler.write().await = Some(handler);
245 }
246
247 /// Store an IncomingCall object for later use
248 ///
249 /// This method stores an incoming call in the handler's storage, allowing it to be
250 /// retrieved later when the application decides to accept or reject the call.
251 /// This enables deferred call handling where the application can examine call
252 /// details before making a decision.
253 ///
254 /// # Arguments
255 ///
256 /// * `call_id` - The client-core call ID for this incoming call
257 /// * `incoming_call` - The session-core IncomingCall object with full details
258 ///
259 /// # Examples
260 ///
261 /// ```rust
262 /// # use rvoip_client_core::client::events::ClientCallHandler;
263 /// # use rvoip_client_core::call::CallId;
264 /// # use rvoip_session_core::api::types::IncomingCall;
265 /// # use std::sync::Arc;
266 /// # use dashmap::DashMap;
267 /// # #[tokio::main]
268 /// # async fn main() {
269 /// let handler = ClientCallHandler::new(
270 /// Arc::new(DashMap::new()),
271 /// Arc::new(DashMap::new()),
272 /// Arc::new(DashMap::new()),
273 /// Arc::new(DashMap::new()),
274 /// );
275 ///
276 /// let call_id = CallId::new_v4();
277 /// # let incoming_call = IncomingCall {
278 /// # id: rvoip_session_core::api::types::SessionId("test".to_string()),
279 /// # from: "sip:caller@example.com".to_string(),
280 /// # to: "sip:callee@example.com".to_string(),
281 /// # sdp: None,
282 /// # headers: std::collections::HashMap::new(),
283 /// # received_at: std::time::Instant::now(),
284 /// # };
285 /// handler.store_incoming_call(call_id, incoming_call).await;
286 /// # }
287 /// ```
288 pub async fn store_incoming_call(&self, call_id: CallId, incoming_call: IncomingCall) {
289 self.incoming_calls.insert(call_id, incoming_call);
290 }
291
292 /// Retrieve a stored IncomingCall object
293 ///
294 /// This method retrieves a previously stored incoming call by its call ID.
295 /// This is useful when the application needs to access the full incoming call
296 /// details (SDP, headers, etc.) when making accept/reject decisions or during
297 /// call processing.
298 ///
299 /// # Arguments
300 ///
301 /// * `call_id` - The client-core call ID to retrieve the incoming call for
302 ///
303 /// # Returns
304 ///
305 /// `Some(IncomingCall)` if a stored incoming call exists for the given call ID,
306 /// `None` if no incoming call is found or if the call has already been processed.
307 ///
308 /// # Examples
309 ///
310 /// ```rust
311 /// # use rvoip_client_core::client::events::ClientCallHandler;
312 /// # use rvoip_client_core::call::CallId;
313 /// # use std::sync::Arc;
314 /// # use dashmap::DashMap;
315 /// # #[tokio::main]
316 /// # async fn main() {
317 /// let handler = ClientCallHandler::new(
318 /// Arc::new(DashMap::new()),
319 /// Arc::new(DashMap::new()),
320 /// Arc::new(DashMap::new()),
321 /// Arc::new(DashMap::new()),
322 /// );
323 ///
324 /// let call_id = CallId::new_v4();
325 ///
326 /// // Retrieve incoming call details
327 /// if let Some(incoming_call) = handler.get_incoming_call(&call_id).await {
328 /// println!("Found incoming call from: {}", incoming_call.from);
329 /// println!("SDP offer present: {}", incoming_call.sdp.is_some());
330 /// } else {
331 /// println!("No incoming call found for ID: {}", call_id);
332 /// }
333 /// # }
334 /// ```
335 pub async fn get_incoming_call(&self, call_id: &CallId) -> Option<IncomingCall> {
336 self.incoming_calls.get(call_id).map(|entry| entry.value().clone())
337 }
338
339 /// Extract display name from SIP URI or headers
340 ///
341 /// This method attempts to extract a human-readable display name from a SIP URI
342 /// or associated headers. It implements multiple extraction strategies to handle
343 /// various SIP message formats and header configurations.
344 ///
345 /// # Arguments
346 ///
347 /// * `uri` - The SIP URI to extract display name from (e.g., "Alice Smith" <sip:alice@example.com>)
348 /// * `headers` - SIP message headers that may contain display name information
349 ///
350 /// # Returns
351 ///
352 /// `Some(String)` containing the extracted display name if found, `None` if no
353 /// display name could be extracted from the URI or headers.
354 ///
355 /// # Extraction Strategy
356 ///
357 /// 1. Check for quoted display name in URI: `"Display Name" <sip:user@domain>`
358 /// 2. Check for unquoted display name before angle brackets: `Display Name <sip:user@domain>`
359 /// 3. Check From header for display name using the same strategies
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// # use rvoip_client_core::client::events::ClientCallHandler;
365 /// # use std::sync::Arc;
366 /// # use std::collections::HashMap;
367 /// # use dashmap::DashMap;
368 /// let handler = ClientCallHandler::new(
369 /// Arc::new(DashMap::new()),
370 /// Arc::new(DashMap::new()),
371 /// Arc::new(DashMap::new()),
372 /// Arc::new(DashMap::new()),
373 /// );
374 ///
375 /// let mut headers = HashMap::new();
376 /// headers.insert("From".to_string(), "\"Alice Smith\" <sip:alice@example.com>".to_string());
377 ///
378 /// // Extract from quoted URI
379 /// let display_name = handler.extract_display_name(
380 /// "\"Alice Smith\" <sip:alice@example.com>",
381 /// &headers
382 /// );
383 /// assert_eq!(display_name, Some("Alice Smith".to_string()));
384 ///
385 /// // Extract from unquoted URI
386 /// let display_name = handler.extract_display_name(
387 /// "Bob Jones <sip:bob@example.com>",
388 /// &HashMap::new()
389 /// );
390 /// assert_eq!(display_name, Some("Bob Jones".to_string()));
391 ///
392 /// // No display name available
393 /// let display_name = handler.extract_display_name(
394 /// "sip:carol@example.com",
395 /// &HashMap::new()
396 /// );
397 /// assert_eq!(display_name, None);
398 /// ```
399 pub fn extract_display_name(&self, uri: &str, headers: &HashMap<String, String>) -> Option<String> {
400 // First try to extract from URI (e.g., "Display Name" <sip:user@domain>)
401 if let Some(start) = uri.find('"') {
402 if let Some(end) = uri[start + 1..].find('"') {
403 let display_name = &uri[start + 1..start + 1 + end];
404 if !display_name.is_empty() {
405 return Some(display_name.to_string());
406 }
407 }
408 }
409
410 // Try display name before < in URI
411 if let Some(angle_pos) = uri.find('<') {
412 let potential_name = uri[..angle_pos].trim();
413 if !potential_name.is_empty() && !potential_name.starts_with("sip:") {
414 return Some(potential_name.to_string());
415 }
416 }
417
418 // Try From header display name
419 if let Some(from_header) = headers.get("From") {
420 return self.extract_display_name_from_header(from_header);
421 }
422
423 None
424 }
425
426 /// Extract display name from a SIP header string
427 ///
428 /// This method extracts a human-readable display name from a SIP header value,
429 /// typically the From or To header. It handles both quoted and unquoted display
430 /// name formats commonly found in SIP messages.
431 ///
432 /// # Arguments
433 ///
434 /// * `header` - The SIP header value to parse for display name information
435 ///
436 /// # Returns
437 ///
438 /// `Some(String)` containing the extracted display name if found, `None` if no
439 /// display name could be extracted from the header.
440 ///
441 /// # Supported Formats
442 ///
443 /// - Quoted display name: `"Alice Smith" <sip:alice@example.com>`
444 /// - Unquoted display name: `Alice Smith <sip:alice@example.com>`
445 /// - Plain SIP URI: `sip:alice@example.com` (returns None)
446 ///
447 /// # Examples
448 ///
449 /// ```rust
450 /// # use rvoip_client_core::client::events::ClientCallHandler;
451 /// # use std::sync::Arc;
452 /// # use dashmap::DashMap;
453 /// let handler = ClientCallHandler::new(
454 /// Arc::new(DashMap::new()),
455 /// Arc::new(DashMap::new()),
456 /// Arc::new(DashMap::new()),
457 /// Arc::new(DashMap::new()),
458 /// );
459 ///
460 /// // Extract from quoted header
461 /// let name = handler.extract_display_name_from_header(
462 /// "\"Alice Smith\" <sip:alice@example.com>"
463 /// );
464 /// assert_eq!(name, Some("Alice Smith".to_string()));
465 ///
466 /// // Extract from unquoted header
467 /// let name = handler.extract_display_name_from_header(
468 /// "Bob Jones <sip:bob@example.com>"
469 /// );
470 /// assert_eq!(name, Some("Bob Jones".to_string()));
471 ///
472 /// // No display name in plain URI
473 /// let name = handler.extract_display_name_from_header(
474 /// "sip:carol@example.com"
475 /// );
476 /// assert_eq!(name, None);
477 /// ```
478 pub fn extract_display_name_from_header(&self, header: &str) -> Option<String> {
479 if let Some(start) = header.find('"') {
480 if let Some(end) = header[start + 1..].find('"') {
481 let display_name = &header[start + 1..start + 1 + end];
482 if !display_name.is_empty() {
483 return Some(display_name.to_string());
484 }
485 }
486 }
487
488 if let Some(angle_pos) = header.find('<') {
489 let potential_name = header[..angle_pos].trim();
490 if !potential_name.is_empty() && !potential_name.starts_with("sip:") {
491 return Some(potential_name.to_string());
492 }
493 }
494
495 None
496 }
497
498 /// Extract call subject from SIP message headers
499 ///
500 /// This method extracts the subject/purpose of a call from SIP message headers.
501 /// The subject provides contextual information about the call and is typically
502 /// used for displaying call purpose in user interfaces or for call routing decisions.
503 ///
504 /// # Arguments
505 ///
506 /// * `headers` - HashMap containing SIP message headers to search for subject information
507 ///
508 /// # Returns
509 ///
510 /// `Some(String)` containing the subject text if found and non-empty,
511 /// `None` if no subject header exists or if the subject is empty.
512 ///
513 /// # Header Priority
514 ///
515 /// The method searches for subject information in the following order:
516 /// 1. "Subject" header (standard case)
517 /// 2. "subject" header (lowercase variant)
518 ///
519 /// # Examples
520 ///
521 /// ```rust
522 /// # use rvoip_client_core::client::events::ClientCallHandler;
523 /// # use std::sync::Arc;
524 /// # use std::collections::HashMap;
525 /// # use dashmap::DashMap;
526 /// let handler = ClientCallHandler::new(
527 /// Arc::new(DashMap::new()),
528 /// Arc::new(DashMap::new()),
529 /// Arc::new(DashMap::new()),
530 /// Arc::new(DashMap::new()),
531 /// );
532 ///
533 /// let mut headers = HashMap::new();
534 /// headers.insert("Subject".to_string(), "Conference Call".to_string());
535 ///
536 /// // Extract subject from headers
537 /// let subject = handler.extract_subject(&headers);
538 /// assert_eq!(subject, Some("Conference Call".to_string()));
539 ///
540 /// // Empty subject returns None
541 /// let mut empty_headers = HashMap::new();
542 /// empty_headers.insert("Subject".to_string(), "".to_string());
543 /// let subject = handler.extract_subject(&empty_headers);
544 /// assert_eq!(subject, None);
545 ///
546 /// // No subject header returns None
547 /// let subject = handler.extract_subject(&HashMap::new());
548 /// assert_eq!(subject, None);
549 /// ```
550 pub fn extract_subject(&self, headers: &HashMap<String, String>) -> Option<String> {
551 headers.get("Subject")
552 .or_else(|| headers.get("subject"))
553 .cloned()
554 .filter(|s| !s.is_empty())
555 }
556
557 /// Extract SIP Call-ID from message headers
558 ///
559 /// This method extracts the unique Call-ID identifier from SIP message headers.
560 /// The Call-ID is a mandatory header in SIP messages that uniquely identifies
561 /// a call dialog and remains constant throughout the entire call session.
562 ///
563 /// # Arguments
564 ///
565 /// * `headers` - HashMap containing SIP message headers to search for Call-ID
566 ///
567 /// # Returns
568 ///
569 /// `Some(String)` containing the Call-ID value if found,
570 /// `None` if no Call-ID header exists in the message.
571 ///
572 /// # Header Priority
573 ///
574 /// The method searches for Call-ID information in the following order:
575 /// 1. "Call-ID" header (standard case)
576 /// 2. "call-id" header (lowercase variant)
577 ///
578 /// # SIP Specification
579 ///
580 /// According to RFC 3261, the Call-ID header is mandatory in all SIP requests
581 /// and responses. It consists of a locally unique identifier followed by an
582 /// "@" sign and a globally unique identifier (usually the host domain).
583 ///
584 /// # Examples
585 ///
586 /// ```rust
587 /// # use rvoip_client_core::client::events::ClientCallHandler;
588 /// # use std::sync::Arc;
589 /// # use std::collections::HashMap;
590 /// # use dashmap::DashMap;
591 /// let handler = ClientCallHandler::new(
592 /// Arc::new(DashMap::new()),
593 /// Arc::new(DashMap::new()),
594 /// Arc::new(DashMap::new()),
595 /// Arc::new(DashMap::new()),
596 /// );
597 ///
598 /// let mut headers = HashMap::new();
599 /// headers.insert("Call-ID".to_string(), "1234567890@example.com".to_string());
600 ///
601 /// // Extract Call-ID from headers
602 /// let call_id = handler.extract_call_id(&headers);
603 /// assert_eq!(call_id, Some("1234567890@example.com".to_string()));
604 ///
605 /// // Case-insensitive header lookup
606 /// let mut headers_lower = HashMap::new();
607 /// headers_lower.insert("call-id".to_string(), "abcdef@sip.example.org".to_string());
608 /// let call_id = handler.extract_call_id(&headers_lower);
609 /// assert_eq!(call_id, Some("abcdef@sip.example.org".to_string()));
610 ///
611 /// // No Call-ID header returns None
612 /// let call_id = handler.extract_call_id(&HashMap::new());
613 /// assert_eq!(call_id, None);
614 /// ```
615 pub fn extract_call_id(&self, headers: &HashMap<String, String>) -> Option<String> {
616 headers.get("Call-ID")
617 .or_else(|| headers.get("call-id"))
618 .cloned()
619 }
620
621 /// Update stored call information with enhanced session data
622 ///
623 /// This method synchronizes call information stored in the client-core layer
624 /// with the current state from the session-core layer. It handles state transitions,
625 /// timestamp updates, and event emission when significant changes occur.
626 ///
627 /// # Arguments
628 ///
629 /// * `call_id` - The client-core call ID to update information for
630 /// * `session` - The session-core CallSession object containing current state and metadata
631 ///
632 /// # Behavior
633 ///
634 /// The method performs the following operations:
635 /// 1. Maps session-core state to client-core state representation
636 /// 2. Updates timestamps for significant state transitions (connected, ended)
637 /// 3. Emits state change events to registered handlers when state changes
638 /// 4. Preserves historical information and call metadata
639 ///
640 /// # State Transitions
641 ///
642 /// Special handling is applied for specific state transitions:
643 /// - **Connected**: Sets `connected_at` timestamp if not already set
644 /// - **Terminated/Failed/Cancelled**: Sets `ended_at` timestamp if not already set
645 /// - **Other states**: Updates state without timestamp modifications
646 ///
647 /// # Event Emission
648 ///
649 /// When a state change is detected, the method automatically emits a `CallStatusInfo`
650 /// event to the registered client event handler, providing:
651 /// - Current and previous states
652 /// - Transition timestamp
653 /// - Call identification information
654 ///
655 /// # Examples
656 ///
657 /// ```rust
658 /// # use rvoip_client_core::client::events::ClientCallHandler;
659 /// # use rvoip_client_core::call::CallId;
660 /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
661 /// # use std::sync::Arc;
662 /// # use dashmap::DashMap;
663 /// # #[tokio::main]
664 /// # async fn main() {
665 /// let handler = ClientCallHandler::new(
666 /// Arc::new(DashMap::new()),
667 /// Arc::new(DashMap::new()),
668 /// Arc::new(DashMap::new()),
669 /// Arc::new(DashMap::new()),
670 /// );
671 ///
672 /// let call_id = CallId::new_v4();
673 /// # let session = CallSession {
674 /// # id: SessionId("session123".to_string()),
675 /// # from: "sip:alice@example.com".to_string(),
676 /// # to: "sip:bob@example.com".to_string(),
677 /// # state: CallState::Active,
678 /// # started_at: Some(std::time::Instant::now()),
679 /// # };
680 ///
681 /// // Update call info when session state changes
682 /// handler.update_call_info_from_session(call_id, &session).await;
683 ///
684 /// // The method will:
685 /// // 1. Map CallState::Active to client-core Connected state
686 /// // 2. Set connected_at timestamp if transitioning to Connected
687 /// // 3. Emit state change event to registered handlers
688 /// # }
689 /// ```
690 ///
691 /// # Thread Safety
692 ///
693 /// This method is async and thread-safe. It uses atomic operations on the
694 /// underlying DashMap storage and properly handles concurrent access to call information.
695 pub async fn update_call_info_from_session(&self, call_id: CallId, session: &CallSession) {
696 if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
697 // Update state if it changed
698 let new_client_state = self.map_session_state_to_client_state(&session.state);
699 let old_state = call_info_ref.state.clone();
700
701 if new_client_state != old_state {
702 // Update timestamps based on state transition
703 match new_client_state {
704 crate::call::CallState::Connected => {
705 if call_info_ref.connected_at.is_none() {
706 call_info_ref.connected_at = Some(Utc::now());
707 }
708 }
709 crate::call::CallState::Terminated |
710 crate::call::CallState::Failed |
711 crate::call::CallState::Cancelled => {
712 if call_info_ref.ended_at.is_none() {
713 call_info_ref.ended_at = Some(Utc::now());
714 }
715 }
716 _ => {}
717 }
718
719 call_info_ref.state = new_client_state.clone();
720
721 // Emit state change event
722 if let Some(handler) = self.client_event_handler.read().await.as_ref() {
723 let status_info = CallStatusInfo {
724 call_id,
725 new_state: new_client_state,
726 previous_state: Some(old_state),
727 reason: None,
728 timestamp: Utc::now(),
729 };
730 handler.on_call_state_changed(status_info).await;
731 }
732 }
733 }
734 }
735
736 /// Map session-core CallState to client-core CallState with enhanced logic
737 ///
738 /// This method translates between the internal session-core call state representation
739 /// and the client-facing call state representation. It provides a clean abstraction
740 /// layer and applies enhanced logic for complex state mappings.
741 ///
742 /// # Arguments
743 ///
744 /// * `session_state` - The session-core CallState to map to client-core representation
745 ///
746 /// # Returns
747 ///
748 /// The corresponding client-core CallState that represents the same logical state
749 /// but with client-appropriate semantics and naming.
750 ///
751 /// # State Mapping Logic
752 ///
753 /// The mapping applies the following transformations:
754 ///
755 /// | Session-Core State | Client-Core State | Notes |
756 /// |-------------------|------------------|--------|
757 /// | `Initiating` | `Initiating` | Direct mapping |
758 /// | `Ringing` | `Ringing` | Direct mapping |
759 /// | `Active` | `Connected` | Semantic clarity for client |
760 /// | `OnHold` | `Connected` | Still connected, just on hold |
761 /// | `Transferring` | `Proceeding` | Transfer in progress |
762 /// | `Terminating` | `Terminating` | Direct mapping |
763 /// | `Terminated` | `Terminated` | Direct mapping |
764 /// | `Cancelled` | `Cancelled` | Direct mapping |
765 /// | `Failed(reason)` | `Failed` | Logs reason, maps to simple Failed |
766 ///
767 /// # Enhanced Logic
768 ///
769 /// - **OnHold Handling**: Calls on hold are still considered "Connected" from the client
770 /// perspective, as the media session is established and can be resumed.
771 /// - **Transfer Handling**: Calls being transferred are mapped to "Proceeding" to indicate
772 /// ongoing call setup activities.
773 /// - **Failure Handling**: Failed states with reasons are logged for debugging but
774 /// simplified to a single Failed state for client consumption.
775 ///
776 /// # Examples
777 ///
778 /// ```rust
779 /// # use rvoip_client_core::client::events::ClientCallHandler;
780 /// # use rvoip_session_core::api::types::CallState;
781 /// # use std::sync::Arc;
782 /// # use dashmap::DashMap;
783 /// let handler = ClientCallHandler::new(
784 /// Arc::new(DashMap::new()),
785 /// Arc::new(DashMap::new()),
786 /// Arc::new(DashMap::new()),
787 /// Arc::new(DashMap::new()),
788 /// );
789 ///
790 /// // Map active session to connected client state
791 /// let session_state = CallState::Active;
792 /// let client_state = handler.map_session_state_to_client_state(&session_state);
793 /// assert_eq!(client_state, rvoip_client_core::call::CallState::Connected);
794 ///
795 /// // Map on-hold session to still connected client state
796 /// let session_state = CallState::OnHold;
797 /// let client_state = handler.map_session_state_to_client_state(&session_state);
798 /// assert_eq!(client_state, rvoip_client_core::call::CallState::Connected);
799 ///
800 /// // Map failed session to failed client state (reason is logged)
801 /// let session_state = CallState::Failed("Network timeout".to_string());
802 /// let client_state = handler.map_session_state_to_client_state(&session_state);
803 /// assert_eq!(client_state, rvoip_client_core::call::CallState::Failed);
804 /// ```
805 ///
806 /// # Logging
807 ///
808 /// The method logs debug information for failed states to assist with troubleshooting
809 /// while providing a clean interface to client applications.
810 pub fn map_session_state_to_client_state(&self, session_state: &CallState) -> crate::call::CallState {
811 match session_state {
812 CallState::Initiating => crate::call::CallState::Initiating,
813 CallState::Ringing => crate::call::CallState::Ringing,
814 CallState::Active => crate::call::CallState::Connected,
815 CallState::OnHold => crate::call::CallState::Connected, // Still connected, just on hold
816 CallState::Transferring => crate::call::CallState::Proceeding,
817 CallState::Terminating => crate::call::CallState::Terminating,
818 CallState::Terminated => crate::call::CallState::Terminated,
819 CallState::Cancelled => crate::call::CallState::Cancelled,
820 CallState::Failed(reason) => {
821 tracing::debug!("Call failed with reason: {}", reason);
822 crate::call::CallState::Failed
823 }
824 }
825 }
826}
827
828/// Implementation of session-core CallHandler trait for ClientCallHandler
829///
830/// This trait implementation bridges session-core events to client-core events,
831/// providing the core event translation and handling logic. The implementation
832/// receives low-level session events and transforms them into high-level client
833/// events that applications can easily consume.
834///
835/// # Event Flow
836///
837/// 1. **Session-core events** arrive through this trait implementation
838/// 2. **Event translation** maps session concepts to client concepts
839/// 3. **State management** updates call information and mappings
840/// 4. **Client events** are emitted to registered handlers and broadcast channels
841/// 5. **Cleanup** removes temporary state when calls complete
842///
843/// # Thread Safety
844///
845/// All methods in this implementation are async and thread-safe, using
846/// atomic operations and concurrent data structures for state management.
847#[async_trait::async_trait]
848impl CallHandler for ClientCallHandler {
849 /// Handle incoming call from session-core layer
850 ///
851 /// This method is called by session-core when a new incoming call arrives.
852 /// It performs comprehensive call processing including ID mapping, metadata extraction,
853 /// event emission, and decision routing to the application layer.
854 ///
855 /// # Arguments
856 ///
857 /// * `call` - The IncomingCall object from session-core containing all call details
858 ///
859 /// # Returns
860 ///
861 /// `CallDecision` indicating how session-core should handle the call:
862 /// - `Accept(sdp)` - Accept the call with optional SDP answer
863 /// - `Reject(reason)` - Reject the call with a reason string
864 /// - `Defer` - Defer the decision (used for ignore action)
865 ///
866 /// # Processing Flow
867 ///
868 /// 1. **ID Mapping**: Creates new client call ID and establishes bidirectional mapping
869 /// 2. **Metadata Extraction**: Extracts display names, subject, and SIP headers
870 /// 3. **Call Info Creation**: Creates comprehensive CallInfo with all available data
871 /// 4. **Event Broadcasting**: Emits incoming call event to broadcast channel if configured
872 /// 5. **Handler Consultation**: Forwards to application event handler for decision
873 /// 6. **Decision Translation**: Maps client decision back to session-core format
874 ///
875 /// # SDP Handling
876 ///
877 /// - If the incoming call contains an SDP offer and the application accepts,
878 /// the method allows session-core to generate the SDP answer automatically
879 /// - If no SDP offer is present, the call is accepted without SDP negotiation
880 /// - Complex SDP scenarios are handled transparently by session-core
881 ///
882 /// # State Management
883 ///
884 /// - Call starts in `IncomingPending` state
885 /// - Full call information is stored for history and reporting
886 /// - Incoming call object is stored for later access during accept/reject operations
887 ///
888 /// # Examples
889 ///
890 /// ```rust
891 /// # use rvoip_client_core::client::events::ClientCallHandler;
892 /// # use rvoip_session_core::api::types::{IncomingCall, CallDecision, SessionId};
893 /// # use rvoip_session_core::CallHandler;
894 /// # use std::sync::Arc;
895 /// # use std::collections::HashMap;
896 /// # use dashmap::DashMap;
897 /// # #[tokio::main]
898 /// # async fn main() {
899 /// let handler = ClientCallHandler::new(
900 /// Arc::new(DashMap::new()),
901 /// Arc::new(DashMap::new()),
902 /// Arc::new(DashMap::new()),
903 /// Arc::new(DashMap::new()),
904 /// );
905 ///
906 /// # let incoming_call = IncomingCall {
907 /// # id: SessionId("session123".to_string()),
908 /// # from: "\"Alice Smith\" <sip:alice@example.com>".to_string(),
909 /// # to: "sip:bob@example.com".to_string(),
910 /// # sdp: Some("v=0...".to_string()),
911 /// # headers: HashMap::new(),
912 /// # received_at: std::time::Instant::now(),
913 /// # };
914 ///
915 /// // This method is called automatically by session-core
916 /// let decision = handler.on_incoming_call(incoming_call).await;
917 ///
918 /// // The method will:
919 /// // 1. Extract caller display name "Alice Smith"
920 /// // 2. Create call info with all metadata
921 /// // 3. Emit incoming call event to application
922 /// // 4. Return application's accept/reject decision
923 /// # }
924 /// ```
925 async fn on_incoming_call(&self, call: IncomingCall) -> CallDecision {
926 // Map session to call
927 let call_id = CallId::new_v4();
928 self.call_mapping.insert(call.id.clone(), call_id);
929 self.session_mapping.insert(call_id, call.id.clone());
930
931 // Store the IncomingCall for later use in answer/reject
932 self.incoming_calls.insert(call_id, call.clone());
933
934 // Enhanced call info extraction
935 let caller_display_name = self.extract_display_name(&call.from, &call.headers);
936 let subject = self.extract_subject(&call.headers);
937 let sip_call_id = self.extract_call_id(&call.headers)
938 .unwrap_or_else(|| call.id.0.clone());
939
940 // Create comprehensive call info
941 let call_info = CallInfo {
942 call_id,
943 state: crate::call::CallState::IncomingPending,
944 direction: CallDirection::Incoming,
945 local_uri: call.to.clone(),
946 remote_uri: call.from.clone(),
947 remote_display_name: caller_display_name.clone(),
948 subject: subject.clone(),
949 created_at: Utc::now(),
950 connected_at: None,
951 ended_at: None,
952 remote_addr: None, // TODO: Extract from session if available
953 media_session_id: None,
954 sip_call_id,
955 metadata: call.headers.clone(),
956 };
957
958 // Store call info
959 self.call_info.insert(call_id, call_info.clone());
960
961 // Create incoming call info for event
962 let incoming_call_info = IncomingCallInfo {
963 call_id,
964 caller_uri: call.from.clone(),
965 callee_uri: call.to.clone(),
966 caller_display_name,
967 subject,
968 created_at: Utc::now(),
969 };
970
971 // Broadcast event
972 if let Some(event_tx) = &self.event_tx {
973 let _ = event_tx.send(crate::events::ClientEvent::IncomingCall {
974 info: incoming_call_info.clone(),
975 priority: crate::events::EventPriority::High,
976 });
977 }
978
979 // Forward to client event handler
980 if let Some(handler) = self.client_event_handler.read().await.as_ref() {
981 let action = handler.on_incoming_call(incoming_call_info).await;
982 match action {
983 crate::events::CallAction::Accept => {
984 // When Accept is returned, we need to generate SDP answer and accept the call
985 tracing::info!("Handler returned Accept for call {}, generating SDP answer", call_id);
986
987 // Generate SDP answer if the incoming call has an offer
988 let sdp_answer = if let Some(_offer) = &call.sdp {
989 // Use session-core's media control to generate answer
990 // Note: We need access to the coordinator here, which we don't have directly
991 // So we'll let session-core handle SDP generation by passing None
992 // and marking that we need SDP generation
993 tracing::info!("Incoming call has SDP offer, will generate answer in session-core");
994 None // Let session-core generate the answer
995 } else {
996 tracing::info!("No SDP offer in incoming call, accepting without SDP");
997 None
998 };
999
1000 // Return Accept with the SDP (or None to let session-core generate it)
1001 CallDecision::Accept(sdp_answer)
1002 }
1003 crate::events::CallAction::Reject => CallDecision::Reject("Call rejected by user".to_string()),
1004 crate::events::CallAction::Ignore => CallDecision::Defer,
1005 }
1006 } else {
1007 CallDecision::Reject("No event handler configured".to_string())
1008 }
1009 }
1010
1011 /// Handle call termination from session-core layer
1012 ///
1013 /// This method is called by session-core when a call ends, regardless of the cause
1014 /// (user hangup, network failure, timeout, etc.). It performs cleanup operations,
1015 /// updates call statistics, and emits final state change events.
1016 ///
1017 /// # Arguments
1018 ///
1019 /// * `session` - The CallSession object containing final call state and metadata
1020 /// * `reason` - Human-readable string describing why the call ended
1021 ///
1022 /// # Processing Flow
1023 ///
1024 /// 1. **Call Lookup**: Maps session ID to client call ID
1025 /// 2. **Statistics Update**: Handles connected call counter management
1026 /// 3. **State Finalization**: Updates call info with final state and timestamp
1027 /// 4. **Metadata Preservation**: Stores termination reason and final state
1028 /// 5. **Event Emission**: Broadcasts call ended event to handlers
1029 /// 6. **Cleanup**: Removes active mappings while preserving call history
1030 ///
1031 /// # Critical Bug Fix
1032 ///
1033 /// This method includes a critical fix for integer overflow in call statistics.
1034 /// Previously, calls ending through session-core (network timeouts, remote hangup)
1035 /// weren't decrementing the connected_calls counter, leading to overflow issues.
1036 ///
1037 /// The fix:
1038 /// - Tracks whether the call was in Connected state before termination
1039 /// - Adds metadata flag for the manager to decrement counters appropriately
1040 /// - Prevents statistics corruption from external call termination
1041 ///
1042 /// # State Management
1043 ///
1044 /// - Final call state is mapped from session-core to client-core representation
1045 /// - `ended_at` timestamp is set to current time
1046 /// - Termination reason is stored in call metadata for history
1047 /// - Call information is preserved for reporting and analytics
1048 ///
1049 /// # Cleanup Operations
1050 ///
1051 /// - Removes active session-to-call and call-to-session mappings
1052 /// - Preserves call_info for historical access and reporting
1053 /// - Cleans up any temporary state related to the call
1054 ///
1055 /// # Examples
1056 ///
1057 /// ```rust
1058 /// # use rvoip_client_core::client::events::ClientCallHandler;
1059 /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
1060 /// # use rvoip_session_core::CallHandler;
1061 /// # use std::sync::Arc;
1062 /// # use dashmap::DashMap;
1063 /// # #[tokio::main]
1064 /// # async fn main() {
1065 /// let handler = ClientCallHandler::new(
1066 /// Arc::new(DashMap::new()),
1067 /// Arc::new(DashMap::new()),
1068 /// Arc::new(DashMap::new()),
1069 /// Arc::new(DashMap::new()),
1070 /// );
1071 ///
1072 /// # let session = CallSession {
1073 /// # id: SessionId("session123".to_string()),
1074 /// # from: "sip:alice@example.com".to_string(),
1075 /// # to: "sip:bob@example.com".to_string(),
1076 /// # state: CallState::Terminated,
1077 /// # started_at: Some(std::time::Instant::now()),
1078 /// # };
1079 ///
1080 /// // This method is called automatically by session-core
1081 /// handler.on_call_ended(session, "User hangup").await;
1082 ///
1083 /// // The method will:
1084 /// // 1. Update call info to Terminated state
1085 /// // 2. Set ended_at timestamp
1086 /// // 3. Store "User hangup" in metadata
1087 /// // 4. Emit final state change event
1088 /// // 5. Clean up active mappings
1089 /// # }
1090 /// ```
1091 async fn on_call_ended(&self, session: CallSession, reason: &str) {
1092 // Map session to client call and emit event
1093 if let Some(call_id) = self.call_mapping.get(&session.id).map(|entry| *entry.value()) {
1094 // Check if the call was previously connected to determine if we need to decrement counter
1095 let was_connected = if let Some(call_info) = self.call_info.get(&call_id) {
1096 call_info.state == crate::call::CallState::Connected
1097 } else {
1098 false
1099 };
1100
1101 // Update call info with final state
1102 if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1103 call_info_ref.state = self.map_session_state_to_client_state(&session.state);
1104 call_info_ref.ended_at = Some(Utc::now());
1105
1106 // Add termination reason to metadata
1107 call_info_ref.metadata.insert("termination_reason".to_string(), reason.to_string());
1108 }
1109
1110 // Update stats - decrement connected_calls if the call was connected
1111 // This fixes the critical integer overflow bug where calls ending through
1112 // session-core (network timeouts, remote hangup, etc.) weren't decrementing the counter
1113 if was_connected {
1114 // We need access to the stats, but we don't have it directly here.
1115 // We'll emit a special event that the manager can handle to update stats.
1116 tracing::debug!("Call {} was connected and ended, should decrement connected_calls counter", call_id);
1117
1118 // Since we can't access stats directly, we'll add metadata to let
1119 // the manager know to decrement the counter when processing this event
1120 if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1121 call_info_ref.metadata.insert("was_connected_when_ended".to_string(), "true".to_string());
1122 }
1123 }
1124
1125 let status_info = CallStatusInfo {
1126 call_id,
1127 new_state: self.map_session_state_to_client_state(&session.state),
1128 previous_state: None, // TODO: Track previous state
1129 reason: Some(reason.to_string()),
1130 timestamp: Utc::now(),
1131 };
1132
1133 // Broadcast event
1134 if let Some(event_tx) = &self.event_tx {
1135 let _ = event_tx.send(crate::events::ClientEvent::CallStateChanged {
1136 info: status_info.clone(),
1137 priority: crate::events::EventPriority::Normal,
1138 });
1139 }
1140
1141 // Forward to client event handler
1142 if let Some(handler) = self.client_event_handler.read().await.as_ref() {
1143 handler.on_call_state_changed(status_info).await;
1144 }
1145
1146 // Clean up mappings but keep call_info for history
1147 self.call_mapping.remove(&session.id);
1148 self.session_mapping.remove(&call_id);
1149 }
1150 }
1151
1152 /// Handle successful call establishment from session-core layer
1153 ///
1154 /// This method is called by session-core when a call is successfully established
1155 /// and media can flow between participants. It represents the transition from
1156 /// call setup to active communication phase.
1157 ///
1158 /// # Arguments
1159 ///
1160 /// * `session` - The CallSession object containing established call state
1161 /// * `local_sdp` - Optional SDP offer/answer generated locally
1162 /// * `remote_sdp` - Optional SDP offer/answer received from remote party
1163 ///
1164 /// # Processing Flow
1165 ///
1166 /// 1. **Call Lookup**: Maps session ID to client call ID
1167 /// 2. **State Update**: Transitions call to Connected state
1168 /// 3. **Timestamp Recording**: Sets connected_at timestamp for analytics
1169 /// 4. **SDP Storage**: Preserves SDP information in call metadata
1170 /// 5. **Event Emission**: Broadcasts call established event with high priority
1171 /// 6. **Logging**: Records successful establishment for debugging
1172 ///
1173 /// # SDP Information Management
1174 ///
1175 /// Both local and remote SDP information is stored in call metadata:
1176 /// - `local_sdp`: The SDP offer or answer generated by the local endpoint
1177 /// - `remote_sdp`: The SDP offer or answer received from the remote endpoint
1178 ///
1179 /// This information is crucial for:
1180 /// - Media session management and troubleshooting
1181 /// - Codec negotiation analysis
1182 /// - Network path and capability verification
1183 /// - Call quality investigation
1184 ///
1185 /// # State Transitions
1186 ///
1187 /// - Updates call state to `Connected`
1188 /// - Sets `connected_at` timestamp if not already set
1189 /// - Preserves call establishment timing for billing and analytics
1190 ///
1191 /// # Event Priority
1192 ///
1193 /// Call establishment events are emitted with `High` priority because:
1194 /// - They represent successful completion of call setup
1195 /// - Applications often need immediate notification for UI updates
1196 /// - Statistics and billing systems require prompt notification
1197 ///
1198 /// # Examples
1199 ///
1200 /// ```rust
1201 /// # use rvoip_client_core::client::events::ClientCallHandler;
1202 /// # use rvoip_session_core::api::types::{CallSession, CallState, SessionId};
1203 /// # use rvoip_session_core::CallHandler;
1204 /// # use std::sync::Arc;
1205 /// # use dashmap::DashMap;
1206 /// # #[tokio::main]
1207 /// # async fn main() {
1208 /// let handler = ClientCallHandler::new(
1209 /// Arc::new(DashMap::new()),
1210 /// Arc::new(DashMap::new()),
1211 /// Arc::new(DashMap::new()),
1212 /// Arc::new(DashMap::new()),
1213 /// );
1214 ///
1215 /// # let session = CallSession {
1216 /// # id: SessionId("session123".to_string()),
1217 /// # from: "sip:alice@example.com".to_string(),
1218 /// # to: "sip:bob@example.com".to_string(),
1219 /// # state: CallState::Active,
1220 /// # started_at: Some(std::time::Instant::now()),
1221 /// # };
1222 ///
1223 /// let local_sdp = Some("v=0\r\no=alice 123456 654321 IN IP4 192.168.1.100\r\n...".to_string());
1224 /// let remote_sdp = Some("v=0\r\no=bob 789012 210987 IN IP4 192.168.1.200\r\n...".to_string());
1225 ///
1226 /// // This method is called automatically by session-core
1227 /// handler.on_call_established(session, local_sdp, remote_sdp).await;
1228 ///
1229 /// // The method will:
1230 /// // 1. Update call state to Connected
1231 /// // 2. Set connected_at timestamp
1232 /// // 3. Store both local and remote SDP in metadata
1233 /// // 4. Emit high-priority call established event
1234 /// // 5. Log successful establishment
1235 /// # }
1236 /// ```
1237 async fn on_call_established(&self, session: CallSession, local_sdp: Option<String>, remote_sdp: Option<String>) {
1238 // Map session to client call
1239 if let Some(call_id) = self.call_mapping.get(&session.id).map(|entry| *entry.value()) {
1240 // Update call info with establishment
1241 if let Some(mut call_info_ref) = self.call_info.get_mut(&call_id) {
1242 call_info_ref.state = crate::call::CallState::Connected;
1243 if call_info_ref.connected_at.is_none() {
1244 call_info_ref.connected_at = Some(Utc::now());
1245 }
1246
1247 // Store SDP information
1248 if let Some(local_sdp) = &local_sdp {
1249 call_info_ref.metadata.insert("local_sdp".to_string(), local_sdp.clone());
1250 }
1251 if let Some(remote_sdp) = &remote_sdp {
1252 call_info_ref.metadata.insert("remote_sdp".to_string(), remote_sdp.clone());
1253
1254 // Process the SDP answer to configure RTP endpoints
1255 // Note: We don't have direct access to ClientManager here, but that's OK
1256 // because session-core will handle the SDP processing when it receives
1257 // the CallAnswered event. The remote SDP is already stored in metadata.
1258 }
1259 }
1260
1261 let status_info = CallStatusInfo {
1262 call_id,
1263 new_state: crate::call::CallState::Connected,
1264 previous_state: Some(crate::call::CallState::Proceeding),
1265 reason: Some("Call established".to_string()),
1266 timestamp: Utc::now(),
1267 };
1268
1269 // Broadcast event
1270 if let Some(event_tx) = &self.event_tx {
1271 let _ = event_tx.send(crate::events::ClientEvent::CallStateChanged {
1272 info: status_info.clone(),
1273 priority: crate::events::EventPriority::High,
1274 });
1275 }
1276
1277 // Forward to client event handler
1278 if let Some(handler) = self.client_event_handler.read().await.as_ref() {
1279 handler.on_call_state_changed(status_info).await;
1280 }
1281
1282 // Notify about call establishment for audio setup
1283 if let Some(tx) = &self.call_established_tx {
1284 let _ = tx.send(call_id);
1285 }
1286
1287 tracing::info!("Call {} established with SDP exchange", call_id);
1288 }
1289 }
1290}