Skip to main content

ringo_core/
event.rs

1use std::collections::HashMap;
2
3/// Headers of each INVITE seen in a trace, keyed by SIP `Call-ID`. First
4/// INVITE per Call-ID wins (the call-establishing one).
5pub type InviteHeaders = HashMap<String, Vec<(String, String)>>;
6
7/// RTP media quality for a call (backend-neutral). `mos` is an estimated Mean
8/// Opinion Score (1.0 = worst … 4.5 = best).
9#[derive(Debug, Clone, Copy)]
10pub struct MediaStats {
11    /// Round-trip time in milliseconds.
12    pub rtt_ms: f64,
13    /// Receive-side inter-arrival jitter in milliseconds.
14    pub jitter_ms: f64,
15    /// Cumulative received RTP packets lost.
16    pub rx_lost: i32,
17    /// Receive-side packet loss as a percentage.
18    pub packet_loss_pct: f64,
19    /// Estimated MOS (1.0–4.5).
20    pub mos: f64,
21}
22
23/// The negotiated audio codec on a call (backend-neutral).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct CodecInfo {
26    /// Codec name as the backend reports it, e.g. `"opus"`, `"PCMU"`.
27    pub name: String,
28    /// Codec sample rate in Hz.
29    pub srate: u32,
30    /// Channel count (1 = mono, 2 = stereo).
31    pub ch: u8,
32}
33
34#[derive(Debug, Clone)]
35pub enum AppEvent {
36    Registering {
37        account: String,
38    },
39    RegisterOk {
40        account: String,
41    },
42    RegisterFailed {
43        reason: String,
44    },
45    Unregistered {
46        account: String,
47    },
48    CallIncoming {
49        call_id: String,
50        number: String,
51        display_name: Option<String>,
52    },
53    CallOutgoing {
54        call_id: String,
55        number: String,
56    },
57    CallRinging {
58        call_id: String,
59    },
60    CallEstablished {
61        call_id: String,
62    },
63    CallClosed {
64        call_id: String,
65        reason: String,
66        error: bool,
67    },
68    CallDeflected {
69        from: String,
70        display_name: Option<String>,
71        target: String,
72    },
73    /// The remote party put this call on hold (the call stays up, media
74    /// pauses). Distinct from a local hold: the peer initiated it.
75    CallHold {
76        call_id: String,
77    },
78    /// The remote party resumed a call it had put on hold.
79    CallResume {
80        call_id: String,
81    },
82    /// A transfer this side initiated failed (the original call, if still
83    /// up, is unaffected).
84    CallTransferFailed {
85        call_id: String,
86    },
87    VoicemailStatus {
88        waiting: bool,
89        new_count: u32,
90    },
91    Response {
92        ok: bool,
93        data: String,
94    },
95    Unknown {
96        class: String,
97        type_: String,
98    },
99    BackendConnectFailed {
100        reason: String,
101    },
102}
103
104/// Whether a call-closed reason indicates an error (not a normal close).
105/// Backend-neutral: shared by all backends that produce SIP reason strings.
106pub fn is_error_reason(reason: &str) -> bool {
107    if reason.is_empty() {
108        return false;
109    }
110    const NORMAL: &[&str] = &[
111        "Connection reset by peer",
112        "Connection closed",
113        "Rejected by user",
114        "Call transfered",
115    ];
116    !NORMAL
117        .iter()
118        .any(|n| reason.to_lowercase().starts_with(&n.to_lowercase()))
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    // ── is_error_reason ────────────────────────────────────────────────────────
126
127    #[test]
128    fn empty_reason_is_not_error() {
129        assert!(!is_error_reason(""));
130    }
131
132    #[test]
133    fn connection_reset_is_not_error() {
134        assert!(!is_error_reason("Connection reset by peer"));
135    }
136
137    #[test]
138    fn connection_reset_with_errno_is_not_error() {
139        assert!(!is_error_reason("Connection reset by peer [104]"));
140    }
141
142    #[test]
143    fn connection_closed_is_not_error() {
144        assert!(!is_error_reason("Connection closed"));
145    }
146
147    #[test]
148    fn rejected_by_user_is_not_error() {
149        assert!(!is_error_reason("Rejected by user"));
150    }
151
152    #[test]
153    fn sip_busy_is_error() {
154        assert!(is_error_reason("486 Busy Here"));
155    }
156
157    #[test]
158    fn sip_not_found_is_error() {
159        assert!(is_error_reason("404 Not Found"));
160    }
161}