Skip to main content

active_call/media/
recorder.rs

1use anyhow::{Result, anyhow};
2use audio_codec::{PcmBuf, samples_to_bytes};
3use futures::StreamExt;
4use serde::{Deserialize, Serialize};
5use std::{
6    path::Path,
7    sync::{
8        Mutex,
9        atomic::{AtomicUsize, Ordering},
10    },
11    time::Duration,
12    u32,
13};
14use tokio::{
15    fs::File,
16    io::{AsyncSeekExt, AsyncWriteExt},
17    select,
18    sync::mpsc::UnboundedReceiver,
19};
20use tokio_stream::wrappers::IntervalStream;
21use tokio_util::sync::CancellationToken;
22use tracing::{info, warn};
23
24use crate::media::{AudioFrame, Samples};
25
26#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
27#[serde(rename_all = "lowercase")]
28pub enum RecorderFormat {
29    Wav,
30    Pcm,
31    Pcmu,
32    Pcma,
33    G722,
34}
35
36impl RecorderFormat {
37    pub fn extension(&self) -> &'static str {
38        "wav"
39    }
40
41    pub fn is_supported(&self) -> bool {
42        true
43    }
44
45    pub fn effective(&self) -> RecorderFormat {
46        *self
47    }
48}
49
50impl Default for RecorderFormat {
51    fn default() -> Self {
52        RecorderFormat::Wav
53    }
54}
55
56#[derive(Debug, Deserialize, Serialize, Clone)]
57#[serde(rename_all = "camelCase")]
58#[serde(default)]
59pub struct RecorderOption {
60    #[serde(default)]
61    pub recorder_file: String,
62    #[serde(default)]
63    pub samplerate: u32,
64    #[serde(default)]
65    pub ptime: u32,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub format: Option<RecorderFormat>,
68}
69
70impl RecorderOption {
71    pub fn new(recorder_file: String) -> Self {
72        Self {
73            recorder_file,
74            ..Default::default()
75        }
76    }
77
78    pub fn resolved_format(&self, default: RecorderFormat) -> RecorderFormat {
79        self.format.unwrap_or(default).effective()
80    }
81
82    pub fn ensure_path_extension(&mut self, fallback_format: RecorderFormat) {
83        let effective_format = self.format.unwrap_or(fallback_format).effective();
84        self.format = Some(effective_format);
85
86        if self.recorder_file.is_empty() {
87            return;
88        }
89
90        let extension = effective_format.extension();
91        if !self
92            .recorder_file
93            .to_lowercase()
94            .ends_with(&format!(".{}", extension.to_lowercase()))
95        {
96            self.recorder_file = format!("{}.{}", self.recorder_file, extension);
97        }
98    }
99}
100
101impl Default for RecorderOption {
102    fn default() -> Self {
103        Self {
104            recorder_file: "".to_string(),
105            samplerate: 16000,
106            ptime: 200,
107            format: None,
108        }
109    }
110}
111
112pub struct Recorder {
113    session_id: String,
114    option: RecorderOption,
115    samples_written: AtomicUsize,
116    cancel_token: CancellationToken,
117    stereo_buf: Mutex<PcmBuf>,
118    mono_buf: Mutex<PcmBuf>,
119}
120
121impl Recorder {
122    pub fn new(
123        cancel_token: CancellationToken,
124        session_id: String,
125        option: RecorderOption,
126    ) -> Self {
127        Self {
128            session_id,
129            option,
130            samples_written: AtomicUsize::new(0),
131            cancel_token,
132            stereo_buf: Mutex::new(Vec::new()),
133            mono_buf: Mutex::new(Vec::new()),
134        }
135    }
136
137    async fn update_wav_header(&self, file: &mut File, payload_type: Option<u8>) -> Result<()> {
138        let total = self.samples_written.load(Ordering::SeqCst);
139
140        let (format_tag, sample_rate, channels, bits_per_sample, data_size): (
141            u16,
142            u32,
143            u16,
144            u16,
145            usize,
146        ) = match payload_type {
147            Some(pt) => {
148                let (tag, rate, chan): (u16, u32, u16) = match pt {
149                    0 => (0x0007, 8000, 1),   // PCMU
150                    8 => (0x0006, 8000, 1),   // PCMA
151                    9 => (0x0064, 16000, 1),  // G722
152                    10 => (0x0001, 44100, 2), // L16 Stereo 44.1k
153                    11 => (0x0001, 44100, 1), // L16 Mono 44.1k
154                    _ => (0x0001, 16000, 1),  // Default to PCM 16k Mono
155                };
156                let bits: u16 = match pt {
157                    9 => 4,
158                    0 | 8 => 8,
159                    _ => 16,
160                };
161                (tag, rate, chan, bits, total)
162            }
163            None => (0x0001, self.option.samplerate, 2, 16, total),
164        };
165
166        let mut header_buf = Vec::new();
167        header_buf.extend_from_slice(b"RIFF");
168        let file_size = data_size + 36;
169        header_buf.extend_from_slice(&(file_size as u32).to_le_bytes());
170        header_buf.extend_from_slice(b"WAVE");
171
172        header_buf.extend_from_slice(b"fmt ");
173        header_buf.extend_from_slice(&16u32.to_le_bytes());
174        header_buf.extend_from_slice(&format_tag.to_le_bytes());
175        header_buf.extend_from_slice(&(channels as u16).to_le_bytes());
176        header_buf.extend_from_slice(&sample_rate.to_le_bytes());
177
178        let bytes_per_sec: u32 = match format_tag {
179            0x0064 => 8000, // G.722 is 64kbps
180            _ => sample_rate * (channels as u32) * (bits_per_sample as u32 / 8),
181        };
182        header_buf.extend_from_slice(&bytes_per_sec.to_le_bytes());
183
184        let block_align: u16 = match format_tag {
185            0x0064 | 0x0007 | 0x0006 => 1 * channels,
186            _ => (bits_per_sample / 8) * channels,
187        };
188        header_buf.extend_from_slice(&block_align.to_le_bytes());
189        header_buf.extend_from_slice(&bits_per_sample.to_le_bytes());
190
191        header_buf.extend_from_slice(b"data");
192        header_buf.extend_from_slice(&(data_size as u32).to_le_bytes());
193
194        file.seek(std::io::SeekFrom::Start(0)).await?;
195        file.write_all(&header_buf).await?;
196        file.seek(std::io::SeekFrom::End(0)).await?;
197
198        Ok(())
199    }
200
201    pub async fn process_recording(
202        &self,
203        file_path: &Path,
204        mut receiver: UnboundedReceiver<AudioFrame>,
205    ) -> Result<()> {
206        let first_frame = match receiver.recv().await {
207            Some(f) => f,
208            None => return Ok(()),
209        };
210
211        if let Samples::RTP { .. } = first_frame.samples {
212            return self
213                .process_recording_rtp(file_path, receiver, first_frame)
214                .await;
215        }
216
217        let _requested_format = self.option.format.unwrap_or(RecorderFormat::Wav);
218
219        self.process_recording_wav(file_path, receiver, first_frame)
220            .await
221    }
222
223    fn ensure_parent_dir(&self, file_path: &Path) -> Result<()> {
224        if let Some(parent) = file_path.parent() {
225            if !parent.exists() {
226                if let Err(e) = std::fs::create_dir_all(parent) {
227                    warn!(
228                        "Failed to create recording file parent directory: {} {}",
229                        e,
230                        file_path.display()
231                    );
232                    return Err(anyhow!("Failed to create recording file parent directory"));
233                }
234            }
235        }
236        Ok(())
237    }
238
239    async fn create_output_file(&self, file_path: &Path) -> Result<File> {
240        self.ensure_parent_dir(file_path)?;
241        match File::create(file_path).await {
242            Ok(file) => {
243                info!(
244                    session_id = self.session_id,
245                    "recorder: created recording file: {}",
246                    file_path.display()
247                );
248                Ok(file)
249            }
250            Err(e) => {
251                warn!(
252                    "Failed to create recording file: {} {}",
253                    e,
254                    file_path.display()
255                );
256                Err(anyhow!("Failed to create recording file"))
257            }
258        }
259    }
260
261    async fn process_recording_rtp(
262        &self,
263        file_path: &Path,
264        mut receiver: UnboundedReceiver<AudioFrame>,
265        first_frame: AudioFrame,
266    ) -> Result<()> {
267        let (payload_type, mut file) =
268            if let Samples::RTP { payload_type, .. } = &first_frame.samples {
269                let file = self.create_output_file(file_path).await?;
270                (*payload_type, file)
271            } else {
272                return Err(anyhow!("Invalid frame type for RTP recording"));
273            };
274
275        self.update_wav_header(&mut file, Some(payload_type))
276            .await?;
277
278        if let Samples::RTP { payload, .. } = first_frame.samples {
279            file.write_all(&payload).await?;
280            self.samples_written
281                .fetch_add(payload.len(), Ordering::SeqCst);
282        }
283
284        loop {
285            match receiver.recv().await {
286                Some(frame) => {
287                    if let Samples::RTP { payload, .. } = frame.samples {
288                        file.write_all(&payload).await?;
289                        self.samples_written
290                            .fetch_add(payload.len(), Ordering::SeqCst);
291                    }
292                }
293                None => break,
294            }
295        }
296
297        self.update_wav_header(&mut file, Some(payload_type))
298            .await?;
299
300        file.sync_all().await?;
301
302        Ok(())
303    }
304
305    async fn process_recording_wav(
306        &self,
307        file_path: &Path,
308        mut receiver: UnboundedReceiver<AudioFrame>,
309        first_frame: AudioFrame,
310    ) -> Result<()> {
311        let mut file = self.create_output_file(file_path).await?;
312        self.update_wav_header(&mut file, None).await?;
313
314        self.append_frame(first_frame).await.ok();
315
316        let chunk_size = (self.option.samplerate / 1000 * self.option.ptime) as usize;
317        info!(
318            session_id = self.session_id,
319            format = "wav",
320            "Recording to {} ptime: {}ms chunk_size: {}",
321            file_path.display(),
322            self.option.ptime,
323            chunk_size
324        );
325
326        let mut interval = IntervalStream::new(tokio::time::interval(Duration::from_millis(
327            self.option.ptime as u64,
328        )));
329        loop {
330            select! {
331                Some(frame) = receiver.recv() => {
332                    self.append_frame(frame).await.ok();
333                }
334                _ = interval.next() => {
335                    let (mono_buf, stereo_buf) = self.pop(chunk_size).await;
336                    self.process_buffers(&mut file, mono_buf, stereo_buf).await?;
337                    self.update_wav_header(&mut file, None).await?;
338                }
339                _ = self.cancel_token.cancelled() => {
340                    self.flush_buffers(&mut file).await?;
341                    self.update_wav_header(&mut file, None).await?;
342                    return Ok(());
343                }
344            }
345        }
346    }
347
348    fn get_channel_index(&self, track_id: &str) -> usize {
349        if track_id == self.session_id.as_str() {
350            0
351        } else {
352            1
353        }
354    }
355
356    async fn append_frame(&self, frame: AudioFrame) -> Result<()> {
357        let buffer = match frame.samples {
358            Samples::PCM { samples } => samples,
359            _ => return Ok(()), // ignore non-PCM frames
360        };
361
362        if buffer.is_empty() {
363            return Ok(());
364        }
365
366        let channel_idx = self.get_channel_index(&frame.track_id);
367        match channel_idx {
368            0 => {
369                let mut mono_buf = self.mono_buf.lock().unwrap();
370                mono_buf.extend(buffer.iter());
371            }
372            1 => {
373                let mut stereo_buf = self.stereo_buf.lock().unwrap();
374                stereo_buf.extend(buffer.iter());
375            }
376            _ => {}
377        }
378
379        Ok(())
380    }
381
382    pub(crate) fn extract_samples(buffer: &mut PcmBuf, extract_size: usize) -> PcmBuf {
383        if extract_size > 0 && !buffer.is_empty() {
384            let take_size = extract_size.min(buffer.len());
385            buffer.drain(..take_size).collect()
386        } else {
387            Vec::new()
388        }
389    }
390
391    async fn pop(&self, chunk_size: usize) -> (PcmBuf, PcmBuf) {
392        let mut mono_buf = self.mono_buf.lock().unwrap();
393        let mut stereo_buf = self.stereo_buf.lock().unwrap();
394
395        let safe_chunk_size = chunk_size.min(16000 * 10);
396
397        let mono_result = if mono_buf.len() >= safe_chunk_size {
398            Self::extract_samples(&mut mono_buf, safe_chunk_size)
399        } else if !mono_buf.is_empty() {
400            let available_len = mono_buf.len();
401            let mut result = Self::extract_samples(&mut mono_buf, available_len);
402            if chunk_size != usize::MAX {
403                result.resize(safe_chunk_size, 0);
404            }
405            result
406        } else {
407            if chunk_size != usize::MAX {
408                vec![0; safe_chunk_size]
409            } else {
410                Vec::new()
411            }
412        };
413
414        let stereo_result = if stereo_buf.len() >= safe_chunk_size {
415            Self::extract_samples(&mut stereo_buf, safe_chunk_size)
416        } else if !stereo_buf.is_empty() {
417            let available_len = stereo_buf.len();
418            let mut result = Self::extract_samples(&mut stereo_buf, available_len);
419            if chunk_size != usize::MAX {
420                result.resize(safe_chunk_size, 0);
421            }
422            result
423        } else {
424            if chunk_size != usize::MAX {
425                vec![0; safe_chunk_size]
426            } else {
427                Vec::new()
428            }
429        };
430
431        if chunk_size == usize::MAX {
432            let max_len = mono_result.len().max(stereo_result.len());
433            let mut mono_final = mono_result;
434            let mut stereo_final = stereo_result;
435            mono_final.resize(max_len, 0);
436            stereo_final.resize(max_len, 0);
437            (mono_final, stereo_final)
438        } else {
439            (mono_result, stereo_result)
440        }
441    }
442
443    pub fn stop_recording(&self) -> Result<()> {
444        self.cancel_token.cancel();
445        Ok(())
446    }
447
448    pub(crate) fn mix_buffers(mono_buf: &PcmBuf, stereo_buf: &PcmBuf) -> Vec<i16> {
449        assert_eq!(
450            mono_buf.len(),
451            stereo_buf.len(),
452            "Buffer lengths must be equal after pop()"
453        );
454
455        let len = mono_buf.len();
456        let mut mix_buff = Vec::with_capacity(len * 2);
457
458        for i in 0..len {
459            mix_buff.push(mono_buf[i]);
460            mix_buff.push(stereo_buf[i]);
461        }
462
463        mix_buff
464    }
465
466    async fn write_audio_data(
467        &self,
468        file: &mut File,
469        mono_buf: &PcmBuf,
470        stereo_buf: &PcmBuf,
471    ) -> Result<usize> {
472        let max_len = mono_buf.len().max(stereo_buf.len());
473        if max_len == 0 {
474            return Ok(0);
475        }
476
477        let mix_buff = Self::mix_buffers(mono_buf, stereo_buf);
478
479        file.seek(std::io::SeekFrom::End(0)).await?;
480        file.write_all(&samples_to_bytes(&mix_buff)).await?;
481
482        Ok(max_len)
483    }
484
485    async fn process_buffers(
486        &self,
487        file: &mut File,
488        mono_buf: PcmBuf,
489        stereo_buf: PcmBuf,
490    ) -> Result<()> {
491        if mono_buf.is_empty() && stereo_buf.is_empty() {
492            return Ok(());
493        }
494        let samples_written = self.write_audio_data(file, &mono_buf, &stereo_buf).await?;
495        if samples_written > 0 {
496            self.samples_written
497                .fetch_add(samples_written * 4, Ordering::SeqCst);
498        }
499        Ok(())
500    }
501
502    async fn flush_buffers(&self, file: &mut File) -> Result<()> {
503        loop {
504            let (mono_buf, stereo_buf) = self.pop(usize::MAX).await;
505
506            if mono_buf.is_empty() && stereo_buf.is_empty() {
507                break;
508            }
509
510            let samples_written = self.write_audio_data(file, &mono_buf, &stereo_buf).await?;
511            if samples_written > 0 {
512                self.samples_written
513                    .fetch_add(samples_written * 4, Ordering::SeqCst);
514            }
515        }
516
517        Ok(())
518    }
519}