1use std::collections::VecDeque;
4use std::fmt;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex as StdMutex};
7
8use async_trait::async_trait;
9use bytes::Bytes;
10use chrono::Utc;
11use dashmap::DashMap;
12use futures::{SinkExt, StreamExt};
13use parking_lot::Mutex;
14use rvoip_core::adapter::{
15 AdapterEvent, AdapterKind, AdapterLifecycleCapabilities, AdapterLifecycleSink,
16 AdapterLifecycleSinkSlot, ConnectionAdapter, ConnectionHandle, EndReason,
17 ExternalConnectionReference, OriginateRequest, OutboundActivation, RejectReason,
18 SignatureHeaders, TransferTarget,
19};
20use rvoip_core::commands::MuteDirection;
21use rvoip_core::connection::{Connection, ConnectionState, Direction, Transport, TransportHandle};
22use rvoip_core::error::{Result as RvoipResult, RvoipError};
23use rvoip_core::identity::IdentityAssurance;
24use rvoip_core::ids::ConnectionId;
25use rvoip_core::message::Message;
26use rvoip_core::stream::{MediaStream, MediaStreamHandle};
27use rvoip_core::{CapabilityDescriptor, NegotiatedCodecs};
28use tokio::sync::{broadcast, mpsc, watch};
29use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
30use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
31use tracing::warn;
32
33use crate::client::{connect_websocket, send_with_timeout, VapiHttpClient, VapiSocket};
34use crate::config::VapiConfig;
35use crate::error::{Result, VapiError};
36use crate::events::{VapiEvent, VapiEventEnvelope};
37use crate::media::{append_bounded_frames, AudioFramer, VapiMediaStream};
38use crate::types::{AddedMessage, VapiCallOptions, VapiCommand};
39
40pub const ADAPTER_EVENT_CAPACITY: usize = 256;
41pub const VAPI_CALL_REFERENCE_KIND: &str = "vapi-call-id";
42
43pub struct VapiTransportHandle {
45 connection_id: ConnectionId,
46}
47
48impl VapiTransportHandle {
49 pub fn connection_id(&self) -> &ConnectionId {
50 &self.connection_id
51 }
52}
53
54impl fmt::Debug for VapiTransportHandle {
55 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56 formatter.write_str("VapiTransportHandle([redacted])")
57 }
58}
59
60struct Route {
61 connection_id: ConnectionId,
62 options: VapiCallOptions,
63 stream: Arc<VapiMediaStream>,
64 cancel: tokio_util::sync::CancellationToken,
65 live: AtomicBool,
66 active: AtomicBool,
67 published: AtomicBool,
68 terminal: AtomicBool,
69 publication: Mutex<()>,
70 activation_started: AtomicBool,
71 activation_result:
72 watch::Sender<Option<std::result::Result<OutboundActivation, ActivationFailure>>>,
73 command_tx: Mutex<Option<mpsc::Sender<String>>>,
74 call_id: Mutex<Option<String>>,
75 requested_end_reason: Mutex<Option<EndReason>>,
76 events: broadcast::Sender<VapiEvent>,
77 initial_events: Mutex<Option<broadcast::Receiver<VapiEvent>>>,
78 finished: tokio_util::sync::CancellationToken,
79}
80
81impl Route {
82 fn new(
83 connection_id: ConnectionId,
84 options: VapiCallOptions,
85 config: &VapiConfig,
86 ) -> Arc<Self> {
87 let cancel = tokio_util::sync::CancellationToken::new();
88 let stream = VapiMediaStream::new(
89 options.audio_format,
90 1,
91 config.media_queue_capacity,
92 cancel.clone(),
93 );
94 let (events, initial_events) = broadcast::channel(config.event_queue_capacity);
95 let (activation_result, _) = watch::channel(None);
96 Arc::new(Self {
97 connection_id,
98 options,
99 stream,
100 cancel,
101 live: AtomicBool::new(true),
102 active: AtomicBool::new(false),
103 published: AtomicBool::new(false),
104 terminal: AtomicBool::new(false),
105 publication: Mutex::new(()),
106 activation_started: AtomicBool::new(false),
107 activation_result,
108 command_tx: Mutex::new(None),
109 call_id: Mutex::new(None),
110 requested_end_reason: Mutex::new(None),
111 events,
112 initial_events: Mutex::new(Some(initial_events)),
113 finished: tokio_util::sync::CancellationToken::new(),
114 })
115 }
116
117 fn activation_receipt(&self) -> RvoipResult<OutboundActivation> {
118 let call_id = self.call_id.lock().clone().ok_or(RvoipError::InvalidState(
119 "Vapi call activation has no call identifier",
120 ))?;
121 let reference = ExternalConnectionReference::new(VAPI_CALL_REFERENCE_KIND, call_id)
122 .map_err(|_| RvoipError::Adapter("Vapi returned an invalid call identifier".into()))?;
123 Ok(OutboundActivation::with_external_reference(reference))
124 }
125
126 fn take_event_receiver(&self) -> broadcast::Receiver<VapiEvent> {
127 self.initial_events
128 .lock()
129 .take()
130 .unwrap_or_else(|| self.events.subscribe())
131 }
132}
133
134struct RuntimeEnvironment {
135 config: VapiConfig,
136 routes: Arc<DashMap<ConnectionId, Arc<Route>>>,
137 adapter_events: mpsc::Sender<AdapterEvent>,
138 vapi_events: broadcast::Sender<VapiEventEnvelope>,
139 lifecycle: AdapterLifecycleSinkSlot,
140}
141
142#[derive(Clone, Copy)]
143enum ActivationFailure {
144 Vapi(VapiError),
145 Cancelled,
146 EventQueueFull,
147}
148
149impl ActivationFailure {
150 fn into_rvoip(self, connection_id: &ConnectionId) -> RvoipError {
151 match self {
152 Self::Vapi(error) => error.into(),
153 Self::Cancelled => RvoipError::ConnectionNotFound(connection_id.clone()),
154 Self::EventQueueFull => {
155 RvoipError::AdmissionRejected("Vapi adapter event queue is full")
156 }
157 }
158 }
159}
160
161pub struct VapiAdapter {
163 config: VapiConfig,
164 http: VapiHttpClient,
165 routes: Arc<DashMap<ConnectionId, Arc<Route>>>,
166 adapter_events_tx: mpsc::Sender<AdapterEvent>,
167 adapter_events_rx: StdMutex<Option<mpsc::Receiver<AdapterEvent>>>,
168 vapi_events: broadcast::Sender<VapiEventEnvelope>,
169 lifecycle: AdapterLifecycleSinkSlot,
170}
171
172impl VapiAdapter {
173 pub fn new(config: VapiConfig) -> Result<Arc<Self>> {
174 config.validate()?;
175 let http = VapiHttpClient::new(&config)?;
176 let (adapter_events_tx, adapter_events_rx) = mpsc::channel(ADAPTER_EVENT_CAPACITY);
177 let (vapi_events, _) = broadcast::channel(config.event_queue_capacity);
178 Ok(Arc::new(Self {
179 config,
180 http,
181 routes: Arc::new(DashMap::new()),
182 adapter_events_tx,
183 adapter_events_rx: StdMutex::new(Some(adapter_events_rx)),
184 vapi_events,
185 lifecycle: AdapterLifecycleSinkSlot::default(),
186 }))
187 }
188
189 pub fn subscribe_vapi_events(&self) -> broadcast::Receiver<VapiEventEnvelope> {
190 self.vapi_events.subscribe()
191 }
192
193 pub fn subscribe_call_events(
194 &self,
195 connection_id: &ConnectionId,
196 ) -> RvoipResult<broadcast::Receiver<VapiEvent>> {
197 self.routes
198 .get(connection_id)
199 .map(|route| route.take_event_receiver())
200 .ok_or_else(|| RvoipError::ConnectionNotFound(connection_id.clone()))
201 }
202
203 pub(crate) fn call_event_sender(
204 &self,
205 connection_id: &ConnectionId,
206 ) -> RvoipResult<broadcast::Sender<VapiEvent>> {
207 self.routes
208 .get(connection_id)
209 .map(|route| route.events.clone())
210 .ok_or_else(|| RvoipError::ConnectionNotFound(connection_id.clone()))
211 }
212
213 pub async fn say(
214 &self,
215 connection_id: &ConnectionId,
216 content: impl Into<String>,
217 end_call_after_spoken: bool,
218 interrupt_assistant: bool,
219 ) -> Result<()> {
220 let content = content.into();
221 if content.trim().is_empty() {
222 return Err(VapiError::InvalidCallOptions(
223 "say content must not be empty",
224 ));
225 }
226 self.send_command(
227 connection_id,
228 VapiCommand::Say {
229 content,
230 end_call_after_spoken,
231 interrupt_assistant_enabled: interrupt_assistant,
232 },
233 )
234 .await
235 }
236
237 pub async fn add_message(
238 &self,
239 connection_id: &ConnectionId,
240 role: impl Into<String>,
241 content: impl Into<String>,
242 trigger_response: bool,
243 ) -> Result<()> {
244 let role = role.into();
245 let content = content.into();
246 if !matches!(role.as_str(), "user" | "assistant" | "system") || content.trim().is_empty() {
247 return Err(VapiError::InvalidCallOptions(
248 "the added message role or content is invalid",
249 ));
250 }
251 self.send_command(
252 connection_id,
253 VapiCommand::AddMessage {
254 message: AddedMessage { role, content },
255 trigger_response_enabled: trigger_response,
256 },
257 )
258 .await
259 }
260
261 pub async fn mute_assistant(&self, connection_id: &ConnectionId) -> Result<()> {
262 self.send_control(connection_id, "mute-assistant").await
263 }
264
265 pub async fn unmute_assistant(&self, connection_id: &ConnectionId) -> Result<()> {
266 self.send_control(connection_id, "unmute-assistant").await
267 }
268
269 async fn send_control(
270 &self,
271 connection_id: &ConnectionId,
272 control: &'static str,
273 ) -> Result<()> {
274 self.send_command(connection_id, VapiCommand::Control { control })
275 .await
276 }
277
278 async fn send_command(&self, connection_id: &ConnectionId, command: VapiCommand) -> Result<()> {
279 let route = self
280 .routes
281 .get(connection_id)
282 .map(|entry| Arc::clone(entry.value()))
283 .ok_or(VapiError::NotActive)?;
284 if !route.active.load(Ordering::Acquire) || !route.live.load(Ordering::Acquire) {
285 return Err(VapiError::NotActive);
286 }
287 let serialized =
288 serde_json::to_string(&command).map_err(|_| VapiError::ControlQueueUnavailable)?;
289 if serialized.len() > self.config.max_message_bytes {
290 return Err(VapiError::ControlMessageTooLarge);
291 }
292 let sender = route
293 .command_tx
294 .lock()
295 .clone()
296 .ok_or(VapiError::NotActive)?;
297 sender
298 .try_send(serialized)
299 .map_err(|_| VapiError::ControlQueueUnavailable)
300 }
301
302 fn runtime_environment(&self) -> RuntimeEnvironment {
303 RuntimeEnvironment {
304 config: self.config.clone(),
305 routes: Arc::clone(&self.routes),
306 adapter_events: self.adapter_events_tx.clone(),
307 vapi_events: self.vapi_events.clone(),
308 lifecycle: self.lifecycle.clone(),
309 }
310 }
311
312 async fn activate_route(&self, route: Arc<Route>) -> RvoipResult<OutboundActivation> {
313 let mut activation_result = route.activation_result.subscribe();
314 if route
315 .activation_started
316 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
317 .is_ok()
318 {
319 let environment = self.runtime_environment();
320 let http = self.http.clone();
321 let worker_route = Arc::clone(&route);
322 tokio::spawn(async move {
323 let result =
324 activate_route_worker(&environment, &http, Arc::clone(&worker_route)).await;
325 worker_route.activation_result.send_replace(Some(result));
326 });
327 }
328 loop {
329 if let Some(result) = activation_result.borrow().clone() {
330 return result.map_err(|failure| failure.into_rvoip(&route.connection_id));
331 }
332 activation_result.changed().await.map_err(|_| {
333 RvoipError::InvalidState("Vapi activation owner stopped unexpectedly")
334 })?;
335 }
336 }
337
338 async fn terminate(&self, connection_id: ConnectionId, reason: EndReason) -> RvoipResult<()> {
339 let route = self
340 .routes
341 .get(&connection_id)
342 .map(|entry| Arc::clone(entry.value()))
343 .ok_or_else(|| RvoipError::ConnectionNotFound(connection_id.clone()))?;
344 *route.requested_end_reason.lock() = Some(reason.clone());
345 route.cancel.cancel();
346
347 if !route.active.load(Ordering::Acquire) {
348 finish_route(
349 &self.runtime_environment(),
350 Arc::clone(&route),
351 SessionOutcome::Local(reason),
352 )
353 .await;
354 return Ok(());
355 }
356 if tokio::time::timeout(
357 self.config.graceful_shutdown_timeout,
358 route.finished.cancelled(),
359 )
360 .await
361 .is_err()
362 {
363 let route = self
364 .routes
365 .get(&connection_id)
366 .map(|entry| Arc::clone(entry.value()));
367 if let Some(route) = route {
368 finish_route(
369 &self.runtime_environment(),
370 route,
371 SessionOutcome::Local(reason),
372 )
373 .await;
374 }
375 }
376 Ok(())
377 }
378}
379
380#[async_trait]
381impl ConnectionAdapter for VapiAdapter {
382 fn transport(&self) -> Transport {
383 Transport::Vapi
384 }
385
386 fn kind(&self) -> AdapterKind {
387 AdapterKind::Interop
388 }
389
390 fn lifecycle_capabilities(&self) -> AdapterLifecycleCapabilities {
391 AdapterLifecycleCapabilities {
392 authoritative_liveness: true,
393 atomic_inbound_handoff: true,
394 terminal_fallback: true,
395 staged_outbound_activation: true,
396 }
397 }
398
399 fn install_lifecycle_sink(&self, sink: Arc<dyn AdapterLifecycleSink>) -> RvoipResult<()> {
400 self.lifecycle
401 .install(sink)
402 .map_err(|_| RvoipError::InvalidState("Vapi lifecycle sink already installed"))
403 }
404
405 fn is_connection_live(&self, connection_id: &ConnectionId) -> bool {
406 self.routes
407 .get(connection_id)
408 .is_some_and(|route| route.live.load(Ordering::Acquire))
409 }
410
411 async fn originate(&self, request: OriginateRequest) -> RvoipResult<ConnectionHandle> {
412 if request.direction != Direction::Outbound {
413 return Err(RvoipError::AdmissionRejected(
414 "Vapi originate requires outbound direction",
415 ));
416 }
417 let options = request.context.downcast_arc::<VapiCallOptions>().ok_or(
418 RvoipError::AdmissionRejected("Vapi originate requires VapiCallOptions context"),
419 )?;
420 options.validate().map_err(RvoipError::from)?;
421
422 let connection_id = ConnectionId::new();
423 let route = Route::new(
424 connection_id.clone(),
425 options.as_ref().clone(),
426 &self.config,
427 );
428 let codec = options.audio_format.codec();
429 let connection = Connection {
430 id: connection_id.clone(),
431 session_id: request.session_id,
432 participant_id: request.participant_id,
433 transport: Transport::Vapi,
434 direction: Direction::Outbound,
435 state: ConnectionState::Connecting,
436 capabilities: options.audio_format.capabilities(),
437 negotiated_codecs: NegotiatedCodecs {
438 audio: Some(codec),
439 video: None,
440 },
441 streams: vec![MediaStreamHandle::new(
442 Arc::clone(&route.stream) as Arc<dyn MediaStream>
443 )],
444 messaging_enabled: false,
445 transport_handle: TransportHandle(Arc::new(VapiTransportHandle {
446 connection_id: connection_id.clone(),
447 })),
448 opened_at: Utc::now(),
449 closed_at: None,
450 };
451 if self.routes.insert(connection_id, route).is_some() {
452 return Err(RvoipError::AdmissionRejected(
453 "Vapi provisional connection ID collided",
454 ));
455 }
456 Ok(ConnectionHandle::new(connection))
457 }
458
459 async fn activate_outbound(&self, connection_id: ConnectionId) -> RvoipResult<()> {
460 self.activate_outbound_with_receipt(connection_id)
461 .await
462 .map(|_| ())
463 }
464
465 async fn activate_outbound_with_receipt(
466 &self,
467 connection_id: ConnectionId,
468 ) -> RvoipResult<OutboundActivation> {
469 let route = self
470 .routes
471 .get(&connection_id)
472 .map(|entry| Arc::clone(entry.value()))
473 .ok_or_else(|| RvoipError::ConnectionNotFound(connection_id))?;
474 self.activate_route(route).await
475 }
476
477 async fn accept(&self, connection_id: ConnectionId) -> RvoipResult<()> {
478 if self.is_connection_live(&connection_id) {
479 Ok(())
480 } else {
481 Err(RvoipError::ConnectionNotFound(connection_id))
482 }
483 }
484
485 async fn reject(&self, connection_id: ConnectionId, _reason: RejectReason) -> RvoipResult<()> {
486 self.terminate(connection_id, EndReason::Cancelled).await
487 }
488
489 async fn end(&self, connection_id: ConnectionId, reason: EndReason) -> RvoipResult<()> {
490 self.terminate(connection_id, reason).await
491 }
492
493 async fn hold(&self, _connection_id: ConnectionId) -> RvoipResult<()> {
494 Err(RvoipError::NotImplemented(
495 "Vapi transport hold is not supported",
496 ))
497 }
498
499 async fn resume(&self, _connection_id: ConnectionId) -> RvoipResult<()> {
500 Err(RvoipError::NotImplemented(
501 "Vapi transport resume is not supported",
502 ))
503 }
504
505 async fn transfer(
506 &self,
507 _connection_id: ConnectionId,
508 _target: TransferTarget,
509 ) -> RvoipResult<()> {
510 Err(RvoipError::NotImplemented(
511 "Vapi transport transfer remains owned by rvoip",
512 ))
513 }
514
515 async fn streams(&self, connection_id: ConnectionId) -> RvoipResult<Vec<Arc<dyn MediaStream>>> {
516 let route = self
517 .routes
518 .get(&connection_id)
519 .map(|entry| Arc::clone(entry.value()))
520 .ok_or_else(|| RvoipError::ConnectionNotFound(connection_id.clone()))?;
521 if !route.active.load(Ordering::Acquire) || !route.live.load(Ordering::Acquire) {
522 return Ok(Vec::new());
523 }
524 Ok(vec![Arc::clone(&route.stream) as Arc<dyn MediaStream>])
525 }
526
527 async fn send_message(
528 &self,
529 _connection_id: ConnectionId,
530 _message: Message,
531 ) -> RvoipResult<()> {
532 Err(RvoipError::NotImplemented(
533 "use VapiAdapter::add_message for Vapi context injection",
534 ))
535 }
536
537 async fn send_dtmf(
538 &self,
539 _connection_id: ConnectionId,
540 _digits: &str,
541 _duration_ms: u32,
542 ) -> RvoipResult<()> {
543 Err(RvoipError::NotImplemented(
544 "Vapi WebSocket transport has no DTMF input message",
545 ))
546 }
547
548 async fn renegotiate_media(
549 &self,
550 _connection_id: ConnectionId,
551 _capabilities: CapabilityDescriptor,
552 ) -> RvoipResult<NegotiatedCodecs> {
553 Err(RvoipError::NotImplemented(
554 "Vapi raw-audio format is fixed for the call lifetime",
555 ))
556 }
557
558 async fn mute(&self, connection_id: ConnectionId, direction: MuteDirection) -> RvoipResult<()> {
559 match direction {
560 MuteDirection::Send | MuteDirection::Both => {
561 return Err(RvoipError::NotImplemented(
562 "Vapi customer-input mute is not a documented WebSocket control",
563 ));
564 }
565 MuteDirection::Receive => self.send_control(&connection_id, "mute-assistant").await?,
566 }
567 Ok(())
568 }
569
570 async fn unmute(
571 &self,
572 connection_id: ConnectionId,
573 direction: MuteDirection,
574 ) -> RvoipResult<()> {
575 match direction {
576 MuteDirection::Send | MuteDirection::Both => {
577 return Err(RvoipError::NotImplemented(
578 "Vapi customer-input unmute is not a documented WebSocket control",
579 ));
580 }
581 MuteDirection::Receive => {
582 self.send_control(&connection_id, "unmute-assistant")
583 .await?
584 }
585 }
586 Ok(())
587 }
588
589 fn subscribe_events(&self) -> mpsc::Receiver<AdapterEvent> {
590 match self
591 .adapter_events_rx
592 .lock()
593 .unwrap_or_else(|poisoned| poisoned.into_inner())
594 .take()
595 {
596 Some(receiver) => receiver,
597 None => {
598 warn!("VapiAdapter::subscribe_events called more than once");
599 mpsc::channel(1).1
600 }
601 }
602 }
603
604 fn capabilities(&self) -> CapabilityDescriptor {
605 CapabilityDescriptor {
606 audio_codecs: vec![
607 crate::types::VapiAudioFormat::MuLaw8Khz.codec(),
608 crate::types::VapiAudioFormat::PcmS16Le16Khz.codec(),
609 ],
610 max_streams_per_connection: 1,
611 ..CapabilityDescriptor::default()
612 }
613 }
614
615 async fn verify_request_signature(
616 &self,
617 _connection_id: ConnectionId,
618 _signature: SignatureHeaders,
619 ) -> RvoipResult<IdentityAssurance> {
620 Ok(IdentityAssurance::Anonymous)
621 }
622}
623
624enum SessionOutcome {
625 Local(EndReason),
626 RemoteEnded,
627 RemoteClosed,
628 Failed(&'static str),
629}
630
631async fn activate_route_worker(
632 environment: &RuntimeEnvironment,
633 http: &VapiHttpClient,
634 route: Arc<Route>,
635) -> std::result::Result<OutboundActivation, ActivationFailure> {
636 if route.cancel.is_cancelled() || !route.live.load(Ordering::Acquire) {
637 return Err(ActivationFailure::Cancelled);
638 }
639
640 let created = match http.create_call(&environment.config, &route.options).await {
644 Ok(created) => created,
645 Err(error) => {
646 metrics::counter!("rvoip_vapi_setup_failures_total", "stage" => "call_creation")
647 .increment(1);
648 return Err(ActivationFailure::Vapi(error));
649 }
650 };
651 *route.call_id.lock() = Some(created.id);
652 let receipt = match route.activation_receipt() {
653 Ok(receipt) => receipt,
654 Err(_) => {
655 metrics::counter!("rvoip_vapi_setup_failures_total", "stage" => "call_response")
656 .increment(1);
657 cleanup_created_call(environment.config.clone(), created.websocket_url).await;
658 return Err(ActivationFailure::Vapi(VapiError::InvalidResponse(
659 "the call ID was invalid",
660 )));
661 }
662 };
663
664 if route.cancel.is_cancelled() || !route.live.load(Ordering::Acquire) {
665 cleanup_created_call(environment.config.clone(), created.websocket_url).await;
666 return Err(ActivationFailure::Cancelled);
667 }
668
669 let mut socket = match connect_websocket(&environment.config, &created.websocket_url).await {
670 Ok(socket) => socket,
671 Err(error) => {
672 metrics::counter!("rvoip_vapi_setup_failures_total", "stage" => "websocket")
673 .increment(1);
674 let cleanup_config = environment.config.clone();
675 let cleanup_url = created.websocket_url;
676 tokio::spawn(async move {
677 cleanup_created_call(cleanup_config, cleanup_url).await;
678 });
679 return Err(ActivationFailure::Vapi(error));
680 }
681 };
682 if route.cancel.is_cancelled() || !route.live.load(Ordering::Acquire) {
683 close_vapi_socket(&environment.config, &mut socket).await;
684 return Err(ActivationFailure::Cancelled);
685 }
686
687 let outgoing = match route.stream.take_outgoing_receiver() {
688 Ok(outgoing) => outgoing,
689 Err(error) => {
690 metrics::counter!("rvoip_vapi_setup_failures_total", "stage" => "media").increment(1);
691 close_vapi_socket(&environment.config, &mut socket).await;
692 return Err(ActivationFailure::Vapi(error));
693 }
694 };
695 let (command_tx, command_rx) = mpsc::channel(environment.config.control_queue_capacity);
696 *route.command_tx.lock() = Some(command_tx);
697 route.active.store(true, Ordering::Release);
698 route.stream.activate();
699
700 if route.cancel.is_cancelled() || !route.live.load(Ordering::Acquire) {
701 route.active.store(false, Ordering::Release);
702 route.stream.deactivate();
703 route.command_tx.lock().take();
704 close_vapi_socket(&environment.config, &mut socket).await;
705 let reason = route
706 .requested_end_reason
707 .lock()
708 .clone()
709 .unwrap_or(EndReason::Cancelled);
710 finish_route(
711 environment,
712 Arc::clone(&route),
713 SessionOutcome::Local(reason),
714 )
715 .await;
716 return Err(ActivationFailure::Cancelled);
717 }
718 let publication = {
722 let _publication = route.publication.lock();
723 if route.cancel.is_cancelled()
724 || !route.live.load(Ordering::Acquire)
725 || route.terminal.load(Ordering::Acquire)
726 {
727 Err(ActivationFailure::Cancelled)
728 } else {
729 route.published.store(true, Ordering::Release);
730 if environment
731 .adapter_events
732 .try_send(AdapterEvent::Connected {
733 connection_id: route.connection_id.clone(),
734 })
735 .is_err()
736 {
737 route.published.store(false, Ordering::Release);
738 Err(ActivationFailure::EventQueueFull)
739 } else {
740 Ok(())
741 }
742 }
743 };
744 if let Err(failure) = publication {
745 if matches!(failure, ActivationFailure::EventQueueFull) {
746 metrics::counter!("rvoip_vapi_setup_failures_total", "stage" => "publication")
747 .increment(1);
748 }
749 route.active.store(false, Ordering::Release);
750 route.stream.deactivate();
751 route.command_tx.lock().take();
752 close_vapi_socket(&environment.config, &mut socket).await;
753 let reason = route
754 .requested_end_reason
755 .lock()
756 .clone()
757 .unwrap_or(EndReason::Cancelled);
758 finish_route(
759 environment,
760 Arc::clone(&route),
761 SessionOutcome::Local(reason),
762 )
763 .await;
764 return Err(failure);
765 }
766 if route.cancel.is_cancelled()
767 || !route.live.load(Ordering::Acquire)
768 || route.terminal.load(Ordering::Acquire)
769 {
770 close_vapi_socket(&environment.config, &mut socket).await;
771 let reason = route
772 .requested_end_reason
773 .lock()
774 .clone()
775 .unwrap_or(EndReason::Cancelled);
776 finish_route(
777 environment,
778 Arc::clone(&route),
779 SessionOutcome::Local(reason),
780 )
781 .await;
782 return Err(ActivationFailure::Cancelled);
783 }
784
785 let runtime_environment = RuntimeEnvironment {
786 config: environment.config.clone(),
787 routes: Arc::clone(&environment.routes),
788 adapter_events: environment.adapter_events.clone(),
789 vapi_events: environment.vapi_events.clone(),
790 lifecycle: environment.lifecycle.clone(),
791 };
792 let task_route = Arc::clone(&route);
793 tokio::spawn(async move {
794 let outcome = run_websocket_session(
795 &runtime_environment.config,
796 &runtime_environment.vapi_events,
797 &task_route,
798 socket,
799 outgoing,
800 command_rx,
801 )
802 .await;
803 finish_route(&runtime_environment, task_route, outcome).await;
804 });
805 Ok(receipt)
806}
807
808async fn cleanup_created_call(config: VapiConfig, websocket_url: url::Url) {
809 if let Ok(mut socket) = connect_websocket(&config, &websocket_url).await {
810 close_vapi_socket(&config, &mut socket).await;
811 }
812}
813
814async fn close_vapi_socket(config: &VapiConfig, socket: &mut VapiSocket) {
815 let shutdown = async {
816 let _ = socket
817 .send(WebSocketMessage::Text(r#"{"type":"end-call"}"#.into()))
818 .await;
819 let _ = socket.close(None).await;
820 };
821 let _ = tokio::time::timeout(config.graceful_shutdown_timeout, shutdown).await;
822}
823
824enum SessionSendFailure {
825 Cancelled,
826 Failed,
827}
828
829async fn cancellable_session_send(
830 config: &VapiConfig,
831 route: &Route,
832 socket: &mut VapiSocket,
833 message: WebSocketMessage,
834) -> std::result::Result<(), SessionSendFailure> {
835 tokio::select! {
836 _ = route.cancel.cancelled() => Err(SessionSendFailure::Cancelled),
837 result = send_with_timeout(config.websocket_io_timeout, socket, message) => {
838 result.map_err(|_| SessionSendFailure::Failed)
839 }
840 }
841}
842
843fn requested_local_outcome(route: &Route) -> SessionOutcome {
844 SessionOutcome::Local(
845 route
846 .requested_end_reason
847 .lock()
848 .clone()
849 .unwrap_or(EndReason::Normal),
850 )
851}
852
853async fn run_websocket_session(
854 config: &VapiConfig,
855 global_events: &broadcast::Sender<VapiEventEnvelope>,
856 route: &Arc<Route>,
857 mut socket: VapiSocket,
858 mut outgoing: mpsc::Receiver<rvoip_core::MediaFrame>,
859 mut commands: mpsc::Receiver<String>,
860) -> SessionOutcome {
861 let mut incoming_framer = AudioFramer::new(route.options.audio_format);
862 let mut outgoing_framer = AudioFramer::new(route.options.audio_format);
863 let mut incoming_frames = VecDeque::<Bytes>::new();
864 let mut outgoing_frames = VecDeque::<Bytes>::new();
865 let mut timestamp_rtp = 0u32;
866 let mut incoming_tick = tokio::time::interval(std::time::Duration::from_millis(20));
867 incoming_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
868 incoming_tick.tick().await;
869 let mut outgoing_tick = tokio::time::interval(std::time::Duration::from_millis(20));
870 outgoing_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
871 outgoing_tick.tick().await;
872 let mut heartbeat = tokio::time::interval(config.heartbeat_interval);
873 heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
874 heartbeat.tick().await;
875 let heartbeat_deadline = tokio::time::sleep(config.websocket_io_timeout);
876 tokio::pin!(heartbeat_deadline);
877 let mut awaiting_heartbeat_response = false;
878
879 let outcome = loop {
880 tokio::select! {
881 _ = route.cancel.cancelled() => {
882 break requested_local_outcome(route);
883 }
884 _ = &mut heartbeat_deadline, if awaiting_heartbeat_response => {
885 break SessionOutcome::Failed("Vapi WebSocket heartbeat response timed out");
886 }
887 _ = incoming_tick.tick(), if !incoming_frames.is_empty() => {
888 if route.stream.incoming_is_closed() {
889 break SessionOutcome::Local(EndReason::BridgeTorn);
890 }
891 if !route.stream.incoming_has_capacity() {
892 continue;
893 }
894 let Some(frame) = incoming_frames.pop_front() else {
895 continue;
896 };
897 if route.stream.try_push_incoming(frame, timestamp_rtp).is_err() {
898 metrics::counter!("rvoip_vapi_queue_overflow_total", "direction" => "in").increment(1);
899 break SessionOutcome::Failed("Vapi inbound media queue overflow");
900 }
901 timestamp_rtp = timestamp_rtp
902 .wrapping_add(route.options.audio_format.timestamp_increment());
903 metrics::counter!("rvoip_vapi_audio_frames_total", "direction" => "in").increment(1);
904 }
905 _ = outgoing_tick.tick(), if !outgoing_frames.is_empty() => {
906 let Some(frame) = outgoing_frames.pop_front() else {
907 continue;
908 };
909 match cancellable_session_send(
910 config,
911 route,
912 &mut socket,
913 WebSocketMessage::Binary(frame),
914 ).await {
915 Ok(()) => {}
916 Err(SessionSendFailure::Cancelled) => break requested_local_outcome(route),
917 Err(SessionSendFailure::Failed) => {
918 break SessionOutcome::Failed("Vapi WebSocket audio write failed");
919 }
920 }
921 metrics::counter!("rvoip_vapi_audio_frames_total", "direction" => "out").increment(1);
922 }
923 _ = heartbeat.tick() => {
924 if awaiting_heartbeat_response {
925 continue;
926 }
927 match cancellable_session_send(
928 config,
929 route,
930 &mut socket,
931 WebSocketMessage::Ping(Bytes::new()),
932 ).await {
933 Ok(()) => {}
934 Err(SessionSendFailure::Cancelled) => break requested_local_outcome(route),
935 Err(SessionSendFailure::Failed) => {
936 break SessionOutcome::Failed("Vapi WebSocket heartbeat failed");
937 }
938 }
939 heartbeat_deadline
940 .as_mut()
941 .reset(tokio::time::Instant::now() + config.websocket_io_timeout);
942 awaiting_heartbeat_response = true;
943 }
944 media = outgoing.recv() => {
945 let Some(media) = media else {
946 break SessionOutcome::Local(EndReason::BridgeTorn);
947 };
948 if media.payload_type == Some(101) {
949 metrics::counter!("rvoip_vapi_dtmf_frames_dropped_total").increment(1);
950 continue;
951 }
952 if append_bounded_frames(
953 &mut outgoing_framer,
954 &media.payload,
955 &mut outgoing_frames,
956 config.media_queue_capacity,
957 config.max_message_bytes,
958 ).is_err() {
959 metrics::counter!("rvoip_vapi_queue_overflow_total", "direction" => "out").increment(1);
960 break SessionOutcome::Failed("Vapi outbound media queue overflow");
961 }
962 }
963 command = commands.recv() => {
964 let Some(json) = command else {
965 break SessionOutcome::Local(EndReason::Normal);
966 };
967 match cancellable_session_send(
968 config,
969 route,
970 &mut socket,
971 WebSocketMessage::Text(json.into()),
972 ).await {
973 Ok(()) => {}
974 Err(SessionSendFailure::Cancelled) => break requested_local_outcome(route),
975 Err(SessionSendFailure::Failed) => {
976 break SessionOutcome::Failed("Vapi WebSocket control write failed");
977 }
978 }
979 }
980 message = socket.next() => {
981 if matches!(message.as_ref(), Some(Ok(_))) {
982 awaiting_heartbeat_response = false;
983 }
984 match message {
985 Some(Ok(WebSocketMessage::Binary(payload))) => {
986 let max_queued = config
987 .startup_audio_frames
988 .saturating_sub(route.stream.incoming_pending_frames());
989 if append_bounded_frames(
990 &mut incoming_framer,
991 &payload,
992 &mut incoming_frames,
993 max_queued,
994 config.max_message_bytes,
995 ).is_err() {
996 metrics::counter!("rvoip_vapi_queue_overflow_total", "direction" => "in").increment(1);
997 break SessionOutcome::Failed("Vapi inbound media buffer overflow");
998 }
999 }
1000 Some(Ok(WebSocketMessage::Text(text))) => {
1001 if text.len() > config.max_message_bytes {
1002 break SessionOutcome::Failed("Vapi event exceeded size limit");
1003 }
1004 let event = VapiEvent::parse(text.as_str());
1005 let terminal = event.is_terminal_status();
1006 publish_vapi_event(global_events, route, event);
1007 if terminal {
1008 break SessionOutcome::RemoteEnded;
1009 }
1010 }
1011 Some(Ok(WebSocketMessage::Ping(payload))) => {
1012 match cancellable_session_send(
1013 config,
1014 route,
1015 &mut socket,
1016 WebSocketMessage::Pong(payload),
1017 ).await {
1018 Ok(()) => {}
1019 Err(SessionSendFailure::Cancelled) => break requested_local_outcome(route),
1020 Err(SessionSendFailure::Failed) => {
1021 break SessionOutcome::Failed("Vapi WebSocket pong failed");
1022 }
1023 }
1024 }
1025 Some(Ok(WebSocketMessage::Pong(_))) => {}
1026 Some(Ok(WebSocketMessage::Close(frame))) => {
1027 break match frame.map(|frame| frame.code) {
1028 Some(CloseCode::Normal | CloseCode::Away) => {
1029 SessionOutcome::RemoteEnded
1030 }
1031 _ => SessionOutcome::RemoteClosed,
1032 };
1033 }
1034 None => break SessionOutcome::RemoteClosed,
1035 Some(Ok(WebSocketMessage::Frame(_))) => {}
1036 Some(Err(_)) => {
1037 break SessionOutcome::Failed("Vapi WebSocket read failed");
1038 }
1039 }
1040 }
1041 }
1042 };
1043
1044 match &outcome {
1045 SessionOutcome::Local(_) => {
1046 wait_for_vapi_shutdown(config, global_events, route, &mut socket).await;
1047 }
1048 _ => {
1049 let _ =
1050 tokio::time::timeout(config.graceful_shutdown_timeout, socket.close(None)).await;
1051 }
1052 }
1053 outcome
1054}
1055
1056async fn wait_for_vapi_shutdown(
1057 config: &VapiConfig,
1058 global_events: &broadcast::Sender<VapiEventEnvelope>,
1059 route: &Route,
1060 socket: &mut VapiSocket,
1061) {
1062 let deadline = tokio::time::Instant::now() + config.graceful_shutdown_timeout;
1063 if !matches!(
1064 tokio::time::timeout_at(
1065 deadline,
1066 socket.send(WebSocketMessage::Text(r#"{"type":"end-call"}"#.into())),
1067 )
1068 .await,
1069 Ok(Ok(()))
1070 ) {
1071 return;
1072 }
1073
1074 loop {
1075 match tokio::time::timeout_at(deadline, socket.next()).await {
1076 Ok(Some(Ok(WebSocketMessage::Text(text)))) => {
1077 if text.len() > config.max_message_bytes {
1078 break;
1079 }
1080 let event = VapiEvent::parse(text.as_str());
1081 let terminal = event.is_terminal_status();
1082 publish_vapi_event(global_events, route, event);
1083 if terminal {
1084 break;
1085 }
1086 }
1087 Ok(Some(Ok(WebSocketMessage::Ping(payload)))) => {
1088 if !matches!(
1089 tokio::time::timeout_at(
1090 deadline,
1091 socket.send(WebSocketMessage::Pong(payload)),
1092 )
1093 .await,
1094 Ok(Ok(()))
1095 ) {
1096 break;
1097 }
1098 }
1099 Ok(Some(Ok(
1100 WebSocketMessage::Binary(_)
1101 | WebSocketMessage::Pong(_)
1102 | WebSocketMessage::Frame(_),
1103 ))) => {}
1104 Ok(Some(Ok(WebSocketMessage::Close(_))) | Some(Err(_)) | None) | Err(_) => break,
1105 }
1106 }
1107
1108 if tokio::time::Instant::now() < deadline {
1109 let _ = tokio::time::timeout_at(deadline, socket.close(None)).await;
1110 }
1111}
1112
1113fn publish_vapi_event(
1114 global_events: &broadcast::Sender<VapiEventEnvelope>,
1115 route: &Route,
1116 event: VapiEvent,
1117) {
1118 if matches!(event, VapiEvent::Unknown(_)) {
1119 metrics::counter!("rvoip_vapi_unknown_events_total").increment(1);
1120 }
1121 if matches!(event, VapiEvent::Malformed { .. }) {
1122 metrics::counter!("rvoip_vapi_malformed_events_total").increment(1);
1123 }
1124 let _ = global_events.send(VapiEventEnvelope {
1125 connection_id: route.connection_id.clone(),
1126 event: event.clone(),
1127 });
1128 let _ = route.events.send(event);
1129}
1130
1131async fn finish_route(
1132 environment: &RuntimeEnvironment,
1133 route: Arc<Route>,
1134 outcome: SessionOutcome,
1135) {
1136 if route.terminal.swap(true, Ordering::AcqRel) {
1137 return;
1138 }
1139 let published = {
1140 let _publication = route.publication.lock();
1141 route.published.load(Ordering::Acquire)
1142 };
1143 route.live.store(false, Ordering::Release);
1144 route.active.store(false, Ordering::Release);
1145 route.stream.deactivate();
1146 route.cancel.cancel();
1147 route.command_tx.lock().take();
1148 route.call_id.lock().take();
1149
1150 let remove = environment
1151 .routes
1152 .get(&route.connection_id)
1153 .is_some_and(|current| Arc::ptr_eq(current.value(), &route));
1154 if remove {
1155 environment.routes.remove(&route.connection_id);
1156 }
1157
1158 match &outcome {
1159 SessionOutcome::RemoteEnded => {
1160 metrics::counter!("rvoip_vapi_disconnects_total", "outcome" => "normal").increment(1);
1161 }
1162 SessionOutcome::RemoteClosed => {
1163 metrics::counter!("rvoip_vapi_disconnects_total", "outcome" => "abnormal").increment(1);
1164 }
1165 SessionOutcome::Failed(_) => {
1166 metrics::counter!("rvoip_vapi_disconnects_total", "outcome" => "failed").increment(1);
1167 }
1168 SessionOutcome::Local(_) => {}
1169 }
1170
1171 if published {
1172 let terminal_event = match outcome {
1173 SessionOutcome::Local(reason) => AdapterEvent::Ended {
1174 connection_id: route.connection_id.clone(),
1175 reason,
1176 },
1177 SessionOutcome::RemoteEnded => AdapterEvent::Ended {
1178 connection_id: route.connection_id.clone(),
1179 reason: EndReason::Normal,
1180 },
1181 SessionOutcome::RemoteClosed => AdapterEvent::Failed {
1182 connection_id: route.connection_id.clone(),
1183 detail: "Vapi WebSocket closed without a terminal status".into(),
1184 },
1185 SessionOutcome::Failed(detail) => AdapterEvent::Failed {
1186 connection_id: route.connection_id.clone(),
1187 detail: detail.into(),
1188 },
1189 };
1190 let _ = environment
1191 .lifecycle
1192 .queue_or_deliver_terminal(&environment.adapter_events, terminal_event)
1193 .await;
1194 }
1195 route.finished.cancel();
1196 metrics::counter!("rvoip_vapi_sessions_terminated_total").increment(1);
1197}