feat(proxy-engine): add Rust-based outbound calling, WebRTC bridging, and voicemail handling
This commit is contained in:
137
rust/crates/proxy-engine/src/voicemail.rs
Normal file
137
rust/crates/proxy-engine/src/voicemail.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
//! Voicemail session — answer → play greeting → beep → record → done.
|
||||
|
||||
use crate::audio_player::{play_beep, play_wav_file};
|
||||
use crate::ipc::{emit_event, OutTx};
|
||||
use crate::recorder::Recorder;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
/// Run a voicemail session on an RTP port.
|
||||
///
|
||||
/// 1. Plays the greeting WAV file to the caller
|
||||
/// 2. Plays a beep tone
|
||||
/// 3. Records the caller's message until BYE or max duration
|
||||
///
|
||||
/// The RTP receive loop is separate — it feeds packets to the recorder
|
||||
/// via the returned channel.
|
||||
pub async fn run_voicemail_session(
|
||||
rtp_socket: Arc<UdpSocket>,
|
||||
provider_media: SocketAddr,
|
||||
codec_pt: u8,
|
||||
greeting_wav: Option<String>,
|
||||
recording_path: String,
|
||||
max_recording_ms: u64,
|
||||
call_id: String,
|
||||
caller_number: String,
|
||||
out_tx: OutTx,
|
||||
) {
|
||||
let ssrc: u32 = rand::random();
|
||||
|
||||
emit_event(
|
||||
&out_tx,
|
||||
"voicemail_started",
|
||||
serde_json::json!({
|
||||
"call_id": call_id,
|
||||
"caller_number": caller_number,
|
||||
}),
|
||||
);
|
||||
|
||||
// Step 1: Play greeting.
|
||||
let mut next_seq: u16 = 0;
|
||||
let mut next_ts: u32 = 0;
|
||||
|
||||
if let Some(wav_path) = &greeting_wav {
|
||||
match play_wav_file(wav_path, rtp_socket.clone(), provider_media, codec_pt, ssrc).await {
|
||||
Ok(frames) => {
|
||||
next_seq = frames as u16;
|
||||
next_ts = frames * crate::rtp::rtp_clock_increment(codec_pt);
|
||||
}
|
||||
Err(e) => {
|
||||
emit_event(
|
||||
&out_tx,
|
||||
"voicemail_error",
|
||||
serde_json::json!({ "call_id": call_id, "error": format!("greeting: {e}") }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Play beep (1kHz, 500ms).
|
||||
match play_beep(
|
||||
rtp_socket.clone(),
|
||||
provider_media,
|
||||
codec_pt,
|
||||
ssrc,
|
||||
next_seq,
|
||||
next_ts,
|
||||
1000,
|
||||
500,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_seq, _ts)) => {}
|
||||
Err(e) => {
|
||||
emit_event(
|
||||
&out_tx,
|
||||
"voicemail_error",
|
||||
serde_json::json!({ "call_id": call_id, "error": format!("beep: {e}") }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Record incoming audio.
|
||||
let recorder = match Recorder::new(&recording_path, codec_pt, Some(max_recording_ms)) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
emit_event(
|
||||
&out_tx,
|
||||
"voicemail_error",
|
||||
serde_json::json!({ "call_id": call_id, "error": format!("recorder: {e}") }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Receive RTP and feed to recorder.
|
||||
let result = record_from_socket(rtp_socket, recorder, max_recording_ms).await;
|
||||
|
||||
// Step 4: Done — emit recording result.
|
||||
emit_event(
|
||||
&out_tx,
|
||||
"recording_done",
|
||||
serde_json::json!({
|
||||
"call_id": call_id,
|
||||
"file_path": result.file_path,
|
||||
"duration_ms": result.duration_ms,
|
||||
"caller_number": caller_number,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Read RTP packets from the socket and feed them to the recorder.
|
||||
/// Returns when the socket errors out (BYE closes the call/socket)
|
||||
/// or max duration is reached.
|
||||
async fn record_from_socket(
|
||||
socket: Arc<UdpSocket>,
|
||||
mut recorder: Recorder,
|
||||
max_ms: u64,
|
||||
) -> crate::recorder::RecordingResult {
|
||||
let mut buf = vec![0u8; 65535];
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(max_ms + 2000);
|
||||
|
||||
loop {
|
||||
let timeout = tokio::time::timeout_at(deadline, socket.recv_from(&mut buf));
|
||||
match timeout.await {
|
||||
Ok(Ok((n, _addr))) => {
|
||||
if !recorder.process_rtp(&buf[..n]) {
|
||||
break; // Max duration reached.
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) => break, // Socket error (closed).
|
||||
Err(_) => break, // Timeout (max duration + grace).
|
||||
}
|
||||
}
|
||||
|
||||
recorder.stop()
|
||||
}
|
||||
Reference in New Issue
Block a user