rvoip_client_core/client/calls.rs
1//! Call operations for the client-core library
2//!
3//! This module contains all call-related operations including making calls,
4//! answering, rejecting, hanging up, and querying call information.
5//!
6//! # Call Management Overview
7//!
8//! The call operations provide a comprehensive API for managing SIP calls through
9//! the session-core infrastructure. This includes:
10//!
11//! - **Outgoing Calls**: Initiate calls with `make_call()`
12//! - **Incoming Calls**: Handle with `answer_call()` and `reject_call()`
13//! - **Call Control**: Terminate calls with `hangup_call()`
14//! - **Call Information**: Query call state and history
15//! - **Statistics**: Track call metrics and performance
16//!
17//! # Architecture
18//!
19//! ```text
20//! ┌─────────────────────────┐
21//! │ Client Application │
22//! └───────────┬─────────────┘
23//! │
24//! ┌───────────▼─────────────┐
25//! │ Call Operations │ ◄── This Module
26//! │ ┌─────────────────────┐ │
27//! │ │ make_call() │ │
28//! │ │ answer_call() │ │
29//! │ │ hangup_call() │ │
30//! │ │ get_call_*() │ │
31//! │ └─────────────────────┘ │
32//! └───────────┬─────────────┘
33//! │
34//! ┌───────────▼─────────────┐
35//! │ session-core │
36//! │ SessionControl API │
37//! └─────────────────────────┘
38//! ```
39//!
40//! # Usage Examples
41//!
42//! ```rust
43//! use rvoip_client_core::{ClientManager, ClientConfig, CallId, CallState};
44//!
45//! async fn call_operations_example() -> Result<(), Box<dyn std::error::Error>> {
46//! // Create and start client
47//! let config = ClientConfig::new()
48//! .with_sip_addr("127.0.0.1:5060".parse()?);
49//! let client = ClientManager::new(config).await?;
50//! client.start().await?;
51//!
52//! // Make an outgoing call
53//! let call_id = client.make_call(
54//! "sip:alice@example.com".to_string(),
55//! "sip:bob@example.com".to_string(),
56//! Some("Business call".to_string()),
57//! ).await?;
58//!
59//! // Check call information
60//! let call_info = client.get_call(&call_id).await?;
61//! println!("Call state: {:?}", call_info.state);
62//!
63//! // List all active calls
64//! let active_calls = client.get_active_calls().await;
65//!
66//! // End the call
67//! client.hangup_call(&call_id).await?;
68//!
69//! Ok(())
70//! }
71//! ```
72
73use std::collections::HashMap;
74use chrono::Utc;
75
76// Import session-core APIs
77use rvoip_session_core::api::{
78 SessionControl,
79 MediaControl,
80};
81
82// Import client-core types
83use crate::{
84 ClientResult, ClientError,
85 call::{CallId, CallInfo, CallDirection},
86};
87
88use super::types::*;
89use super::recovery::{retry_with_backoff, RetryConfig, ErrorContext};
90
91/// Call operations implementation for ClientManager
92impl super::manager::ClientManager {
93 /// Make an outgoing call with enhanced information tracking
94 ///
95 /// This method initiates a new outgoing call using the session-core infrastructure.
96 /// It handles SDP generation, session creation, and proper event notification.
97 ///
98 /// # Arguments
99 ///
100 /// * `from` - The caller's SIP URI (e.g., "sip:alice@example.com")
101 /// * `to` - The callee's SIP URI (e.g., "sip:bob@example.com")
102 /// * `subject` - Optional call subject/reason
103 ///
104 /// # Returns
105 ///
106 /// Returns a unique `CallId` that can be used to track and control the call.
107 ///
108 /// # Errors
109 ///
110 /// * `ClientError::InvalidConfiguration` - If the URIs are malformed
111 /// * `ClientError::NetworkError` - If there's a network connectivity issue
112 /// * `ClientError::CallSetupFailed` - If the call cannot be initiated
113 ///
114 /// # Examples
115 ///
116 /// Basic call:
117 /// ```rust
118 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
119 ///
120 /// async fn make_basic_call() -> Result<CallId, Box<dyn std::error::Error>> {
121 /// let config = ClientConfig::new()
122 /// .with_sip_addr("127.0.0.1:5060".parse()?);
123 /// let client = ClientManager::new(config).await?;
124 /// client.start().await?;
125 ///
126 /// let call_id = client.make_call(
127 /// "sip:alice@ourcompany.com".to_string(),
128 /// "sip:bob@example.com".to_string(),
129 /// None,
130 /// ).await?;
131 ///
132 /// println!("Outgoing call started: {}", call_id);
133 /// Ok(call_id)
134 /// }
135 /// ```
136 ///
137 /// Call with subject:
138 /// ```rust
139 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
140 ///
141 /// async fn make_call_with_subject() -> Result<CallId, Box<dyn std::error::Error>> {
142 /// let config = ClientConfig::new()
143 /// .with_sip_addr("127.0.0.1:5061".parse()?);
144 /// let client = ClientManager::new(config).await?;
145 /// client.start().await?;
146 ///
147 /// let call_id = client.make_call(
148 /// "sip:support@ourcompany.com".to_string(),
149 /// "sip:customer@example.com".to_string(),
150 /// Some("Technical Support Call".to_string()),
151 /// ).await?;
152 ///
153 /// Ok(call_id)
154 /// }
155 /// ```
156 ///
157 /// # Call Flow
158 ///
159 /// 1. Validates the SIP URIs
160 /// 2. Creates a new session via session-core
161 /// 3. Generates and stores call metadata
162 /// 4. Emits appropriate events
163 /// 5. Returns the CallId for tracking
164 pub async fn make_call(
165 &self,
166 from: String,
167 to: String,
168 subject: Option<String>,
169 ) -> ClientResult<CallId> {
170 // Check if client is running
171 if !*self.is_running.read().await {
172 return Err(ClientError::InternalError {
173 message: "Client is not started. Call start() before making calls.".to_string()
174 });
175 }
176
177 // Create call via session-core with retry logic for network errors
178 let prepared_call = retry_with_backoff(
179 "prepare_outgoing_call",
180 RetryConfig::quick(),
181 || async {
182 SessionControl::prepare_outgoing_call(
183 &self.coordinator,
184 &from,
185 &to
186 )
187 .await
188 .map_err(|e| ClientError::CallSetupFailed {
189 reason: format!("Failed to prepare call: {}", e)
190 })
191 }
192 )
193 .await
194 .with_context(|| format!("Failed to prepare call from {} to {}", from, to))?;
195
196 // Log the allocated RTP port
197 tracing::info!("Prepared call with allocated RTP port: {}", prepared_call.local_rtp_port);
198
199 // Now initiate the prepared call
200 let session = retry_with_backoff(
201 "initiate_prepared_call",
202 RetryConfig::quick(),
203 || async {
204 SessionControl::initiate_prepared_call(
205 &self.coordinator,
206 &prepared_call
207 )
208 .await
209 .map_err(|e| ClientError::CallSetupFailed {
210 reason: format!("Failed to initiate call: {}", e)
211 })
212 }
213 )
214 .await
215 .with_context(|| format!("Failed to initiate call from {} to {}", from, to))?;
216
217 // Create call ID and mapping
218 let call_id = CallId::new_v4();
219 self.call_handler.call_mapping.insert(session.id.clone(), call_id);
220 self.session_mapping.insert(call_id, session.id.clone());
221
222 // Create enhanced call info
223 let mut metadata = HashMap::new();
224 metadata.insert("created_via".to_string(), "make_call".to_string());
225 if let Some(ref subj) = subject {
226 metadata.insert("subject".to_string(), subj.clone());
227 }
228
229 let call_info = CallInfo {
230 call_id,
231 state: crate::call::CallState::Initiating,
232 direction: CallDirection::Outgoing,
233 local_uri: from,
234 remote_uri: to,
235 remote_display_name: None,
236 subject,
237 created_at: Utc::now(),
238 connected_at: None,
239 ended_at: None,
240 remote_addr: None,
241 media_session_id: None,
242 sip_call_id: session.id.0.clone(),
243 metadata,
244 };
245
246 self.call_info.insert(call_id, call_info.clone());
247
248 // Emit call created event
249 let _ = self.event_tx.send(crate::events::ClientEvent::CallStateChanged {
250 info: crate::events::CallStatusInfo {
251 call_id,
252 new_state: crate::call::CallState::Initiating,
253 previous_state: None, // No previous state for new calls
254 reason: Some("Call created".to_string()),
255 timestamp: Utc::now(),
256 },
257 priority: crate::events::EventPriority::Normal,
258 });
259
260 // Update stats
261 let mut stats = self.stats.lock().await;
262 stats.total_calls += 1;
263
264 tracing::info!("Created outgoing call {} -> {} (call_id: {})",
265 call_info.local_uri, call_info.remote_uri, call_id);
266
267 Ok(call_id)
268 }
269
270 /// Answer an incoming call with SDP negotiation
271 ///
272 /// This method accepts an incoming call that was previously stored by the event handler.
273 /// It performs SDP offer/answer negotiation and establishes the media session.
274 ///
275 /// # Arguments
276 ///
277 /// * `call_id` - The unique identifier of the incoming call to answer
278 ///
279 /// # Returns
280 ///
281 /// Returns `Ok(())` if the call was successfully answered and connected.
282 ///
283 /// # Errors
284 ///
285 /// * `ClientError::CallNotFound` - If no incoming call exists with the given ID
286 /// * `ClientError::CallSetupFailed` - If SDP negotiation or call setup fails
287 /// * `ClientError::InvalidCallState` - If the call is not in an answerable state
288 ///
289 /// # Examples
290 ///
291 /// Basic call answering:
292 /// ```rust
293 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, ClientEventHandler, CallAction, IncomingCallInfo};
294 /// use std::sync::Arc;
295 ///
296 /// struct MyEventHandler;
297 ///
298 /// #[async_trait::async_trait]
299 /// impl ClientEventHandler for MyEventHandler {
300 /// async fn on_incoming_call(&self, call_info: IncomingCallInfo) -> CallAction {
301 /// // Store call_id for later use
302 /// println!("Incoming call from: {}", call_info.caller_uri);
303 /// CallAction::Ignore // Let application handle it
304 /// }
305 ///
306 /// async fn on_call_state_changed(&self, _info: rvoip_client_core::CallStatusInfo) {}
307 /// async fn on_registration_status_changed(&self, _info: rvoip_client_core::RegistrationStatusInfo) {}
308 /// async fn on_media_event(&self, _info: rvoip_client_core::MediaEventInfo) {}
309 /// async fn on_client_error(&self, _error: rvoip_client_core::ClientError, _call_id: Option<CallId>) {}
310 /// async fn on_network_event(&self, _connected: bool, _reason: Option<String>) {}
311 /// }
312 ///
313 /// async fn answer_incoming_call(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
314 /// let config = ClientConfig::new()
315 /// .with_sip_addr("127.0.0.1:5062".parse()?);
316 /// let client = ClientManager::new(config).await?;
317 /// client.set_event_handler(Arc::new(MyEventHandler)).await;
318 /// client.start().await?;
319 ///
320 /// // Answer the call (assuming call_id was obtained from event handler)
321 /// client.answer_call(&call_id).await?;
322 /// println!("Successfully answered call: {}", call_id);
323 ///
324 /// Ok(())
325 /// }
326 /// ```
327 ///
328 /// # SDP Negotiation Process
329 ///
330 /// 1. Retrieves the stored incoming call information
331 /// 2. If an SDP offer was provided, generates an appropriate SDP answer
332 /// 3. If no offer was provided, generates an SDP offer (rare case)
333 /// 4. Calls session-core to accept the call with the negotiated SDP
334 /// 5. Updates call state to Connected and emits events
335 ///
336 /// # Thread Safety
337 ///
338 /// This method is async and thread-safe. Multiple calls can be answered concurrently.
339 pub async fn answer_call(&self, call_id: &CallId) -> ClientResult<()> {
340 // Get the stored IncomingCall object
341 let incoming_call = self.call_handler.get_incoming_call(call_id)
342 .await
343 .ok_or(ClientError::CallNotFound { call_id: *call_id })?;
344
345 // Generate SDP answer based on the offer
346 let sdp_answer = if let Some(offer) = &incoming_call.sdp {
347 // Use MediaControl to generate SDP answer
348 MediaControl::generate_sdp_answer(
349 &self.coordinator,
350 &incoming_call.id,
351 offer
352 )
353 .await
354 .map_err(|e| ClientError::CallSetupFailed {
355 reason: format!("Failed to generate SDP answer: {}", e)
356 })?
357 } else {
358 // No offer provided, generate our own SDP
359 MediaControl::generate_sdp_offer(
360 &self.coordinator,
361 &incoming_call.id
362 )
363 .await
364 .map_err(|e| ClientError::CallSetupFailed {
365 reason: format!("Failed to generate SDP: {}", e)
366 })?
367 };
368
369 // Use SessionControl to accept the call with SDP answer
370 SessionControl::accept_incoming_call(
371 &self.coordinator,
372 &incoming_call,
373 Some(sdp_answer) // Provide the generated SDP answer
374 )
375 .await
376 .map_err(|e| ClientError::CallSetupFailed {
377 reason: format!("Failed to answer call: {}", e)
378 })?;
379
380 // Update call info
381 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
382 call_info.state = crate::call::CallState::Connected;
383 call_info.connected_at = Some(Utc::now());
384 call_info.metadata.insert("answered_at".to_string(), Utc::now().to_rfc3339());
385 }
386
387 // Set up automatic audio frame subscription for the call
388 if let Err(e) = self.setup_call_audio(call_id).await {
389 // Log the error but don't fail the call - audio might still work
390 // through other means or this might be a non-audio call
391 tracing::warn!("Failed to set up audio for call {}: {}", call_id, e);
392 }
393
394 // Update stats
395 let mut stats = self.stats.lock().await;
396 stats.connected_calls += 1;
397
398 tracing::info!("Answered call {}", call_id);
399 Ok(())
400 }
401
402 /// Reject an incoming call with optional reason
403 ///
404 /// This method rejects an incoming call that was previously stored by the event handler.
405 /// The call will be terminated with a SIP rejection response.
406 ///
407 /// # Arguments
408 ///
409 /// * `call_id` - The unique identifier of the incoming call to reject
410 ///
411 /// # Returns
412 ///
413 /// Returns `Ok(())` if the call was successfully rejected.
414 ///
415 /// # Errors
416 ///
417 /// * `ClientError::CallNotFound` - If no incoming call exists with the given ID
418 /// * `ClientError::CallTerminated` - If the rejection fails to send properly
419 ///
420 /// # Examples
421 ///
422 /// Basic call rejection:
423 /// ```rust
424 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, ClientEventHandler, CallAction, IncomingCallInfo};
425 /// use std::sync::Arc;
426 ///
427 /// struct RejectingEventHandler;
428 ///
429 /// #[async_trait::async_trait]
430 /// impl ClientEventHandler for RejectingEventHandler {
431 /// async fn on_incoming_call(&self, call_info: IncomingCallInfo) -> CallAction {
432 /// // Automatically reject calls from unknown numbers
433 /// if !call_info.caller_uri.contains("@trusted-domain.com") {
434 /// CallAction::Reject
435 /// } else {
436 /// CallAction::Ignore
437 /// }
438 /// }
439 ///
440 /// async fn on_call_state_changed(&self, _info: rvoip_client_core::CallStatusInfo) {}
441 /// async fn on_registration_status_changed(&self, _info: rvoip_client_core::RegistrationStatusInfo) {}
442 /// async fn on_media_event(&self, _info: rvoip_client_core::MediaEventInfo) {}
443 /// async fn on_client_error(&self, _error: rvoip_client_core::ClientError, _call_id: Option<CallId>) {}
444 /// async fn on_network_event(&self, _connected: bool, _reason: Option<String>) {}
445 /// }
446 ///
447 /// async fn reject_unwanted_call(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
448 /// let config = ClientConfig::new()
449 /// .with_sip_addr("127.0.0.1:5063".parse()?);
450 /// let client = ClientManager::new(config).await?;
451 /// client.set_event_handler(Arc::new(RejectingEventHandler)).await;
452 /// client.start().await?;
453 ///
454 /// // Reject the call
455 /// client.reject_call(&call_id).await?;
456 /// println!("Successfully rejected call: {}", call_id);
457 ///
458 /// Ok(())
459 /// }
460 /// ```
461 ///
462 /// # Call Rejection Process
463 ///
464 /// 1. Retrieves the stored incoming call information
465 /// 2. Sends a SIP rejection response (typically 603 Decline)
466 /// 3. Updates call state to Terminated
467 /// 4. Records rejection reason in metadata
468 /// 5. Emits appropriate events
469 ///
470 /// # SIP Response Codes
471 ///
472 /// The rejection will typically result in a SIP 603 "Decline" response being sent
473 /// to the caller, indicating that the call was explicitly rejected by the user.
474 pub async fn reject_call(&self, call_id: &CallId) -> ClientResult<()> {
475 // Get the stored IncomingCall object
476 let incoming_call = self.call_handler.get_incoming_call(call_id)
477 .await
478 .ok_or(ClientError::CallNotFound { call_id: *call_id })?;
479
480 // Use SessionControl to reject the call
481 SessionControl::reject_incoming_call(
482 &self.coordinator,
483 &incoming_call,
484 "User rejected"
485 )
486 .await
487 .map_err(|e| ClientError::CallTerminated {
488 reason: format!("Failed to reject call: {}", e)
489 })?;
490
491 // Update call info
492 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
493 call_info.state = crate::call::CallState::Terminated;
494 call_info.ended_at = Some(Utc::now());
495 call_info.metadata.insert("rejected_at".to_string(), Utc::now().to_rfc3339());
496 call_info.metadata.insert("rejection_reason".to_string(), "user_rejected".to_string());
497 }
498
499 tracing::info!("Rejected call {}", call_id);
500 Ok(())
501 }
502
503 /// Terminate an active call (hang up)
504 ///
505 /// This method terminates any call regardless of its current state. It handles
506 /// proper session cleanup and state management.
507 ///
508 /// # Arguments
509 ///
510 /// * `call_id` - The unique identifier of the call to terminate
511 ///
512 /// # Returns
513 ///
514 /// Returns `Ok(())` if the call was successfully terminated or was already terminated.
515 ///
516 /// # Errors
517 ///
518 /// * `ClientError::CallNotFound` - If no call exists with the given ID
519 /// * `ClientError::CallTerminated` - If the termination process fails
520 ///
521 /// # Examples
522 ///
523 /// Basic call hangup:
524 /// ```rust
525 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
526 ///
527 /// async fn hangup_active_call(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
528 /// let config = ClientConfig::new()
529 /// .with_sip_addr("127.0.0.1:5064".parse()?);
530 /// let client = ClientManager::new(config).await?;
531 /// client.start().await?;
532 ///
533 /// // Terminate the call
534 /// client.hangup_call(&call_id).await?;
535 /// println!("Successfully hung up call: {}", call_id);
536 ///
537 /// Ok(())
538 /// }
539 /// ```
540 ///
541 /// Hangup with error handling:
542 /// ```rust
543 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, ClientError};
544 ///
545 /// async fn safe_hangup(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
546 /// let config = ClientConfig::new()
547 /// .with_sip_addr("127.0.0.1:5065".parse()?);
548 /// let client = ClientManager::new(config).await?;
549 /// client.start().await?;
550 ///
551 /// match client.hangup_call(&call_id).await {
552 /// Ok(()) => {
553 /// println!("Call terminated successfully");
554 /// }
555 /// Err(ClientError::CallNotFound { .. }) => {
556 /// println!("Call was already terminated or doesn't exist");
557 /// }
558 /// Err(e) => {
559 /// eprintln!("Failed to hangup call: {}", e);
560 /// return Err(e.into());
561 /// }
562 /// }
563 ///
564 /// Ok(())
565 /// }
566 /// ```
567 ///
568 /// # Termination Process
569 ///
570 /// 1. Locates the session associated with the call
571 /// 2. Checks current call state (skips if already terminated)
572 /// 3. Calls session-core to terminate the SIP session
573 /// 4. Updates call state to Terminated
574 /// 5. Updates statistics and emits events
575 ///
576 /// # Idempotent Operation
577 ///
578 /// This method is idempotent - calling it multiple times on the same call
579 /// will not cause errors. If the call is already terminated, it will return
580 /// success immediately.
581 pub async fn hangup_call(&self, call_id: &CallId) -> ClientResult<()> {
582 let session_id = self.session_mapping.get(call_id)
583 .ok_or(ClientError::CallNotFound { call_id: *call_id })?
584 .clone();
585
586 // Check the current call state first
587 if let Some(call_info) = self.call_info.get(call_id) {
588 match call_info.state {
589 crate::call::CallState::Terminated |
590 crate::call::CallState::Failed |
591 crate::call::CallState::Cancelled => {
592 tracing::info!("Call {} is already terminated (state: {:?}), skipping hangup",
593 call_id, call_info.state);
594 return Ok(());
595 }
596 _ => {
597 // Proceed with termination for other states
598 }
599 }
600 }
601
602 // Terminate the session using SessionControl trait
603 match SessionControl::terminate_session(&self.coordinator, &session_id).await {
604 Ok(()) => {
605 tracing::info!("Successfully terminated session for call {}", call_id);
606 }
607 Err(e) => {
608 // Check if the error is because the session is already terminated
609 let error_msg = e.to_string();
610 if error_msg.contains("No INVITE transaction found") ||
611 error_msg.contains("already terminated") ||
612 error_msg.contains("already in state") {
613 tracing::warn!("Session already terminated for call {}: {}", call_id, error_msg);
614 // Continue to update our internal state even if session is already gone
615 } else {
616 return Err(ClientError::CallTerminated {
617 reason: format!("Failed to hangup call: {}", e)
618 });
619 }
620 }
621 }
622
623 // Update call info
624 if let Some(mut call_info) = self.call_info.get_mut(call_id) {
625 let old_state = call_info.state.clone();
626 call_info.state = crate::call::CallState::Terminated;
627 call_info.ended_at = Some(Utc::now());
628 call_info.metadata.insert("hangup_at".to_string(), Utc::now().to_rfc3339());
629 call_info.metadata.insert("hangup_reason".to_string(), "user_hangup".to_string());
630
631 // Emit state change event
632 let _ = self.event_tx.send(crate::events::ClientEvent::CallStateChanged {
633 info: crate::events::CallStatusInfo {
634 call_id: *call_id,
635 new_state: crate::call::CallState::Terminated,
636 previous_state: Some(old_state),
637 reason: Some("User hangup".to_string()),
638 timestamp: Utc::now(),
639 },
640 priority: crate::events::EventPriority::Normal,
641 });
642 }
643
644 // Clean up audio setup state if it exists
645 self.audio_setup_calls.remove(call_id);
646
647 // Update stats - use saturating_sub to prevent integer underflow
648 let mut stats = self.stats.lock().await;
649 stats.connected_calls = stats.connected_calls.saturating_sub(1);
650
651 tracing::info!("Hung up call {}", call_id);
652 Ok(())
653 }
654
655 /// Get basic information about a specific call
656 ///
657 /// Retrieves the current state and metadata for a call by its ID.
658 ///
659 /// # Arguments
660 ///
661 /// * `call_id` - The unique identifier of the call to query
662 ///
663 /// # Returns
664 ///
665 /// Returns a `CallInfo` struct containing all information about the call.
666 ///
667 /// # Errors
668 ///
669 /// * `ClientError::CallNotFound` - If no call exists with the given ID
670 ///
671 /// # Examples
672 ///
673 /// ```rust
674 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId, CallState};
675 ///
676 /// async fn check_call_status(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
677 /// let config = ClientConfig::new()
678 /// .with_sip_addr("127.0.0.1:5066".parse()?);
679 /// let client = ClientManager::new(config).await?;
680 /// client.start().await?;
681 ///
682 /// let call_info = client.get_call(&call_id).await?;
683 ///
684 /// println!("Call ID: {}", call_info.call_id);
685 /// println!("State: {:?}", call_info.state);
686 /// println!("From: {}", call_info.local_uri);
687 /// println!("To: {}", call_info.remote_uri);
688 ///
689 /// if let Some(connected_at) = call_info.connected_at {
690 /// println!("Connected at: {}", connected_at);
691 /// }
692 ///
693 /// match call_info.state {
694 /// CallState::Connected => println!("Call is active"),
695 /// CallState::Terminated => println!("Call has ended"),
696 /// _ => println!("Call is in progress"),
697 /// }
698 ///
699 /// Ok(())
700 /// }
701 /// ```
702 pub async fn get_call(&self, call_id: &CallId) -> ClientResult<CallInfo> {
703 self.call_info.get(call_id)
704 .map(|entry| entry.value().clone())
705 .ok_or(ClientError::CallNotFound { call_id: *call_id })
706 }
707
708 /// Get detailed call information with enhanced metadata
709 ///
710 /// Retrieves comprehensive information about a call including session metadata
711 /// and real-time statistics.
712 ///
713 /// # Arguments
714 ///
715 /// * `call_id` - The unique identifier of the call to query
716 ///
717 /// # Returns
718 ///
719 /// Returns a `CallInfo` struct with additional metadata fields populated.
720 ///
721 /// # Errors
722 ///
723 /// * `ClientError::CallNotFound` - If no call exists with the given ID
724 ///
725 /// # Examples
726 ///
727 /// ```rust
728 /// use rvoip_client_core::{ClientManager, ClientConfig, CallId};
729 ///
730 /// async fn get_detailed_call_info(call_id: CallId) -> Result<(), Box<dyn std::error::Error>> {
731 /// let config = ClientConfig::new()
732 /// .with_sip_addr("127.0.0.1:5067".parse()?);
733 /// let client = ClientManager::new(config).await?;
734 /// client.start().await?;
735 ///
736 /// let detailed_info = client.get_call_detailed(&call_id).await?;
737 ///
738 /// println!("Call Details:");
739 /// println!(" ID: {}", detailed_info.call_id);
740 /// println!(" SIP Call-ID: {}", detailed_info.sip_call_id);
741 ///
742 /// // Check enhanced metadata
743 /// for (key, value) in &detailed_info.metadata {
744 /// println!(" {}: {}", key, value);
745 /// }
746 ///
747 /// if let Some(session_id) = detailed_info.metadata.get("session_id") {
748 /// println!(" Session tracking: {}", session_id);
749 /// }
750 ///
751 /// Ok(())
752 /// }
753 /// ```
754 ///
755 /// # Enhanced Metadata
756 ///
757 /// The detailed call information includes additional metadata fields:
758 ///
759 /// - `session_id` - The internal session-core session identifier
760 /// - `last_updated` - ISO 8601 timestamp of the last metadata update
761 /// - Plus any existing metadata from the basic call info
762 pub async fn get_call_detailed(&self, call_id: &CallId) -> ClientResult<CallInfo> {
763 // Get base call info
764 let mut call_info = self.call_info.get(call_id)
765 .map(|entry| entry.value().clone())
766 .ok_or(ClientError::CallNotFound { call_id: *call_id })?;
767
768 // Add session metadata if available
769 if let Some(session_id) = self.session_mapping.get(call_id) {
770 call_info.metadata.insert("session_id".to_string(), session_id.0.clone());
771 call_info.metadata.insert("last_updated".to_string(), Utc::now().to_rfc3339());
772 }
773
774 Ok(call_info)
775 }
776
777 /// List all calls (active and historical)
778 ///
779 /// Returns a vector of all calls known to the client, regardless of their state.
780 /// This includes active calls, completed calls, and failed calls.
781 ///
782 /// # Returns
783 ///
784 /// Returns a `Vec<CallInfo>` containing all calls. The list is not sorted.
785 ///
786 /// # Examples
787 ///
788 /// ```rust
789 /// use rvoip_client_core::{ClientManager, ClientConfig, CallState};
790 ///
791 /// async fn review_all_calls() -> Result<(), Box<dyn std::error::Error>> {
792 /// let config = ClientConfig::new()
793 /// .with_sip_addr("127.0.0.1:5068".parse()?);
794 /// let client = ClientManager::new(config).await?;
795 /// client.start().await?;
796 ///
797 /// let all_calls = client.list_calls().await;
798 ///
799 /// println!("Total calls: {}", all_calls.len());
800 ///
801 /// for call in all_calls {
802 /// println!("Call {}: {} -> {} ({:?})",
803 /// call.call_id,
804 /// call.local_uri,
805 /// call.remote_uri,
806 /// call.state);
807 /// }
808 ///
809 /// Ok(())
810 /// }
811 /// ```
812 ///
813 /// # Performance Note
814 ///
815 /// This method iterates through all stored calls. For applications with
816 /// many historical calls, consider using filtered methods like
817 /// `get_active_calls()` or `get_calls_by_state()` instead.
818 pub async fn list_calls(&self) -> Vec<CallInfo> {
819 self.call_info.iter()
820 .map(|entry| entry.value().clone())
821 .collect()
822 }
823
824 /// Get calls filtered by state
825 ///
826 /// Returns all calls that are currently in the specified state.
827 ///
828 /// # Arguments
829 ///
830 /// * `state` - The call state to filter by
831 ///
832 /// # Returns
833 ///
834 /// Returns a `Vec<CallInfo>` containing all calls in the specified state.
835 ///
836 /// # Examples
837 ///
838 /// Get all connected calls:
839 /// ```rust
840 /// use rvoip_client_core::{ClientManager, ClientConfig, CallState};
841 ///
842 /// async fn list_connected_calls() -> Result<(), Box<dyn std::error::Error>> {
843 /// let config = ClientConfig::new()
844 /// .with_sip_addr("127.0.0.1:5069".parse()?);
845 /// let client = ClientManager::new(config).await?;
846 /// client.start().await?;
847 ///
848 /// let connected_calls = client.get_calls_by_state(CallState::Connected).await;
849 ///
850 /// println!("Currently connected calls: {}", connected_calls.len());
851 /// for call in connected_calls {
852 /// if let Some(connected_at) = call.connected_at {
853 /// let duration = chrono::Utc::now().signed_duration_since(connected_at);
854 /// println!("Call {}: {} minutes active",
855 /// call.call_id,
856 /// duration.num_minutes());
857 /// }
858 /// }
859 ///
860 /// Ok(())
861 /// }
862 /// ```
863 ///
864 /// Get all failed calls:
865 /// ```rust
866 /// use rvoip_client_core::{ClientManager, ClientConfig, CallState};
867 ///
868 /// async fn review_failed_calls() -> Result<(), Box<dyn std::error::Error>> {
869 /// let config = ClientConfig::new()
870 /// .with_sip_addr("127.0.0.1:5070".parse()?);
871 /// let client = ClientManager::new(config).await?;
872 /// client.start().await?;
873 ///
874 /// let failed_calls = client.get_calls_by_state(CallState::Failed).await;
875 ///
876 /// for call in failed_calls {
877 /// println!("Failed call: {} -> {}", call.local_uri, call.remote_uri);
878 /// if let Some(reason) = call.metadata.get("failure_reason") {
879 /// println!(" Reason: {}", reason);
880 /// }
881 /// }
882 ///
883 /// Ok(())
884 /// }
885 /// ```
886 pub async fn get_calls_by_state(&self, state: crate::call::CallState) -> Vec<CallInfo> {
887 self.call_info.iter()
888 .filter(|entry| entry.value().state == state)
889 .map(|entry| entry.value().clone())
890 .collect()
891 }
892
893 /// Get calls filtered by direction (incoming or outgoing)
894 ///
895 /// Returns all calls that match the specified direction.
896 ///
897 /// # Arguments
898 ///
899 /// * `direction` - The call direction to filter by (`Incoming` or `Outgoing`)
900 ///
901 /// # Returns
902 ///
903 /// Returns a `Vec<CallInfo>` containing all calls with the specified direction.
904 ///
905 /// # Examples
906 ///
907 /// Get all outgoing calls:
908 /// ```rust
909 /// use rvoip_client_core::{ClientManager, ClientConfig, CallDirection};
910 ///
911 /// async fn review_outgoing_calls() -> Result<(), Box<dyn std::error::Error>> {
912 /// let config = ClientConfig::new()
913 /// .with_sip_addr("127.0.0.1:5071".parse()?);
914 /// let client = ClientManager::new(config).await?;
915 /// client.start().await?;
916 ///
917 /// let outgoing_calls = client.get_calls_by_direction(CallDirection::Outgoing).await;
918 ///
919 /// println!("Outgoing calls made: {}", outgoing_calls.len());
920 /// for call in outgoing_calls {
921 /// println!("Called: {} at {}", call.remote_uri, call.created_at);
922 /// }
923 ///
924 /// Ok(())
925 /// }
926 /// ```
927 ///
928 /// Get all incoming calls:
929 /// ```rust
930 /// use rvoip_client_core::{ClientManager, ClientConfig, CallDirection};
931 ///
932 /// async fn review_incoming_calls() -> Result<(), Box<dyn std::error::Error>> {
933 /// let config = ClientConfig::new()
934 /// .with_sip_addr("127.0.0.1:5072".parse()?);
935 /// let client = ClientManager::new(config).await?;
936 /// client.start().await?;
937 ///
938 /// let incoming_calls = client.get_calls_by_direction(CallDirection::Incoming).await;
939 ///
940 /// println!("Calls received: {}", incoming_calls.len());
941 /// for call in incoming_calls {
942 /// println!("From: {} ({})",
943 /// call.remote_display_name.as_deref().unwrap_or("Unknown"),
944 /// call.remote_uri);
945 /// }
946 ///
947 /// Ok(())
948 /// }
949 /// ```
950 pub async fn get_calls_by_direction(&self, direction: CallDirection) -> Vec<CallInfo> {
951 self.call_info.iter()
952 .filter(|entry| entry.value().direction == direction)
953 .map(|entry| entry.value().clone())
954 .collect()
955 }
956
957 /// Get call history (completed and terminated calls)
958 ///
959 /// Returns all calls that have finished, regardless of how they ended.
960 /// This includes successfully completed calls, failed calls, and cancelled calls.
961 ///
962 /// # Returns
963 ///
964 /// Returns a `Vec<CallInfo>` containing all terminated calls.
965 ///
966 /// # Examples
967 ///
968 /// ```rust
969 /// use rvoip_client_core::{ClientManager, ClientConfig, CallState};
970 ///
971 /// async fn generate_call_report() -> Result<(), Box<dyn std::error::Error>> {
972 /// let config = ClientConfig::new()
973 /// .with_sip_addr("127.0.0.1:5073".parse()?);
974 /// let client = ClientManager::new(config).await?;
975 /// client.start().await?;
976 ///
977 /// let history = client.get_call_history().await;
978 ///
979 /// let mut completed = 0;
980 /// let mut failed = 0;
981 /// let mut cancelled = 0;
982 /// let mut total_duration = chrono::Duration::zero();
983 ///
984 /// for call in history {
985 /// match call.state {
986 /// CallState::Terminated => {
987 /// completed += 1;
988 /// if let (Some(connected), Some(ended)) = (call.connected_at, call.ended_at) {
989 /// total_duration = total_duration + ended.signed_duration_since(connected);
990 /// }
991 /// }
992 /// CallState::Failed => failed += 1,
993 /// CallState::Cancelled => cancelled += 1,
994 /// _ => {} // Should not happen in history
995 /// }
996 /// }
997 ///
998 /// println!("Call History Summary:");
999 /// println!(" Completed: {}", completed);
1000 /// println!(" Failed: {}", failed);
1001 /// println!(" Cancelled: {}", cancelled);
1002 /// println!(" Total talk time: {} minutes", total_duration.num_minutes());
1003 ///
1004 /// Ok(())
1005 /// }
1006 /// ```
1007 ///
1008 /// # Use Cases
1009 ///
1010 /// - Call reporting and analytics
1011 /// - Billing and usage tracking
1012 /// - Debugging call quality issues
1013 /// - User activity monitoring
1014 pub async fn get_call_history(&self) -> Vec<CallInfo> {
1015 self.call_info.iter()
1016 .filter(|entry| {
1017 matches!(entry.value().state,
1018 crate::call::CallState::Terminated |
1019 crate::call::CallState::Failed |
1020 crate::call::CallState::Cancelled
1021 )
1022 })
1023 .map(|entry| entry.value().clone())
1024 .collect()
1025 }
1026
1027 /// Get all currently active calls
1028 ///
1029 /// Returns all calls that are not in a terminated state. This includes
1030 /// calls that are connecting, ringing, connected, or in any other non-final state.
1031 ///
1032 /// # Returns
1033 ///
1034 /// Returns a `Vec<CallInfo>` containing all active calls.
1035 ///
1036 /// # Examples
1037 ///
1038 /// Monitor active calls:
1039 /// ```rust
1040 /// use rvoip_client_core::{ClientManager, ClientConfig, CallState};
1041 ///
1042 /// async fn monitor_active_calls() -> Result<(), Box<dyn std::error::Error>> {
1043 /// let config = ClientConfig::new()
1044 /// .with_sip_addr("127.0.0.1:5074".parse()?);
1045 /// let client = ClientManager::new(config).await?;
1046 /// client.start().await?;
1047 ///
1048 /// let active_calls = client.get_active_calls().await;
1049 ///
1050 /// if active_calls.is_empty() {
1051 /// println!("No active calls");
1052 /// } else {
1053 /// println!("Active calls: {}", active_calls.len());
1054 ///
1055 /// for call in active_calls {
1056 /// match call.state {
1057 /// CallState::Initiating => {
1058 /// println!("📞 Dialing {} -> {}", call.local_uri, call.remote_uri);
1059 /// }
1060 /// CallState::Ringing => {
1061 /// println!("📳 Ringing {} -> {}", call.local_uri, call.remote_uri);
1062 /// }
1063 /// CallState::Connected => {
1064 /// if let Some(connected_at) = call.connected_at {
1065 /// let duration = chrono::Utc::now().signed_duration_since(connected_at);
1066 /// println!("☎️ Connected {} -> {} ({}:{})",
1067 /// call.local_uri, call.remote_uri,
1068 /// duration.num_minutes(),
1069 /// duration.num_seconds() % 60);
1070 /// }
1071 /// }
1072 /// _ => {
1073 /// println!("📱 {} -> {} ({:?})", call.local_uri, call.remote_uri, call.state);
1074 /// }
1075 /// }
1076 /// }
1077 /// }
1078 ///
1079 /// Ok(())
1080 /// }
1081 /// ```
1082 ///
1083 /// # Real-time Monitoring
1084 ///
1085 /// This method is useful for:
1086 /// - Dashboard displays showing current call status
1087 /// - Resource management (checking call limits)
1088 /// - User interface updates
1089 /// - Load balancing decisions
1090 pub async fn get_active_calls(&self) -> Vec<CallInfo> {
1091 self.call_info.iter()
1092 .filter(|entry| {
1093 !matches!(entry.value().state,
1094 crate::call::CallState::Terminated |
1095 crate::call::CallState::Failed |
1096 crate::call::CallState::Cancelled
1097 )
1098 })
1099 .map(|entry| entry.value().clone())
1100 .collect()
1101 }
1102
1103 /// Get comprehensive client statistics
1104 ///
1105 /// Returns detailed statistics about the client's call activity and performance.
1106 /// The statistics are recalculated on each call to ensure accuracy.
1107 ///
1108 /// # Returns
1109 ///
1110 /// Returns a `ClientStats` struct containing:
1111 /// - Total number of calls ever made/received
1112 /// - Currently connected calls count
1113 /// - Other performance metrics
1114 ///
1115 /// # Examples
1116 ///
1117 /// Basic statistics display:
1118 /// ```rust
1119 /// use rvoip_client_core::{ClientManager, ClientConfig};
1120 ///
1121 /// async fn display_client_stats() -> Result<(), Box<dyn std::error::Error>> {
1122 /// let config = ClientConfig::new()
1123 /// .with_sip_addr("127.0.0.1:5075".parse()?);
1124 /// let client = ClientManager::new(config).await?;
1125 /// client.start().await?;
1126 ///
1127 /// let stats = client.get_client_stats().await;
1128 ///
1129 /// println!("Client Statistics:");
1130 /// println!(" Total calls: {}", stats.total_calls);
1131 /// println!(" Connected calls: {}", stats.connected_calls);
1132 /// println!(" Utilization: {:.1}%",
1133 /// if stats.total_calls > 0 {
1134 /// (stats.connected_calls as f64 / stats.total_calls as f64) * 100.0
1135 /// } else {
1136 /// 0.0
1137 /// });
1138 ///
1139 /// Ok(())
1140 /// }
1141 /// ```
1142 ///
1143 /// Monitoring loop:
1144 /// ```rust
1145 /// use rvoip_client_core::{ClientManager, ClientConfig};
1146 /// use tokio::time::{interval, Duration};
1147 ///
1148 /// async fn monitor_client_performance() -> Result<(), Box<dyn std::error::Error>> {
1149 /// let config = ClientConfig::new()
1150 /// .with_sip_addr("127.0.0.1:5076".parse()?);
1151 /// let client = ClientManager::new(config).await?;
1152 /// client.start().await?;
1153 ///
1154 /// let mut interval = interval(Duration::from_secs(30));
1155 ///
1156 /// // Monitor for a limited time in doc test
1157 /// for _ in 0..3 {
1158 /// interval.tick().await;
1159 /// let stats = client.get_client_stats().await;
1160 ///
1161 /// println!("📊 Stats: {} total, {} active",
1162 /// stats.total_calls, stats.connected_calls);
1163 ///
1164 /// if stats.connected_calls > 10 {
1165 /// println!("⚠️ High call volume detected");
1166 /// }
1167 /// }
1168 ///
1169 /// Ok(())
1170 /// }
1171 /// ```
1172 ///
1173 /// # Accuracy Guarantee
1174 ///
1175 /// This method recalculates statistics from the actual call states rather than
1176 /// relying on potentially inconsistent counters. This prevents issues with:
1177 /// - Race conditions in concurrent call handling
1178 /// - Integer overflow/underflow
1179 /// - Inconsistent state after error recovery
1180 ///
1181 /// # Performance Note
1182 ///
1183 /// The recalculation involves iterating through all calls, so for applications
1184 /// with very large call histories, consider calling this method judiciously.
1185 pub async fn get_client_stats(&self) -> ClientStats {
1186 let mut stats = self.stats.lock().await.clone();
1187
1188 // Always recalculate call counts from actual call states to avoid counter bugs
1189 // This prevents integer overflow/underflow issues from race conditions
1190 let _active_calls = self.get_active_calls().await;
1191 let connected_calls = self.get_calls_by_state(crate::call::CallState::Connected).await;
1192
1193 // Use actual counts instead of potentially corrupted stored counters
1194 stats.connected_calls = connected_calls.len();
1195 stats.total_calls = self.call_info.len();
1196
1197 // Ensure connected_calls never exceeds total_calls (defensive programming)
1198 if stats.connected_calls > stats.total_calls {
1199 tracing::warn!("Connected calls ({}) exceeded total calls ({}), correcting to total",
1200 stats.connected_calls, stats.total_calls);
1201 stats.connected_calls = stats.total_calls;
1202 }
1203
1204 stats
1205 }
1206}