feat(proxy-engine): add Rust-based outbound calling, WebRTC bridging, and voicemail handling
This commit is contained in:
132
rust/crates/proxy-engine/src/recorder.rs
Normal file
132
rust/crates/proxy-engine/src/recorder.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Audio recorder — receives RTP packets and writes a WAV file.
|
||||
|
||||
use codec_lib::TranscodeState;
|
||||
use std::path::Path;
|
||||
|
||||
/// Active recording session.
|
||||
pub struct Recorder {
|
||||
writer: hound::WavWriter<std::io::BufWriter<std::fs::File>>,
|
||||
transcoder: TranscodeState,
|
||||
source_pt: u8,
|
||||
total_samples: u64,
|
||||
sample_rate: u32,
|
||||
max_samples: Option<u64>,
|
||||
file_path: String,
|
||||
}
|
||||
|
||||
impl Recorder {
|
||||
/// Create a new recorder that writes to a WAV file.
|
||||
/// `source_pt` is the RTP payload type of the incoming audio.
|
||||
/// `max_duration_ms` optionally limits the recording length.
|
||||
pub fn new(
|
||||
file_path: &str,
|
||||
source_pt: u8,
|
||||
max_duration_ms: Option<u64>,
|
||||
) -> Result<Self, String> {
|
||||
// Ensure parent directory exists.
|
||||
if let Some(parent) = Path::new(file_path).parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create dir: {e}"))?;
|
||||
}
|
||||
|
||||
let sample_rate = 8000u32; // Record at 8kHz (standard telephony)
|
||||
let spec = hound::WavSpec {
|
||||
channels: 1,
|
||||
sample_rate,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
};
|
||||
|
||||
let writer = hound::WavWriter::create(file_path, spec)
|
||||
.map_err(|e| format!("create WAV {file_path}: {e}"))?;
|
||||
|
||||
let transcoder = TranscodeState::new().map_err(|e| format!("codec init: {e}"))?;
|
||||
|
||||
let max_samples = max_duration_ms.map(|ms| (sample_rate as u64 * ms) / 1000);
|
||||
|
||||
Ok(Self {
|
||||
writer,
|
||||
transcoder,
|
||||
source_pt,
|
||||
total_samples: 0,
|
||||
sample_rate,
|
||||
max_samples,
|
||||
file_path: file_path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Process an incoming RTP packet (full packet with header).
|
||||
/// Returns true if recording should continue, false if max duration reached.
|
||||
pub fn process_rtp(&mut self, data: &[u8]) -> bool {
|
||||
if data.len() <= 12 {
|
||||
return true; // Too short, skip.
|
||||
}
|
||||
|
||||
let pt = data[1] & 0x7F;
|
||||
// Skip telephone-event (DTMF) packets.
|
||||
if pt == 101 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let payload = &data[12..];
|
||||
if payload.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decode to PCM.
|
||||
let (pcm, rate) = match self.transcoder.decode_to_pcm(payload, self.source_pt) {
|
||||
Ok(result) => result,
|
||||
Err(_) => return true, // Decode failed, skip packet.
|
||||
};
|
||||
|
||||
// Resample to 8kHz if needed.
|
||||
let pcm_8k = if rate != self.sample_rate {
|
||||
match self.transcoder.resample(&pcm, rate, self.sample_rate) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return true,
|
||||
}
|
||||
} else {
|
||||
pcm
|
||||
};
|
||||
|
||||
// Write samples.
|
||||
for &sample in &pcm_8k {
|
||||
if let Err(_) = self.writer.write_sample(sample) {
|
||||
return false;
|
||||
}
|
||||
self.total_samples += 1;
|
||||
|
||||
if let Some(max) = self.max_samples {
|
||||
if self.total_samples >= max {
|
||||
return false; // Max duration reached.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Stop recording and finalize the WAV file.
|
||||
pub fn stop(self) -> RecordingResult {
|
||||
let duration_ms = if self.sample_rate > 0 {
|
||||
(self.total_samples * 1000) / self.sample_rate as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Writer is finalized on drop (writes RIFF header sizes).
|
||||
drop(self.writer);
|
||||
|
||||
RecordingResult {
|
||||
file_path: self.file_path,
|
||||
duration_ms,
|
||||
total_samples: self.total_samples,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecordingResult {
|
||||
pub file_path: String,
|
||||
pub duration_ms: u64,
|
||||
pub total_samples: u64,
|
||||
}
|
||||
Reference in New Issue
Block a user