Skip to main content

active_call/media/
recorder.rs

1use anyhow::{Result, anyhow};
2use audio_codec::{BoxedResampler, PcmBuf, Sample, samples_to_bytes};
3use futures::StreamExt;
4use serde::{Deserialize, Serialize};
5use std::{
6    collections::HashMap,
7    path::Path,
8    sync::{
9        Mutex,
10        atomic::{AtomicU32, AtomicUsize, Ordering},
11    },
12    time::Duration,
13    u32,
14};
15use tokio::{
16    fs::File,
17    io::{AsyncSeekExt, AsyncWriteExt},
18    select,
19    sync::mpsc::UnboundedReceiver,
20};
21use tokio_stream::wrappers::IntervalStream;
22use tokio_util::sync::CancellationToken;
23use tracing::{info, warn};
24
25use crate::media::processor::convert_to_mono;
26use crate::media::{AudioFrame, Samples};
27
28#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
29#[serde(rename_all = "lowercase")]
30pub enum RecorderFormat {
31    Wav,
32    Pcm,
33    Pcmu,
34    Pcma,
35    G722,
36}
37
38impl RecorderFormat {
39    pub fn extension(&self) -> &'static str {
40        "wav"
41    }
42
43    pub fn is_supported(&self) -> bool {
44        true
45    }
46
47    pub fn effective(&self) -> RecorderFormat {
48        *self
49    }
50}
51
52impl Default for RecorderFormat {
53    fn default() -> Self {
54        RecorderFormat::Wav
55    }
56}
57
58#[derive(Debug, Deserialize, Serialize, Clone)]
59#[serde(rename_all = "camelCase")]
60#[serde(default)]
61pub struct RecorderOption {
62    #[serde(default)]
63    pub recorder_file: String,
64    #[serde(default)]
65    pub samplerate: u32,
66    #[serde(default)]
67    pub ptime: u32,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub format: Option<RecorderFormat>,
70    /// When enabled, the recorder captures audio at the source's native
71    /// sample rate (as decoded from the wire) instead of the 16 kHz
72    /// pipeline rate. The WAV header records the detected rate; the
73    /// `samplerate` option is only used as a fallback if detection fails.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub native_samplerate: Option<bool>,
76}
77
78impl RecorderOption {
79    pub fn new(recorder_file: String) -> Self {
80        Self {
81            recorder_file,
82            ..Default::default()
83        }
84    }
85
86    pub fn resolved_format(&self, default: RecorderFormat) -> RecorderFormat {
87        self.format.unwrap_or(default).effective()
88    }
89
90    pub fn ensure_path_extension(&mut self, fallback_format: RecorderFormat) {
91        let effective_format = self.format.unwrap_or(fallback_format).effective();
92        self.format = Some(effective_format);
93
94        if self.recorder_file.is_empty() {
95            return;
96        }
97
98        let extension = effective_format.extension();
99        if !self
100            .recorder_file
101            .to_lowercase()
102            .ends_with(&format!(".{}", extension.to_lowercase()))
103        {
104            self.recorder_file = format!("{}.{}", self.recorder_file, extension);
105        }
106    }
107}
108
109impl Default for RecorderOption {
110    fn default() -> Self {
111        Self {
112            recorder_file: "".to_string(),
113            samplerate: 16000,
114            ptime: 200,
115            format: None,
116            native_samplerate: None,
117        }
118    }
119}
120
121/// Max frames buffered while waiting for the caller leg to reveal the native
122/// target sample rate in native-samplerate mode (~5s of 20ms frames).
123const NATIVE_PENDING_FRAME_LIMIT: usize = 250;
124
125#[derive(Debug)]
126struct PendingRaw {
127    channel: usize,
128    samples: PcmBuf,
129    sample_rate: u32,
130}
131
132pub struct Recorder {
133    session_id: String,
134    option: RecorderOption,
135    samples_written: AtomicUsize,
136    cancel_token: CancellationToken,
137    stereo_buf: Mutex<PcmBuf>,
138    mono_buf: Mutex<PcmBuf>,
139    native_samplerate: bool,
140    /// Native target sample rate in native mode; 0 until detected from the
141    /// first caller-leg (channel 0) frame.
142    target_rate: AtomicU32,
143    /// Stateful resamplers keyed by `(channel, source_rate)` so each leg keeps
144    /// a continuous resampling stream.
145    resamplers: Mutex<HashMap<(usize, u32), BoxedResampler>>,
146    /// Frames received before the target rate is known.
147    pending: Mutex<Vec<PendingRaw>>,
148}
149
150impl Recorder {
151    pub fn new(
152        cancel_token: CancellationToken,
153        session_id: String,
154        option: RecorderOption,
155    ) -> Self {
156        let native_samplerate = option.native_samplerate.unwrap_or(false);
157        Self {
158            session_id,
159            option,
160            samples_written: AtomicUsize::new(0),
161            cancel_token,
162            stereo_buf: Mutex::new(Vec::new()),
163            mono_buf: Mutex::new(Vec::new()),
164            native_samplerate,
165            target_rate: AtomicU32::new(0),
166            resamplers: Mutex::new(HashMap::new()),
167            pending: Mutex::new(Vec::new()),
168        }
169    }
170
171    /// The sample rate used for the WAV header and chunking: the detected
172    /// native rate in native mode, otherwise the configured `samplerate`.
173    fn effective_samplerate(&self) -> u32 {
174        if self.native_samplerate {
175            let latched = self.target_rate.load(Ordering::SeqCst);
176            if latched > 0 {
177                return latched;
178            }
179        }
180        if self.option.samplerate > 0 {
181            self.option.samplerate
182        } else {
183            16000
184        }
185    }
186
187    fn compute_chunk_size(&self) -> usize {
188        if self.native_samplerate && self.target_rate.load(Ordering::SeqCst) == 0 {
189            return 0;
190        }
191        let rate = self.effective_samplerate();
192        (rate / 1000 * self.option.ptime.max(1)) as usize
193    }
194
195    async fn update_wav_header(&self, file: &mut File, payload_type: Option<u8>) -> Result<()> {
196        let total = self.samples_written.load(Ordering::SeqCst);
197
198        let (format_tag, sample_rate, channels, bits_per_sample, data_size): (
199            u16,
200            u32,
201            u16,
202            u16,
203            usize,
204        ) = match payload_type {
205            Some(pt) => {
206                let (tag, rate, chan): (u16, u32, u16) = match pt {
207                    0 => (0x0007, 8000, 1),   // PCMU
208                    8 => (0x0006, 8000, 1),   // PCMA
209                    9 => (0x0064, 16000, 1),  // G722
210                    10 => (0x0001, 44100, 2), // L16 Stereo 44.1k
211                    11 => (0x0001, 44100, 1), // L16 Mono 44.1k
212                    _ => (0x0001, 16000, 1),  // Default to PCM 16k Mono
213                };
214                let bits: u16 = match pt {
215                    9 => 4,
216                    0 | 8 => 8,
217                    _ => 16,
218                };
219                (tag, rate, chan, bits, total)
220            }
221            None => (0x0001, self.effective_samplerate(), 2, 16, total),
222        };
223
224        let mut header_buf = Vec::new();
225        header_buf.extend_from_slice(b"RIFF");
226        let file_size = data_size + 36;
227        header_buf.extend_from_slice(&(file_size as u32).to_le_bytes());
228        header_buf.extend_from_slice(b"WAVE");
229
230        header_buf.extend_from_slice(b"fmt ");
231        header_buf.extend_from_slice(&16u32.to_le_bytes());
232        header_buf.extend_from_slice(&format_tag.to_le_bytes());
233        header_buf.extend_from_slice(&(channels as u16).to_le_bytes());
234        header_buf.extend_from_slice(&sample_rate.to_le_bytes());
235
236        let bytes_per_sec: u32 = match format_tag {
237            0x0064 => 8000, // G.722 is 64kbps
238            _ => sample_rate * (channels as u32) * (bits_per_sample as u32 / 8),
239        };
240        header_buf.extend_from_slice(&bytes_per_sec.to_le_bytes());
241
242        let block_align: u16 = match format_tag {
243            0x0064 | 0x0007 | 0x0006 => 1 * channels,
244            _ => (bits_per_sample / 8) * channels,
245        };
246        header_buf.extend_from_slice(&block_align.to_le_bytes());
247        header_buf.extend_from_slice(&bits_per_sample.to_le_bytes());
248
249        header_buf.extend_from_slice(b"data");
250        header_buf.extend_from_slice(&(data_size as u32).to_le_bytes());
251
252        file.seek(std::io::SeekFrom::Start(0)).await?;
253        file.write_all(&header_buf).await?;
254        file.seek(std::io::SeekFrom::End(0)).await?;
255
256        Ok(())
257    }
258
259    pub async fn process_recording(
260        &self,
261        file_path: &Path,
262        mut receiver: UnboundedReceiver<AudioFrame>,
263    ) -> Result<()> {
264        let first_frame = match receiver.recv().await {
265            Some(f) => f,
266            None => return Ok(()),
267        };
268
269        if let Samples::RTP { .. } = first_frame.samples {
270            return self
271                .process_recording_rtp(file_path, receiver, first_frame)
272                .await;
273        }
274
275        let _requested_format = self.option.format.unwrap_or(RecorderFormat::Wav);
276
277        self.process_recording_wav(file_path, receiver, first_frame)
278            .await
279    }
280
281    fn ensure_parent_dir(&self, file_path: &Path) -> Result<()> {
282        if let Some(parent) = file_path.parent() {
283            if !parent.exists() {
284                if let Err(e) = std::fs::create_dir_all(parent) {
285                    warn!(
286                        "Failed to create recording file parent directory: {} {}",
287                        e,
288                        file_path.display()
289                    );
290                    return Err(anyhow!("Failed to create recording file parent directory"));
291                }
292            }
293        }
294        Ok(())
295    }
296
297    async fn create_output_file(&self, file_path: &Path) -> Result<File> {
298        self.ensure_parent_dir(file_path)?;
299        match File::create(file_path).await {
300            Ok(file) => {
301                info!(
302                    session_id = self.session_id,
303                    "recorder: created recording file: {}",
304                    file_path.display()
305                );
306                Ok(file)
307            }
308            Err(e) => {
309                warn!(
310                    "Failed to create recording file: {} {}",
311                    e,
312                    file_path.display()
313                );
314                Err(anyhow!("Failed to create recording file"))
315            }
316        }
317    }
318
319    async fn process_recording_rtp(
320        &self,
321        file_path: &Path,
322        mut receiver: UnboundedReceiver<AudioFrame>,
323        first_frame: AudioFrame,
324    ) -> Result<()> {
325        let (payload_type, mut file) =
326            if let Samples::RTP { payload_type, .. } = &first_frame.samples {
327                let file = self.create_output_file(file_path).await?;
328                (*payload_type, file)
329            } else {
330                return Err(anyhow!("Invalid frame type for RTP recording"));
331            };
332
333        self.update_wav_header(&mut file, Some(payload_type))
334            .await?;
335
336        if let Samples::RTP { payload, .. } = first_frame.samples {
337            file.write_all(&payload).await?;
338            self.samples_written
339                .fetch_add(payload.len(), Ordering::SeqCst);
340        }
341
342        loop {
343            match receiver.recv().await {
344                Some(frame) => {
345                    if let Samples::RTP { payload, .. } = frame.samples {
346                        file.write_all(&payload).await?;
347                        self.samples_written
348                            .fetch_add(payload.len(), Ordering::SeqCst);
349                    }
350                }
351                None => break,
352            }
353        }
354
355        self.update_wav_header(&mut file, Some(payload_type))
356            .await?;
357
358        file.sync_all().await?;
359
360        Ok(())
361    }
362
363    async fn process_recording_wav(
364        &self,
365        file_path: &Path,
366        mut receiver: UnboundedReceiver<AudioFrame>,
367        first_frame: AudioFrame,
368    ) -> Result<()> {
369        let mut file = self.create_output_file(file_path).await?;
370        self.update_wav_header(&mut file, None).await?;
371
372        self.append_frame(first_frame).await.ok();
373
374        if self.native_samplerate {
375            info!(
376                session_id = self.session_id,
377                format = "wav",
378                "Recording to {} in native samplerate mode (rate detected from caller leg)",
379                file_path.display()
380            );
381        } else {
382            let chunk_size = self.compute_chunk_size();
383            info!(
384                session_id = self.session_id,
385                format = "wav",
386                "Recording to {} ptime: {}ms chunk_size: {}",
387                file_path.display(),
388                self.option.ptime,
389                chunk_size
390            );
391        }
392
393        let mut interval = IntervalStream::new(tokio::time::interval(Duration::from_millis(
394            self.option.ptime.max(1) as u64,
395        )));
396        loop {
397            select! {
398                Some(frame) = receiver.recv() => {
399                    self.append_frame(frame).await.ok();
400                }
401                _ = interval.next() => {
402                    let chunk_size = self.compute_chunk_size();
403                    if chunk_size == 0 {
404                        continue;
405                    }
406                    let (mono_buf, stereo_buf) = self.pop(chunk_size).await;
407                    self.process_buffers(&mut file, mono_buf, stereo_buf).await?;
408                    self.update_wav_header(&mut file, None).await?;
409                }
410                _ = self.cancel_token.cancelled() => {
411                    self.flush_buffers(&mut file).await?;
412                    self.update_wav_header(&mut file, None).await?;
413                    return Ok(());
414                }
415            }
416        }
417    }
418
419    fn get_channel_index(&self, track_id: &str) -> usize {
420        if track_id == self.session_id.as_str() {
421            0
422        } else {
423            1
424        }
425    }
426
427    /// Resample `samples` from `src_rate` to `target` using a per-(channel,
428    /// source rate) stateful resampler so each leg keeps a continuous stream.
429    fn normalize_samples(
430        &self,
431        channel: usize,
432        samples: PcmBuf,
433        src_rate: u32,
434        target: u32,
435    ) -> PcmBuf {
436        if src_rate == target || samples.is_empty() {
437            return samples;
438        }
439        let mut resamplers = self.resamplers.lock().unwrap();
440        let resampler = resamplers.entry((channel, src_rate)).or_insert_with(|| {
441            BoxedResampler::new(src_rate as usize, target as usize)
442                .expect("invalid sample rate for resampler")
443        });
444        resampler.resample(&samples)
445    }
446
447    fn push_channel(&self, channel_idx: usize, samples: &[Sample]) {
448        match channel_idx {
449            0 => {
450                let mut mono_buf = self.mono_buf.lock().unwrap();
451                mono_buf.extend_from_slice(samples);
452            }
453            1 => {
454                let mut stereo_buf = self.stereo_buf.lock().unwrap();
455                stereo_buf.extend_from_slice(samples);
456            }
457            _ => {}
458        }
459    }
460
461    /// Normalize pre-latch pending frames once the target rate is known.
462    fn flush_pending(&self, target: u32) {
463        let pending = {
464            let mut pending = self.pending.lock().unwrap();
465            std::mem::take(&mut *pending)
466        };
467        for frame in pending {
468            let normalized =
469                self.normalize_samples(frame.channel, frame.samples, frame.sample_rate, target);
470            self.push_channel(frame.channel, &normalized);
471        }
472    }
473
474    async fn append_frame(&self, frame: AudioFrame) -> Result<()> {
475        let mut samples = match frame.samples {
476            Samples::PCM { samples } => samples,
477            _ => return Ok(()), // ignore non-PCM frames
478        };
479
480        if samples.is_empty() {
481            return Ok(());
482        }
483
484        if frame.channels == 2 {
485            convert_to_mono(&mut samples, 2);
486        }
487
488        let channel_idx = self.get_channel_index(&frame.track_id);
489        let src_rate = if frame.sample_rate > 0 {
490            frame.sample_rate
491        } else {
492            16000
493        };
494
495        if self.native_samplerate {
496            let mut target = self.target_rate.load(Ordering::SeqCst);
497            if target == 0 {
498                // Latch the target rate from the first caller-leg frame; if
499                // only other-leg audio keeps arriving, fall back to the oldest
500                // pending frame's rate to avoid buffering forever.
501                let should_latch = channel_idx == 0 || {
502                    self.pending.lock().unwrap().len() >= NATIVE_PENDING_FRAME_LIMIT
503                };
504                if should_latch {
505                    target = src_rate;
506                    self.target_rate.store(target, Ordering::SeqCst);
507                    info!(
508                        session_id = self.session_id,
509                        native_samplerate = target,
510                        "recorder: detected native samplerate"
511                    );
512                    self.flush_pending(target);
513                } else {
514                    let mut pending = self.pending.lock().unwrap();
515                    pending.push(PendingRaw {
516                        channel: channel_idx,
517                        samples,
518                        sample_rate: src_rate,
519                    });
520                    return Ok(());
521                }
522            }
523            let normalized = self.normalize_samples(channel_idx, samples, src_rate, target);
524            self.push_channel(channel_idx, &normalized);
525            return Ok(());
526        }
527
528        self.push_channel(channel_idx, &samples);
529
530        Ok(())
531    }
532
533    pub(crate) fn extract_samples(buffer: &mut PcmBuf, extract_size: usize) -> PcmBuf {
534        if extract_size > 0 && !buffer.is_empty() {
535            let take_size = extract_size.min(buffer.len());
536            buffer.drain(..take_size).collect()
537        } else {
538            Vec::new()
539        }
540    }
541
542    async fn pop(&self, chunk_size: usize) -> (PcmBuf, PcmBuf) {
543        let mut mono_buf = self.mono_buf.lock().unwrap();
544        let mut stereo_buf = self.stereo_buf.lock().unwrap();
545
546        // Safety cap of buffered samples per pop: 10s worth of audio.
547        let safe_chunk_size = chunk_size.min((self.effective_samplerate() as usize) * 10);
548
549        let mono_result = if mono_buf.len() >= safe_chunk_size {
550            Self::extract_samples(&mut mono_buf, safe_chunk_size)
551        } else if !mono_buf.is_empty() {
552            let available_len = mono_buf.len();
553            let mut result = Self::extract_samples(&mut mono_buf, available_len);
554            if chunk_size != usize::MAX {
555                result.resize(safe_chunk_size, 0);
556            }
557            result
558        } else {
559            if chunk_size != usize::MAX {
560                vec![0; safe_chunk_size]
561            } else {
562                Vec::new()
563            }
564        };
565
566        let stereo_result = if stereo_buf.len() >= safe_chunk_size {
567            Self::extract_samples(&mut stereo_buf, safe_chunk_size)
568        } else if !stereo_buf.is_empty() {
569            let available_len = stereo_buf.len();
570            let mut result = Self::extract_samples(&mut stereo_buf, available_len);
571            if chunk_size != usize::MAX {
572                result.resize(safe_chunk_size, 0);
573            }
574            result
575        } else {
576            if chunk_size != usize::MAX {
577                vec![0; safe_chunk_size]
578            } else {
579                Vec::new()
580            }
581        };
582
583        if chunk_size == usize::MAX {
584            let max_len = mono_result.len().max(stereo_result.len());
585            let mut mono_final = mono_result;
586            let mut stereo_final = stereo_result;
587            mono_final.resize(max_len, 0);
588            stereo_final.resize(max_len, 0);
589            (mono_final, stereo_final)
590        } else {
591            (mono_result, stereo_result)
592        }
593    }
594
595    pub fn stop_recording(&self) -> Result<()> {
596        self.cancel_token.cancel();
597        Ok(())
598    }
599
600    pub(crate) fn mix_buffers(mono_buf: &PcmBuf, stereo_buf: &PcmBuf) -> Vec<i16> {
601        assert_eq!(
602            mono_buf.len(),
603            stereo_buf.len(),
604            "Buffer lengths must be equal after pop()"
605        );
606
607        let len = mono_buf.len();
608        let mut mix_buff = Vec::with_capacity(len * 2);
609
610        for i in 0..len {
611            mix_buff.push(mono_buf[i]);
612            mix_buff.push(stereo_buf[i]);
613        }
614
615        mix_buff
616    }
617
618    async fn write_audio_data(
619        &self,
620        file: &mut File,
621        mono_buf: &PcmBuf,
622        stereo_buf: &PcmBuf,
623    ) -> Result<usize> {
624        let max_len = mono_buf.len().max(stereo_buf.len());
625        if max_len == 0 {
626            return Ok(0);
627        }
628
629        let mix_buff = Self::mix_buffers(mono_buf, stereo_buf);
630
631        file.seek(std::io::SeekFrom::End(0)).await?;
632        file.write_all(&samples_to_bytes(&mix_buff)).await?;
633
634        Ok(max_len)
635    }
636
637    async fn process_buffers(
638        &self,
639        file: &mut File,
640        mono_buf: PcmBuf,
641        stereo_buf: PcmBuf,
642    ) -> Result<()> {
643        if mono_buf.is_empty() && stereo_buf.is_empty() {
644            return Ok(());
645        }
646        let samples_written = self.write_audio_data(file, &mono_buf, &stereo_buf).await?;
647        if samples_written > 0 {
648            self.samples_written
649                .fetch_add(samples_written * 4, Ordering::SeqCst);
650        }
651        Ok(())
652    }
653
654    async fn flush_buffers(&self, file: &mut File) -> Result<()> {
655        loop {
656            let (mono_buf, stereo_buf) = self.pop(usize::MAX).await;
657
658            if mono_buf.is_empty() && stereo_buf.is_empty() {
659                break;
660            }
661
662            let samples_written = self.write_audio_data(file, &mono_buf, &stereo_buf).await?;
663            if samples_written > 0 {
664                self.samples_written
665                    .fetch_add(samples_written * 4, Ordering::SeqCst);
666            }
667        }
668
669        Ok(())
670    }
671}