1use crate::media::cache;
2use crate::synthesis::{SynthesisEvent, SynthesisOption};
3use crate::{
4 app::AppState,
5 call::{
6 ActiveCall, ActiveCallType, Command,
7 active_call::{ActiveCallGuard, CallParams},
8 },
9 handler::playbook,
10 playbook::{Playbook, PlaybookRunner},
11};
12use crate::{event::SessionEvent, media::track::TrackConfig};
13use axum::{
14 Json, Router,
15 extract::{Path, Query, State, WebSocketUpgrade, ws::Message},
16 response::sse::{Event, KeepAlive, Sse},
17 response::{IntoResponse, Response},
18 routing::{get, post},
19};
20use bytes::Bytes;
21use chrono::Utc;
22use futures::{SinkExt, StreamExt};
23use rustrtc::IceServer;
24use serde_json::json;
25use std::collections::HashMap;
26use std::{path::PathBuf, sync::Arc, time::Duration};
27use tokio::{join, select};
28use tokio_util::sync::CancellationToken;
29use tracing::{debug, info, trace, warn};
30use uuid::Uuid;
31
32fn filter_headers(
33 extras: &mut std::collections::HashMap<String, serde_json::Value>,
34 allowed_headers: &[String],
35) {
36 extras.retain(|k, _| allowed_headers.iter().any(|h| h.eq_ignore_ascii_case(k)));
37}
38
39pub fn call_router() -> Router<AppState> {
40 let r = Router::new()
41 .route("/call", get(ws_handler))
42 .route("/call/webrtc", get(webrtc_handler))
43 .route("/call/sip", get(sip_handler))
44 .route("/list", get(list_active_calls))
45 .route("/kill/{id}", get(kill_active_call))
46 .route("/events/{id}", get(stream_events))
47 .route("/command/{id}", post(send_command))
48 .route("/precache", post(precache));
49 r
50}
51
52pub fn iceservers_router() -> Router<AppState> {
53 let r = Router::new();
54 r.route("/iceservers", get(get_iceservers))
55}
56
57pub fn playbook_router() -> Router<AppState> {
58 Router::new()
59 .route("/api/playbooks", get(playbook::list_playbooks))
60 .route(
61 "/api/playbooks/{name}",
62 get(playbook::get_playbook).post(playbook::save_playbook),
63 )
64 .route(
65 "/api/playbook/run",
66 axum::routing::post(playbook::run_playbook),
67 )
68 .route("/api/records", get(playbook::list_records))
69}
70
71pub async fn ws_handler(
72 ws: WebSocketUpgrade,
73 State(state): State<AppState>,
74 Query(params): Query<CallParams>,
75) -> Response {
76 call_handler(ActiveCallType::WebSocket, ws, state, params).await
77}
78
79pub async fn sip_handler(
80 ws: WebSocketUpgrade,
81 State(state): State<AppState>,
82 Query(params): Query<CallParams>,
83) -> Response {
84 call_handler(ActiveCallType::Sip, ws, state, params).await
85}
86
87pub async fn webrtc_handler(
88 ws: WebSocketUpgrade,
89 State(state): State<AppState>,
90 Query(params): Query<CallParams>,
91) -> Response {
92 call_handler(ActiveCallType::Webrtc, ws, state, params).await
93}
94
95pub async fn call_handler_core(
102 call_type: ActiveCallType,
103 session_id: String,
104 app_state: AppState,
105 cancel_token: CancellationToken,
106 audio_receiver: tokio::sync::mpsc::UnboundedReceiver<Bytes>,
107 server_side_track: Option<String>,
108 dump_events: bool,
109 ping_interval: u64,
110 mut command_receiver: tokio::sync::mpsc::UnboundedReceiver<Command>,
111 event_sender_to_client: tokio::sync::mpsc::UnboundedSender<crate::event::SessionEvent>,
112 extras: Option<HashMap<String, serde_json::Value>>,
113 playbook_name: Option<String>,
114) -> Option<HashMap<String, serde_json::Value>> {
115 let _cancel_guard = cancel_token.clone().drop_guard();
116 let track_config = TrackConfig::default();
117
118 let active_call = Arc::new(ActiveCall::new(
119 call_type.clone(),
120 cancel_token.clone(),
121 session_id.clone(),
122 app_state.invitation.clone(),
123 app_state.clone(),
124 track_config,
125 Some(audio_receiver),
126 dump_events,
127 server_side_track,
128 extras,
129 None,
130 ));
131
132 {
135 let name_or_content = playbook_name.or_else(|| {
136 app_state
137 .pending_playbooks
138 .try_lock()
139 .ok()
140 .and_then(|mut pending| pending.remove(&session_id).map(|(val, _)| val))
141 });
142 if let Some(name_or_content) = name_or_content {
143 let playbook_result = if name_or_content.trim().starts_with("---") {
144 Playbook::parse(&name_or_content)
145 } else {
146 let path = if name_or_content.starts_with("config/playbook/") {
148 PathBuf::from(&name_or_content)
149 } else {
150 PathBuf::from("config/playbook").join(&name_or_content)
151 };
152 Playbook::load(path).await
153 };
154
155 match playbook_result {
156 Ok(mut playbook) => {
157 if call_type == ActiveCallType::Sip {
159 if let Some(sip_config) = &playbook.config.sip {
160 if let Some(allowed_headers) = &sip_config.extract_headers {
161 let mut state = active_call.call_state.write().await;
162 if let Some(extras) = &mut state.extras {
163 filter_headers(extras, allowed_headers);
164 let header_keys: Vec<String> = extras
166 .keys()
167 .filter(|k| !k.starts_with('_'))
168 .cloned()
169 .collect();
170 extras.insert(
171 "_sip_header_keys".to_string(),
172 serde_json::to_value(&header_keys).unwrap_or_default(),
173 );
174 if let Ok(result) = playbook.render(extras) {
175 playbook = result;
176 }
177 }
178 }
179 }
180 }
181
182 match PlaybookRunner::new(playbook, active_call.clone()) {
183 Ok(runner) => {
184 crate::spawn(async move {
185 runner.run().await;
186 });
187 let display_name = if name_or_content.trim().starts_with("---") {
188 "custom content"
189 } else {
190 &name_or_content
191 };
192 info!(session_id, "Playbook runner started for {}", display_name);
193 }
194 Err(e) => {
195 let display_name = if name_or_content.trim().starts_with("---") {
196 "custom content"
197 } else {
198 &name_or_content
199 };
200 warn!(
201 session_id,
202 "Failed to create runner {}: {}", display_name, e
203 )
204 }
205 }
206 }
207 Err(e) => {
208 let display_name = if name_or_content.trim().starts_with("---") {
209 "custom content"
210 } else {
211 &name_or_content
212 };
213 warn!(
214 session_id,
215 "Failed to load playbook {}: {}", display_name, e
216 );
217 let event = SessionEvent::Error {
218 timestamp: crate::media::get_timestamp(),
219 track_id: session_id.clone(),
220 sender: "playbook".to_string(),
221 error: format!("{}", e),
222 code: None,
223 };
224 event_sender_to_client.send(event).ok();
225 return None;
226 }
227 }
228 }
229 }
230
231 let recv_commands_loop = async {
232 while let Some(command) = command_receiver.recv().await {
233 if let Err(_) = active_call.enqueue_command(command).await {
234 break;
235 }
236 }
237 };
238
239 let mut event_receiver = active_call.event_sender.subscribe();
240 let send_events_loop = async {
241 loop {
242 match event_receiver.recv().await {
243 Ok(event) => {
244 if let Err(_) = event_sender_to_client.send(event) {
245 break;
246 }
247 }
248 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
249 Err(_) => break,
250 }
251 }
252 };
253
254 let send_ping_loop = async {
255 if ping_interval == 0 {
256 active_call.cancel_token.cancelled().await;
257 return;
258 }
259 let mut ticker = tokio::time::interval(Duration::from_secs(ping_interval));
260 loop {
261 ticker.tick().await;
262 let payload = Utc::now().to_rfc3339();
263 let event = SessionEvent::Ping {
264 timestamp: crate::media::get_timestamp(),
265 payload: Some(payload),
266 };
267 if let Err(_) = active_call.event_sender.send(event) {
268 break;
269 }
270 }
271 };
272
273 let guard = ActiveCallGuard::new(active_call.clone());
274 info!(
275 session_id,
276 active_calls = guard.active_calls,
277 ?call_type,
278 "new call started"
279 );
280 let receiver = active_call.new_receiver();
281
282 let (r, _) = join! {
283 active_call.serve(receiver),
284 async {
285 select!{
286 _ = send_ping_loop => {},
287 _ = cancel_token.cancelled() => {},
288 _ = send_events_loop => { },
289 _ = recv_commands_loop => {
290 info!(session_id, "Command receiver closed");
291 },
292 }
293 cancel_token.cancel();
294 }
295 };
296 while let Ok(event) = event_receiver.try_recv() {
298 if let Err(_) = event_sender_to_client.send(event) {
299 break;
300 }
301 }
302 match r {
303 Ok(_) => info!(session_id, "call ended successfully"),
304 Err(e) => warn!(session_id, "call ended with error: {}", e),
305 }
306
307 let final_extras = active_call.call_state.read().await.extras.clone();
309
310 active_call.cleanup().await.ok();
311 debug!(session_id, "Call handler core completed");
312
313 final_extras
314}
315
316pub async fn call_handler(
317 call_type: ActiveCallType,
318 ws: WebSocketUpgrade,
319 app_state: AppState,
320 params: CallParams,
321) -> Response {
322 let has_explicit_id = params.id.is_some();
323 let session_id = params
324 .id
325 .clone()
326 .unwrap_or_else(|| format!("s.{}", Uuid::new_v4().to_string()));
327 let server_side_track = params.server_side_track.clone();
328 let dump_events = params.dump_events.unwrap_or(true);
329 let ping_interval = params.ping_interval.unwrap_or(20);
330
331 if has_explicit_id {
335 let found_locally = {
336 let active_calls = app_state.active_calls.lock().unwrap();
337 active_calls.contains_key(&session_id)
338 };
339 if !found_locally {
340 if params.forward.is_none() {
341 if let Some(peer_ws) =
342 crate::handler::peer::try_forward(&app_state, &session_id, ¶ms).await
343 {
344 info!(session_id, "forwarding websocket to peer hosting the call");
345 return ws.on_upgrade(move |socket| async move {
346 crate::handler::peer::tunnel(socket, peer_ws).await;
347 });
348 }
349 }
350 if params.forward == Some(true) {
351 warn!(
352 session_id,
353 "call not found on this node, rejecting forwarded request"
354 );
355 return (
356 axum::http::StatusCode::NOT_FOUND,
357 "call not found on this node",
358 )
359 .into_response();
360 }
361 }
362 }
363
364 let resp = ws.on_upgrade(move |socket| async move {
365 let (mut ws_sender, mut ws_receiver) = socket.split();
366 let (audio_sender, audio_receiver) = tokio::sync::mpsc::unbounded_channel::<Bytes>();
367 let (command_sender, command_receiver) = tokio::sync::mpsc::unbounded_channel::<Command>();
368 let (event_sender_to_client, mut event_receiver_from_core) =
369 tokio::sync::mpsc::unbounded_channel::<crate::event::SessionEvent>();
370 let cancel_token = CancellationToken::new();
371
372 let session_id_clone = session_id.clone();
374 let app_state_clone = app_state.clone();
375 let cancel_token_clone = cancel_token.clone();
376 crate::spawn(async move {
377 call_handler_core(
378 call_type,
379 session_id_clone,
380 app_state_clone,
381 cancel_token_clone,
382 audio_receiver,
383 server_side_track,
384 dump_events,
385 ping_interval.into(),
386 command_receiver,
387 event_sender_to_client,
388 None, None, )
391 .await;
392 });
393
394 let recv_from_ws_loop = async {
396 while let Some(Ok(message)) = ws_receiver.next().await {
397 match message {
398 Message::Text(text) => {
399 let command = match serde_json::from_str::<Command>(&text) {
400 Ok(cmd) => cmd,
401 Err(e) => {
402 warn!(session_id, %text, "Failed to parse command {}", e);
403 continue;
404 }
405 };
406 if let Err(_) = command_sender.send(command) {
407 break;
408 }
409 }
410 Message::Binary(bin) => {
411 audio_sender.send(bin.into()).ok();
412 }
413 Message::Close(_) => {
414 info!(session_id, "WebSocket closed by client");
415 break;
416 }
417 _ => {}
418 }
419 }
420 };
421
422 let send_to_ws_loop = async {
423 while let Some(event) = event_receiver_from_core.recv().await {
424 trace!(session_id, %event, "Sending WS message");
425 let message = match event.into_ws_message() {
426 Ok(msg) => msg,
427 Err(e) => {
428 warn!(session_id, error=%e, "Failed to serialize event to WS message");
429 continue;
430 }
431 };
432 if let Err(_) = ws_sender.send(message).await {
433 info!(session_id, "WebSocket send failed, closing");
434 break;
435 }
436 }
437 };
438
439 select! {
440 _ = recv_from_ws_loop => {
441 info!(session_id, "WebSocket receive loop ended");
442 },
443 _ = send_to_ws_loop => {
444 info!(session_id, "WebSocket send loop ended");
445 },
446 }
447
448 cancel_token.cancel();
449 ws_sender.flush().await.ok();
450 ws_sender.close().await.ok();
451 debug!(session_id, "WebSocket connection closed");
452 });
453 resp
454}
455
456pub(crate) async fn get_iceservers(State(state): State<AppState>) -> Response {
457 if let Some(ice_servers) = state.config.ice_servers.as_ref() {
458 return Json(ice_servers).into_response();
459 }
460 Json(vec![IceServer {
461 urls: vec!["stun:stun.l.google.com:19302".to_string()],
462 ..Default::default()
463 }])
464 .into_response()
465}
466
467pub(crate) async fn list_active_calls(State(state): State<AppState>) -> Response {
468 let calls = state
469 .active_calls
470 .lock()
471 .unwrap()
472 .iter()
473 .map(|(_, c)| {
474 if let Ok(cs) = c.call_state.try_read() {
475 json!({
476 "id": c.session_id,
477 "callType": c.call_type,
478 "cs.option": cs.option,
479 "ringTime": cs.ring_time,
480 "startTime": cs.answer_time,
481 })
482 } else {
483 json!({
484 "id": c.session_id,
485 "callType": c.call_type,
486 "status": "locked",
487 })
488 }
489 })
490 .collect::<Vec<_>>();
491 Json(serde_json::json!({ "active_calls": calls })).into_response()
492}
493
494pub(crate) async fn kill_active_call(
495 Path(id): Path<String>,
496 State(state): State<AppState>,
497) -> Response {
498 let active_calls = state.active_calls.lock().unwrap();
499 if let Some(call) = active_calls.get(&id) {
500 call.cancel_token.cancel();
501 Json(serde_json::json!({ "status": "killed", "id": id })).into_response()
502 } else {
503 (
504 axum::http::StatusCode::NOT_FOUND,
505 Json(serde_json::json!({ "status": "not_found", "id": id })),
506 )
507 .into_response()
508 }
509}
510
511pub(crate) async fn stream_events(
512 Path(id): Path<String>,
513 State(state): State<AppState>,
514) -> Response {
515 let mut rx_events;
516 let mut rx_commands;
517 {
518 let active_calls = state.active_calls.lock().unwrap();
519 if let Some(call) = active_calls.get(&id) {
520 rx_events = call.event_sender.subscribe();
521 rx_commands = call.cmd_sender.subscribe();
522 } else {
523 return (axum::http::StatusCode::NOT_FOUND, "track not active").into_response();
524 }
525 }
526
527 let stream = async_stream::stream! {
528 loop {
529 let result = tokio::select! {
530 r = rx_events.recv() => r.map(|e| serde_json::to_string(&e).map(|json| Event::default().event("event").data(json))),
531 r = rx_commands.recv() => r.map(|c| serde_json::to_string(&c).map(|json| Event::default().event("command").data(json))),
532 };
533 match result {
534 Ok(Ok(sse_event)) => yield Ok::<Event, serde_json::Error>(sse_event),
535 Ok(Err(e)) => yield Err(e.into()),
536 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
537 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
538 }
539 }
540 };
541
542 let mut response = Sse::new(stream)
543 .keep_alive(KeepAlive::default())
544 .into_response();
545 response.headers_mut().insert(
546 axum::http::header::CONTENT_TYPE,
547 "text/event-stream;charset=utf-8".parse().unwrap(),
548 );
549 response
550}
551
552pub(crate) async fn send_command(
553 Path(id): Path<String>,
554 State(state): State<AppState>,
555 Json(command): Json<Command>,
556) -> Response {
557 let active_calls = state.active_calls.lock().unwrap();
558 if let Some(call) = active_calls.get(&id) {
559 if let Ok(_) = call.cmd_sender.send(command) {
560 return Json(serde_json::json!({ "status": "sent", "id": id })).into_response();
561 }
562 }
563
564 (
565 axum::http::StatusCode::NOT_FOUND,
566 Json(serde_json::json!({ "status": "not_found", "id": id })),
567 )
568 .into_response()
569}
570
571pub(crate) async fn precache(
582 State(state): State<AppState>,
583 Json(command): Json<Command>,
584) -> Response {
585 let result = match command {
586 Command::Tts {
587 text,
588 speaker,
589 option,
590 cache_key,
591 ..
592 } => precache_tts(&state, text, speaker, option, cache_key).await,
593 _ => {
594 return (
595 axum::http::StatusCode::BAD_REQUEST,
596 Json(json!({ "status": "error", "error": "precache only accepts tts commands" })),
597 )
598 .into_response();
599 }
600 };
601
602 match result {
603 Ok((cache_key, bytes, existed)) => Json(json!({
604 "status": if existed { "exists" } else { "cached" },
605 "cacheKey": cache_key,
606 "bytes": bytes,
607 }))
608 .into_response(),
609 Err(e) => (
610 axum::http::StatusCode::INTERNAL_SERVER_ERROR,
611 Json(json!({ "status": "error", "error": e.to_string() })),
612 )
613 .into_response(),
614 }
615}
616
617async fn precache_tts(
620 state: &AppState,
621 text: String,
622 speaker: Option<String>,
623 option: Option<SynthesisOption>,
624 cache_key: Option<String>,
625) -> anyhow::Result<(String, usize, bool)> {
626 let mut opt = option.ok_or_else(|| anyhow::anyhow!("tts precache requires an option"))?;
627 opt.speaker = speaker.or(opt.speaker);
629 opt.check_default();
630
631 let mut client = state.stream_engine.create_tts_client(false, &opt).await?;
632 let sample_rate = opt.samplerate.unwrap_or(16000) as u32;
634 let cache_key = cache_key.unwrap_or_else(|| {
635 cache::generate_cache_key(
636 &format!("tts:{}{}", client.provider(), text),
637 sample_rate,
638 opt.speaker.as_ref(),
639 opt.speed,
640 )
641 });
642 if cache::is_cached(&cache_key).await.unwrap_or(false) {
643 return Ok((cache_key, 0, true));
644 }
645
646 let mut stream = client.start().await?;
647 client.synthesize(&text, Some(0), Some(opt.clone())).await?;
648 client.stop().await?;
649
650 let mut chunks: Vec<Bytes> = Vec::new();
651 let mut first_chunk = true;
652 while let Some((_seq, res)) = stream.next().await {
653 match res? {
654 SynthesisEvent::AudioChunk(mut chunk) => {
655 if first_chunk {
657 if chunk.len() > 44 && chunk[..4] == [0x52, 0x49, 0x46, 0x46] {
658 let _ = chunk.split_to(44);
659 }
660 first_chunk = false;
661 }
662 chunks.push(chunk);
663 }
664 SynthesisEvent::Finished => break,
665 _ => {}
666 }
667 }
668 if chunks.is_empty() {
669 return Err(anyhow::anyhow!("tts produced no audio"));
670 }
671 let bytes: usize = chunks.iter().map(|c| c.len()).sum();
672 cache::store_in_cache_vectored(&cache_key, &chunks).await?;
673 info!(cache_key = %cache_key, bytes, "precache: stored tts audio");
674 Ok((cache_key, bytes, false))
675}
676
677trait IntoWsMessage {
678 fn into_ws_message(self) -> Result<Message, serde_json::Error>;
679}
680
681impl IntoWsMessage for crate::event::SessionEvent {
682 fn into_ws_message(self) -> Result<Message, serde_json::Error> {
683 match self {
684 SessionEvent::Binary { data, .. } => Ok(Message::Binary(data.into())),
685 SessionEvent::Ping { timestamp, payload } => {
686 let payload = payload.unwrap_or_else(|| timestamp.to_string());
687 Ok(Message::Ping(payload.into()))
688 }
689 event => serde_json::to_string(&event).map(|payload| Message::Text(payload.into())),
690 }
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use serde_json::json;
698 use std::collections::HashMap;
699
700 #[test]
701 fn test_filter_headers() {
702 let mut extras = HashMap::new();
703 extras.insert("X-Tenant-ID".to_string(), json!("123"));
704 extras.insert("X-User-ID".to_string(), json!("456"));
705 extras.insert("Custom-Header".to_string(), json!("abc"));
706 extras.insert("Irrelevant-Header".to_string(), json!("ignore"));
707
708 let allowed = vec!["x-tenant-id".to_string(), "Custom-Header".to_string()];
710
711 filter_headers(&mut extras, &allowed);
712
713 assert!(extras.contains_key("X-Tenant-ID"));
714 assert!(extras.contains_key("Custom-Header"));
715 assert!(!extras.contains_key("X-User-ID"));
716 assert!(!extras.contains_key("Irrelevant-Header"));
717
718 assert_eq!(extras.get("X-Tenant-ID").unwrap(), &json!("123"));
720 assert_eq!(extras.get("Custom-Header").unwrap(), &json!("abc"));
721 }
722
723 #[tokio::test]
724 async fn test_call_handler_core_extras_are_session_scoped() {
725 use crate::app::AppStateBuilder;
726 use crate::call::{ActiveCallType, Command};
727 use crate::config::Config;
728
729 let mut config = Config::default();
730 config.udp_port = 0;
731 let app_state = AppStateBuilder::new()
732 .with_config(config)
733 .build()
734 .await
735 .expect("Failed to build app state");
736
737 let session_id = "test-session-scoped".to_string();
738 let cancel_token = CancellationToken::new();
739
740 let mut extras = HashMap::new();
742 extras.insert("X-Custom".to_string(), json!("value"));
743
744 let (_audio_sender, audio_receiver) = tokio::sync::mpsc::unbounded_channel::<Bytes>();
745 let (command_sender, command_receiver) = tokio::sync::mpsc::unbounded_channel::<Command>();
746 let (event_sender, _event_receiver) =
747 tokio::sync::mpsc::unbounded_channel::<crate::event::SessionEvent>();
748
749 command_sender
751 .send(Command::Hangup {
752 reason: None,
753 initiator: None,
754 headers: None,
755 refer: None,
756 })
757 .ok();
758 drop(command_sender);
759
760 let final_extras = call_handler_core(
762 ActiveCallType::Sip,
763 session_id.clone(),
764 app_state.clone(),
765 cancel_token,
766 audio_receiver,
767 None,
768 false,
769 0,
770 command_receiver,
771 event_sender,
772 Some(extras), None, )
775 .await;
776
777 assert!(final_extras.is_some(), "final extras should be returned");
779 let extras = final_extras.unwrap();
780 assert_eq!(
781 extras.get("X-Custom"),
782 Some(&json!("value")),
783 "session-scoped extras should be preserved"
784 );
785 }
786}