rustpbx 0.3.19

A SIP PBX implementation in Rust
Documentation
#[cfg(test)]
mod audio_feature_tests {
    use audio_codec::CodecType;
    use rustpbx::media::{FileTrack, Track, audio_source::AudioSourceManager};
    use std::fs::File;
    use std::io::Write;

    #[tokio::test]
    async fn test_file_track_supports_multiple_formats() {
        let temp_dir = std::env::temp_dir();

        let wav_path = temp_dir.join("test_audio.wav");
        let mp3_path = temp_dir.join("test_audio.mp3");
        let raw_path = temp_dir.join("test_audio.pcmu");

        create_dummy_wav(&wav_path).unwrap();
        create_dummy_mp3(&mp3_path).unwrap();
        create_dummy_raw(&raw_path).unwrap();

        let track_wav = FileTrack::new("wav-test".to_string())
            .with_path(wav_path.to_string_lossy().to_string())
            .with_loop(false);

        let track_mp3 = FileTrack::new("mp3-test".to_string())
            .with_path(mp3_path.to_string_lossy().to_string())
            .with_loop(false);

        let track_raw = FileTrack::new("raw-test".to_string())
            .with_path(raw_path.to_string_lossy().to_string())
            .with_loop(false);

        assert!(
            track_wav.local_description().await.is_ok(),
            "Wav format should be supported"
        );
        assert!(
            track_mp3.local_description().await.is_ok(),
            "Mp3 format should be supported"
        );
        assert!(
            track_raw.local_description().await.is_ok(),
            "Raw PCM is should be supported"
        );

        std::fs::remove_file(wav_path).ok();
        std::fs::remove_file(mp3_path).ok();
        std::fs::remove_file(raw_path).ok();
    }

    #[tokio::test]
    async fn test_file_track_loop_playback() {
        let temp_dir = std::env::temp_dir();
        let audio_path = temp_dir.join("loop_test.pcmu");
        create_dummy_raw(&audio_path).unwrap();

        let track_loop = FileTrack::new("loop-test".to_string())
            .with_path(audio_path.to_string_lossy().to_string())
            .with_loop(true);

        let track_no_loop = FileTrack::new("no-loop-test".to_string())
            .with_path(audio_path.to_string_lossy().to_string())
            .with_loop(false);

        assert!(
            track_loop.local_description().await.is_ok(),
            "Must support looped playback"
        );
        assert!(
            track_no_loop.local_description().await.is_ok(),
            "Must support non-looped playback"
        );

        std::fs::remove_file(audio_path).ok();
    }

    #[tokio::test]
    async fn test_file_track_codec_preference() {
        let temp_dir = std::env::temp_dir();
        let audio_path = temp_dir.join("codec_test.pcmu");
        create_dummy_raw(&audio_path).unwrap();

        let track_pcmu = FileTrack::new("pcmu-test".to_string())
            .with_path(audio_path.to_string_lossy().to_string())
            .with_codec_preference(vec![CodecType::PCMU]);

        let track_pcma = FileTrack::new("pcma-test".to_string())
            .with_path(audio_path.to_string_lossy().to_string())
            .with_codec_preference(vec![CodecType::PCMA]);

        let offer_pcmu = track_pcmu.local_description().await.unwrap();
        let offer_pcma = track_pcma.local_description().await.unwrap();

        assert!(
            offer_pcmu.contains("PCMU") || offer_pcmu.contains("0"),
            "PCMU must be in the offer"
        );
        assert!(
            offer_pcma.contains("PCMA") || offer_pcma.contains("8"),
            "PCMA must be in the offer"
        );

        std::fs::remove_file(audio_path).ok();
    }

    #[tokio::test]
    async fn test_audio_source_manager_file_switching() {
        let temp_dir = std::env::temp_dir();
        let audio1_path = temp_dir.join("switch_test1.pcmu");
        let audio2_path = temp_dir.join("switch_test2.pcmu");

        create_dummy_raw(&audio1_path).unwrap();
        create_dummy_raw(&audio2_path).unwrap();

        let manager = AudioSourceManager::new(8000);

        assert!(
            manager
                .switch_to_file(audio1_path.to_string_lossy().to_string(), false)
                .is_ok(),
            "Must be able to switch to first audio file (no loop)"
        );

        assert!(
            manager
                .switch_to_file(audio2_path.to_string_lossy().to_string(), true)
                .is_ok(),
            "Must be able to switch to second audio file (with loop)"
        );

        std::fs::remove_file(audio1_path).ok();
        std::fs::remove_file(audio2_path).ok();
    }

    fn create_dummy_wav(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
        let spec = hound::WavSpec {
            channels: 1,
            sample_rate: 8000,
            bits_per_sample: 16,
            sample_format: hound::SampleFormat::Int,
        };

        let mut writer = hound::WavWriter::create(path, spec)?;

        for _ in 0..1600 {
            writer.write_sample(0i16)?;
        }

        writer.finalize()?;
        Ok(())
    }

    fn create_dummy_mp3(path: &std::path::Path) -> std::io::Result<()> {
        let mut file = File::create(path)?;
        let mp3_header = vec![
            0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00,
        ];
        file.write_all(&mp3_header)?;
        for _ in 0..100 {
            file.write_all(&[0u8; 32])?;
        }
        Ok(())
    }

    fn create_dummy_raw(path: &std::path::Path) -> std::io::Result<()> {
        let mut file = File::create(path)?;
        let silence: Vec<u8> = vec![0xFF; 1600];
        file.write_all(&silence)?;
        Ok(())
    }
}