active_call/call/
mod.rs

1use serde::{Deserialize, Serialize};
2use serde_with::skip_serializing_none;
3use std::{
4    collections::HashMap,
5    sync::{Arc, Mutex},
6};
7use voice_engine::{
8    CallOption, ReferOption, media::recorder::RecorderOption, synthesis::SynthesisOption,
9};
10
11pub mod active_call;
12pub mod sip;
13pub use active_call::ActiveCall;
14pub use active_call::ActiveCallRef;
15pub use active_call::ActiveCallType;
16
17pub type CommandSender = tokio::sync::broadcast::Sender<Command>;
18pub type CommandReceiver = tokio::sync::broadcast::Receiver<Command>;
19
20// WebSocket Commands
21#[skip_serializing_none]
22#[derive(Debug, Deserialize, Serialize, Clone)]
23#[serde(
24    tag = "command",
25    rename_all = "camelCase",
26    rename_all_fields = "camelCase"
27)]
28pub enum Command {
29    Invite {
30        option: CallOption,
31    },
32    Accept {
33        option: CallOption,
34    },
35    Reject {
36        reason: String,
37        code: Option<u32>,
38    },
39    Ringing {
40        recorder: Option<RecorderOption>,
41        early_media: Option<bool>,
42        ringtone: Option<String>,
43    },
44    Tts {
45        text: String,
46        speaker: Option<String>,
47        /// If the play_id is the same, it will not interrupt the previous playback
48        play_id: Option<String>,
49        /// If auto_hangup is true, it means the call will be hung up automatically after the TTS playback is finished
50        auto_hangup: Option<bool>,
51        /// If streaming is true, it means the input text is streaming text,
52        /// and end_of_stream needs to be used to determine if it's finished,
53        /// equivalent to LLM's streaming output to TTS synthesis
54        streaming: Option<bool>,
55        /// If end_of_stream is true, it means the input text is finished
56        end_of_stream: Option<bool>,
57        option: Option<SynthesisOption>,
58        wait_input_timeout: Option<u32>,
59        /// if true, the text is base64 encoded pcm samples
60        base64: Option<bool>,
61    },
62    Play {
63        url: String,
64        play_id: Option<String>,
65        auto_hangup: Option<bool>,
66        wait_input_timeout: Option<u32>,
67    },
68    Interrupt {
69        graceful: Option<bool>,
70    },
71    Pause {},
72    Resume {},
73    Hangup {
74        reason: Option<String>,
75        initiator: Option<String>,
76    },
77    Refer {
78        caller: String,
79        /// aor of the calee, e.g., sip:bob@restsend.com
80        callee: String,
81        options: Option<ReferOption>,
82    },
83    Mute {
84        track_id: Option<String>,
85    },
86    Unmute {
87        track_id: Option<String>,
88    },
89    History {
90        speaker: String,
91        text: String,
92    },
93}
94
95/// Routing state for managing stateful load balancing
96#[derive(Debug)]
97pub struct RoutingState {
98    /// Round-robin counters for each destination group
99    round_robin_counters: Arc<Mutex<HashMap<String, usize>>>,
100}
101
102impl Default for RoutingState {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl RoutingState {
109    pub fn new() -> Self {
110        Self {
111            round_robin_counters: Arc::new(Mutex::new(HashMap::new())),
112        }
113    }
114
115    /// Get the next trunk index for round-robin selection
116    pub fn next_round_robin_index(&self, destination_key: &str, trunk_count: usize) -> usize {
117        if trunk_count == 0 {
118            return 0;
119        }
120
121        let mut counters = self.round_robin_counters.lock().unwrap();
122        let counter = counters
123            .entry(destination_key.to_string())
124            .or_insert_with(|| 0);
125        let r = *counter % trunk_count;
126        *counter += 1;
127        return r;
128    }
129}