flowcat_core/serializer/
plivo.rs1use base64::Engine;
27use serde_json::{json, Value};
28
29use crate::codec::{pcm16_to_ulaw, ulaw_to_pcm16};
30use crate::serializer::MediaSerializer;
31use crate::types::{AudioChunk, SerIn, WsIn, WsOut};
32
33#[derive(Debug, Default)]
35pub struct PlivoSerializer {
36 rate: u32,
38 stream_id: Option<String>,
41}
42
43impl PlivoSerializer {
44 pub fn new(rate: u32) -> Self {
46 Self {
47 rate,
48 stream_id: None,
49 }
50 }
51
52 pub fn stream_id(&self) -> Option<&str> {
54 self.stream_id.as_deref()
55 }
56}
57
58impl MediaSerializer for PlivoSerializer {
59 fn on_message(&mut self, msg: &WsIn) -> SerIn {
60 let text = match msg {
62 WsIn::Text(t) => t,
63 WsIn::Close => return SerIn::Stop,
64 WsIn::Binary(_) => return SerIn::Ignore,
65 };
66
67 let v: Value = match serde_json::from_str(text) {
68 Ok(v) => v,
69 Err(_) => return SerIn::Ignore,
71 };
72
73 match v.get("event").and_then(Value::as_str) {
74 Some("start") => {
75 let start = v.get("start").cloned().unwrap_or(Value::Null);
76 let stream_id = start
77 .get("streamId")
78 .and_then(Value::as_str)
79 .map(str::to_owned);
80 let call_id = start
83 .get("callId")
84 .and_then(Value::as_str)
85 .map(str::to_owned)
86 .or_else(|| stream_id.clone())
87 .unwrap_or_default();
88 self.stream_id = stream_id.clone();
89 SerIn::StreamStart { call_id, stream_id }
90 }
91 Some("media") => {
92 let payload = v
93 .get("media")
94 .and_then(|m| m.get("payload"))
95 .and_then(Value::as_str);
96 match payload {
97 Some(b64) => match base64::engine::general_purpose::STANDARD.decode(b64) {
98 Ok(ulaw) => SerIn::Audio(AudioChunk {
99 pcm: ulaw_to_pcm16(&ulaw),
100 sample_rate: self.rate,
101 }),
102 Err(_) => SerIn::Ignore,
103 },
104 None => SerIn::Ignore,
105 }
106 }
107 Some("stop") => SerIn::Stop,
108 _ => SerIn::Ignore,
110 }
111 }
112
113 fn encode_audio(&self, chunk: &AudioChunk) -> WsOut {
114 let ulaw = pcm16_to_ulaw(&chunk.pcm);
119 let payload = base64::engine::general_purpose::STANDARD.encode(ulaw);
120 let mut answer = json!({
121 "event": "playAudio",
122 "media": {
123 "contentType": "audio/x-mulaw",
124 "sampleRate": self.rate,
125 "payload": payload,
126 },
127 });
128 if let Some(sid) = &self.stream_id {
130 answer["streamId"] = Value::String(sid.clone());
131 }
132 WsOut::Text(answer.to_string())
133 }
134
135 fn encode_clear(&self) -> Option<WsOut> {
136 let mut answer = json!({ "event": "clearAudio" });
138 if let Some(sid) = &self.stream_id {
139 answer["streamId"] = Value::String(sid.clone());
140 }
141 Some(WsOut::Text(answer.to_string()))
142 }
143
144 fn carrier_rate(&self) -> u32 {
145 self.rate
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 fn b64(bytes: &[u8]) -> String {
154 base64::engine::general_purpose::STANDARD.encode(bytes)
155 }
156
157 #[test]
158 fn start_event_parses_to_stream_start_with_ids() {
159 let mut s = PlivoSerializer::new(8000);
160 let frame = WsIn::Text(
161 json!({
162 "event": "start",
163 "start": {
164 "streamId": "strm-123",
165 "callId": "call-abc",
166 "mediaFormat": {"encoding": "audio/x-mulaw", "sampleRate": 8000}
167 }
168 })
169 .to_string(),
170 );
171 match s.on_message(&frame) {
172 SerIn::StreamStart { call_id, stream_id } => {
173 assert_eq!(call_id, "call-abc");
174 assert_eq!(stream_id.as_deref(), Some("strm-123"));
175 }
176 other => panic!("expected StreamStart, got {other:?}"),
177 }
178 assert_eq!(s.stream_id(), Some("strm-123"));
180 }
181
182 #[test]
183 fn media_event_base64_ulaw_decodes_to_pcm() {
184 let mut s = PlivoSerializer::new(8000);
185 let ulaw_in = vec![0xFFu8, 0x00, 0x80, 0x7F];
189 let expected_pcm = ulaw_to_pcm16(&ulaw_in);
190 let frame =
191 WsIn::Text(json!({"event": "media", "media": {"payload": b64(&ulaw_in)}}).to_string());
192 match s.on_message(&frame) {
193 SerIn::Audio(chunk) => {
194 assert_eq!(chunk.sample_rate, 8000);
195 assert_eq!(chunk.pcm.len(), ulaw_in.len());
197 assert_eq!(chunk.pcm, expected_pcm);
198 }
199 other => panic!("expected Audio, got {other:?}"),
200 }
201 }
202
203 #[test]
204 fn stop_event_and_close_map_to_stop() {
205 let mut s = PlivoSerializer::new(8000);
206 assert!(matches!(
207 s.on_message(&WsIn::Text(json!({"event": "stop"}).to_string())),
208 SerIn::Stop
209 ));
210 assert!(matches!(s.on_message(&WsIn::Close), SerIn::Stop));
211 }
212
213 #[test]
214 fn unknown_and_malformed_frames_are_ignored() {
215 let mut s = PlivoSerializer::new(8000);
216 assert!(matches!(
217 s.on_message(&WsIn::Text(json!({"event": "dtmf"}).to_string())),
218 SerIn::Ignore
219 ));
220 assert!(matches!(
221 s.on_message(&WsIn::Text("not json".to_string())),
222 SerIn::Ignore
223 ));
224 assert!(matches!(
225 s.on_message(&WsIn::Binary(vec![1, 2, 3])),
226 SerIn::Ignore
227 ));
228 }
229
230 #[test]
231 fn encode_audio_has_plivo_keys_and_round_trippable_payload() {
232 let mut s = PlivoSerializer::new(8000);
233 s.on_message(&WsIn::Text(
235 json!({"event": "start", "start": {"streamId": "S1", "callId": "C1"}}).to_string(),
236 ));
237
238 let pcm = vec![0i16, 100, -100, 32000, -32000];
239 let out = s.encode_audio(&AudioChunk {
240 pcm: pcm.clone(),
241 sample_rate: 8000,
242 });
243 let text = match out {
244 WsOut::Text(t) => t,
245 other => panic!("expected Text, got {other:?}"),
246 };
247 let v: Value = serde_json::from_str(&text).unwrap();
248 assert_eq!(v["event"], "playAudio");
249 assert_eq!(v["media"]["contentType"], "audio/x-mulaw");
250 assert_eq!(v["media"]["sampleRate"], 8000);
251 assert_eq!(v["streamId"], "S1");
252
253 let payload = v["media"]["payload"].as_str().unwrap();
255 let ulaw = base64::engine::general_purpose::STANDARD
256 .decode(payload)
257 .unwrap();
258 assert_eq!(ulaw, pcm16_to_ulaw(&pcm));
259 assert_eq!(ulaw_to_pcm16(&ulaw).len(), pcm.len());
261 }
262
263 #[test]
264 fn encode_clear_is_plivo_clear_audio() {
265 let mut s = PlivoSerializer::new(8000);
266 s.on_message(&WsIn::Text(
267 json!({"event": "start", "start": {"streamId": "S9"}}).to_string(),
268 ));
269 let out = s.encode_clear().expect("plivo supports clearAudio");
270 let text = match out {
271 WsOut::Text(t) => t,
272 other => panic!("expected Text, got {other:?}"),
273 };
274 let v: Value = serde_json::from_str(&text).unwrap();
275 assert_eq!(v["event"], "clearAudio");
276 assert_eq!(v["streamId"], "S9");
277 }
278}