mod common;
use std::time::Duration;
use anyhow::Result;
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::{AudioError, AudioResult, PlatformAudio, RtcAudioSource};
use serial_test::serial;
#[cfg(feature = "__lk-e2e-test")]
use anyhow::anyhow;
#[cfg(feature = "__lk-e2e-test")]
use common::test_rooms;
#[cfg(feature = "__lk-e2e-test")]
use livekit::options::TrackPublishOptions;
#[cfg(feature = "__lk-e2e-test")]
use livekit::prelude::{ConnectionState, LocalAudioTrack, LocalTrack, RoomEvent, TrackSource};
#[cfg(feature = "__lk-e2e-test")]
use tokio::time::timeout;
fn try_create_platform_audio(test_name: &str) -> Option<PlatformAudio> {
match PlatformAudio::new() {
Ok(audio) => Some(audio),
Err(err) => {
log::info!("Skipping {test_name} - PlatformAudio unavailable: {err}");
None
}
}
}
fn try_acquire_platform_adm(
pcf: &libwebrtc::peer_connection_factory::PeerConnectionFactory,
test_name: &str,
) -> bool {
if pcf.acquire_platform_adm() {
true
} else {
log::info!("Skipping {test_name} - Platform ADM unavailable on this environment");
false
}
}
#[test]
fn test_audio_error_display() {
let err = AudioError::PlatformInitFailed;
let msg = format!("{}", err);
assert!(msg.contains("platform audio"));
let err = AudioError::InvalidDeviceIndex;
let msg = format!("{}", err);
assert!(msg.contains("Invalid device index"));
let err = AudioError::OperationFailed("test message".to_string());
let msg = format!("{}", err);
assert!(msg.contains("test message"));
}
#[test]
fn test_audio_error_equality() {
assert_eq!(AudioError::PlatformInitFailed, AudioError::PlatformInitFailed);
assert_eq!(AudioError::InvalidDeviceIndex, AudioError::InvalidDeviceIndex);
assert_eq!(
AudioError::OperationFailed("a".to_string()),
AudioError::OperationFailed("a".to_string())
);
assert_ne!(
AudioError::OperationFailed("a".to_string()),
AudioError::OperationFailed("b".to_string())
);
}
#[test]
fn test_audio_error_clone() {
let err = AudioError::OperationFailed("test".to_string());
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn test_audio_result_ok() {
let result: AudioResult<i32> = Ok(42);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_audio_result_err() {
let result: AudioResult<i32> = Err(AudioError::InvalidDeviceIndex);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), AudioError::InvalidDeviceIndex);
}
#[test]
fn test_rtc_audio_source_device() {
let source = RtcAudioSource::Device;
assert!(matches!(source, RtcAudioSource::Device));
}
#[test]
fn test_audio_processing_options_default() {
use livekit::AudioProcessingOptions;
let opts = AudioProcessingOptions::default();
assert!(opts.echo_cancellation);
assert!(opts.noise_suppression);
assert!(opts.auto_gain_control);
assert!(!opts.prefer_hardware_processing); }
#[test]
fn test_audio_processing_options_custom() {
use livekit::AudioProcessingOptions;
let opts = AudioProcessingOptions {
echo_cancellation: false,
noise_suppression: true,
auto_gain_control: false,
prefer_hardware_processing: true,
};
assert!(!opts.echo_cancellation);
assert!(opts.noise_suppression);
assert!(!opts.auto_gain_control);
assert!(opts.prefer_hardware_processing);
}
#[test]
fn test_audio_processing_type_default() {
use livekit::AudioProcessingType;
let atype = AudioProcessingType::default();
assert_eq!(atype, AudioProcessingType::Software);
}
#[test]
fn test_audio_processing_type_variants() {
use livekit::AudioProcessingType;
let hw = AudioProcessingType::Hardware;
let sw = AudioProcessingType::Software;
let none = AudioProcessingType::None;
assert_ne!(hw, sw);
assert_ne!(sw, none);
assert_ne!(hw, none);
assert!(format!("{:?}", hw).contains("Hardware"));
assert!(format!("{:?}", sw).contains("Software"));
assert!(format!("{:?}", none).contains("None"));
}
#[test]
fn test_audio_processing_options_clone() {
use livekit::AudioProcessingOptions;
let opts = AudioProcessingOptions {
echo_cancellation: false,
noise_suppression: true,
auto_gain_control: false,
prefer_hardware_processing: true,
};
let cloned = opts.clone();
assert_eq!(opts, cloned);
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_creation() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_standalone_creation") else {
return Ok(());
};
log::info!("PlatformAudio created successfully");
assert_eq!(audio.ref_count(), 1);
log::info!("Initial ref_count: {}", audio.ref_count());
assert!(matches!(audio.rtc_source(), RtcAudioSource::Device));
log::info!("rtc_source() returns Device variant");
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
log::info!(
"Found {} recording devices, {} playout devices",
recording_devices.len(),
playout_devices.len()
);
for device in &recording_devices {
log::info!(" Recording device {}: {} (ID: {})", device.index, device.name, device.id);
}
for device in &playout_devices {
log::info!(" Playout device {}: {} (ID: {})", device.index, device.name, device.id);
}
let debug_str = format!("{:?}", audio);
log::info!("Debug output: {}", debug_str);
assert!(debug_str.contains("PlatformAudio"));
drop(audio);
log::info!("PlatformAudio dropped successfully");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_ref_counting() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio1) = try_create_platform_audio("test_platform_audio_standalone_ref_counting")
else {
return Ok(());
};
assert_eq!(audio1.ref_count(), 1);
log::info!("Created audio1, ref_count: {}", audio1.ref_count());
let audio2 = PlatformAudio::new()?;
assert_eq!(audio1.ref_count(), 2);
assert_eq!(audio2.ref_count(), 2);
log::info!("Created audio2, ref_count: {}", audio1.ref_count());
let audio3 = audio1.clone();
assert_eq!(audio1.ref_count(), 3);
assert_eq!(audio2.ref_count(), 3);
assert_eq!(audio3.ref_count(), 3);
log::info!("Cloned audio1 to audio3, ref_count: {}", audio1.ref_count());
drop(audio2);
assert_eq!(audio1.ref_count(), 2);
log::info!("Dropped audio2, ref_count: {}", audio1.ref_count());
drop(audio1);
assert_eq!(audio3.ref_count(), 1);
log::info!("Dropped audio1, audio3 ref_count: {}", audio3.ref_count());
drop(audio3);
log::info!("Dropped audio3, all references released");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_device_selection() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_standalone_device_selection")
else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
if let Some(device) = recording_devices.first() {
audio.set_recording_device(&device.id)?;
log::info!("Selected recording device: {} (ID: {})", device.name, device.id);
} else {
log::info!("No recording devices available");
}
if let Some(device) = playout_devices.first() {
audio.set_playout_device(&device.id)?;
log::info!("Selected playout device: {} (ID: {})", device.name, device.id);
} else {
log::info!("No playout devices available");
}
log::info!("Type-safe device selection verified");
drop(audio);
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_processing_config() -> Result<()> {
use livekit::reset_platform_audio;
use livekit::AudioProcessingOptions;
use livekit::AudioProcessingType;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_standalone_processing_config")
else {
return Ok(());
};
let hw_aec = audio.is_hardware_aec_available();
let hw_agc = audio.is_hardware_agc_available();
let hw_ns = audio.is_hardware_ns_available();
log::info!("Hardware availability: AEC={}, AGC={}, NS={}", hw_aec, hw_agc, hw_ns);
let aec_type = audio.active_aec_type();
let agc_type = audio.active_agc_type();
let ns_type = audio.active_ns_type();
log::info!("Active processing: AEC={:?}, AGC={:?}, NS={:?}", aec_type, agc_type, ns_type);
if hw_aec {
assert_eq!(aec_type, AudioProcessingType::Hardware);
} else {
assert_eq!(aec_type, AudioProcessingType::Software);
}
audio.configure_audio_processing(AudioProcessingOptions::default())?;
log::info!("Configured with default options");
audio.configure_audio_processing(AudioProcessingOptions {
echo_cancellation: true,
noise_suppression: false,
auto_gain_control: true,
prefer_hardware_processing: false,
})?;
log::info!("Configured with custom options");
audio.set_echo_cancellation(true, false)?;
log::info!("Set AEC: enabled, prefer software");
audio.set_auto_gain_control(true, false)?;
log::info!("Set AGC: enabled, prefer software");
audio.set_noise_suppression(true, false)?;
log::info!("Set NS: enabled, prefer software");
drop(audio);
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_reset() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio1) = try_create_platform_audio("test_platform_audio_standalone_reset") else {
return Ok(());
};
assert_eq!(audio1.ref_count(), 1);
log::info!("Created audio1, ref_count: {}", audio1.ref_count());
reset_platform_audio();
log::info!("Called reset_platform_audio()");
assert_eq!(audio1.ref_count(), 1);
log::info!("audio1 ref_count after reset: {}", audio1.ref_count());
let audio2 = PlatformAudio::new()?;
assert_eq!(audio2.ref_count(), 1); assert_eq!(audio1.ref_count(), 1); log::info!("Created audio2 after reset, audio2.ref_count: {}", audio2.ref_count());
log::info!("audio1.ref_count still: {}", audio1.ref_count());
let audio3 = PlatformAudio::new()?;
assert_eq!(audio2.ref_count(), 2); assert_eq!(audio3.ref_count(), 2);
assert_eq!(audio1.ref_count(), 1); log::info!(
"Created audio3, audio2/3 ref_count: {}, audio1 ref_count: {}",
audio2.ref_count(),
audio1.ref_count()
);
drop(audio1);
drop(audio2);
drop(audio3);
log::info!("reset_platform_audio test completed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_standalone_lifecycle() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
log::info!("=== Phase 1: Create and Configure ===");
let Some(audio) = try_create_platform_audio("test_platform_audio_standalone_lifecycle") else {
return Ok(());
};
log::info!("Created PlatformAudio");
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
log::info!("Devices: {} recording, {} playout", recording_devices.len(), playout_devices.len());
if let Some(device) = recording_devices.first() {
audio.set_recording_device(&device.id)?;
log::info!("Set recording device to {}", device.name);
}
if let Some(device) = playout_devices.first() {
audio.set_playout_device(&device.id)?;
log::info!("Set playout device to {}", device.name);
}
log::info!("=== Phase 2: Get Audio Source ===");
let source = audio.rtc_source();
assert!(matches!(source, RtcAudioSource::Device));
log::info!("Got RtcAudioSource::Device for track creation");
log::info!("=== Phase 3: Simulated Activity ===");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
log::info!("Simulated 100ms of activity");
log::info!("=== Phase 4: Cleanup ===");
audio.release();
log::info!("Called release(), PlatformAudio destroyed");
log::info!("=== Phase 5: Verify Re-creation ===");
let audio2 = PlatformAudio::new()?;
assert_eq!(audio2.ref_count(), 1);
log::info!("Created new PlatformAudio successfully");
drop(audio2);
log::info!("Lifecycle test completed successfully");
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_creation() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_creation") else {
return Ok(());
};
assert_eq!(audio.ref_count(), 1);
assert!(matches!(audio.rtc_source(), RtcAudioSource::Device));
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
log::info!(
"PlatformAudio: {} recording devices, {} playout devices",
recording_devices.len(),
playout_devices.len()
);
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_ref_counting() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio1) = try_create_platform_audio("test_platform_audio_ref_counting") else {
return Ok(());
};
assert_eq!(audio1.ref_count(), 1);
let audio2 = PlatformAudio::new()?;
assert_eq!(audio1.ref_count(), 2);
assert_eq!(audio2.ref_count(), 2);
let audio3 = audio1.clone();
assert_eq!(audio1.ref_count(), 3);
drop(audio2);
assert_eq!(audio1.ref_count(), 2);
drop(audio1);
drop(audio3);
log::info!("Reference counting works correctly");
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_device_enumeration() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_device_enumeration") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
log::info!("Recording devices: {}", recording_devices.len());
for device in &recording_devices {
log::info!(" Mic {}: {} (ID: {})", device.index, device.name, device.id);
}
let playout_devices: Vec<_> = audio.playout_devices().collect();
log::info!("Playout devices: {}", playout_devices.len());
for device in &playout_devices {
log::info!(" Speaker {}: {} (ID: {})", device.index, device.name, device.id);
}
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_device_selection() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_device_selection") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
if let Some(device) = recording_devices.first() {
audio.set_recording_device(&device.id)?;
log::info!("Selected recording device: {}", device.name);
}
if let Some(device) = playout_devices.first() {
audio.set_playout_device(&device.id)?;
log::info!("Selected playout device: {}", device.name);
}
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_release() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_release") else {
return Ok(());
};
assert_eq!(audio.ref_count(), 1);
audio.release();
log::info!("Explicit release works");
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_with_native_source() -> Result<()> {
use livekit::reset_platform_audio;
use livekit::webrtc::audio_source::native::NativeAudioSource;
use livekit::webrtc::audio_source::AudioSourceOptions;
reset_platform_audio();
let Some(mic) = try_create_platform_audio("test_platform_audio_with_native_source") else {
return Ok(());
};
log::info!("Created PlatformAudio with {} mics", mic.recording_devices().count());
let screen_source = NativeAudioSource::new(AudioSourceOptions::default(), 48000, 2, 100);
let mic_source = mic.rtc_source();
let screen_rtc_source = RtcAudioSource::Native(screen_source.clone());
assert!(matches!(mic_source, RtcAudioSource::Device));
assert!(matches!(screen_rtc_source, RtcAudioSource::Native(_)));
let recording_count = mic.recording_devices().count();
log::info!("PlatformAudio ({} mics) and NativeAudioSource can coexist", recording_count);
drop(mic);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_room_connection() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_room_connection") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
log::info!("Connecting to room with {} recording devices", recording_devices.len());
let mut rooms = test_rooms(1).await?;
let (room, _events) = rooms.pop().unwrap();
assert_eq!(room.connection_state(), ConnectionState::Connected);
if let Some(device) = recording_devices.first() {
audio.set_recording_device(&device.id)?;
let track = LocalAudioTrack::create_audio_track("microphone", audio.rtc_source());
room.local_participant()
.publish_track(
LocalTrack::Audio(track),
TrackPublishOptions { source: TrackSource::Microphone, ..Default::default() },
)
.await?;
log::info!("Published audio track using PlatformAudio");
let publications = room.local_participant().track_publications();
assert!(
publications.values().any(|p| p.source() == TrackSource::Microphone),
"Microphone track should be published"
);
} else {
log::info!("Skipping track publish - no microphone available");
}
room.close().await?;
drop(audio);
log::info!("Room connection test completed");
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_two_participants() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_two_participants") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
if recording_devices.is_empty() {
log::info!("Skipping two participants test - no microphone available");
drop(audio);
return Ok(());
}
audio.set_recording_device(&recording_devices[0].id)?;
let mut rooms = test_rooms(2).await?;
let (pub_room, _) = rooms.pop().unwrap();
let (sub_room, mut sub_events) = rooms.pop().unwrap();
let track = LocalAudioTrack::create_audio_track("microphone", audio.rtc_source());
pub_room
.local_participant()
.publish_track(
LocalTrack::Audio(track),
TrackPublishOptions { source: TrackSource::Microphone, ..Default::default() },
)
.await?;
log::info!("Publisher published audio track");
let wait_for_track = async {
while let Some(event) = sub_events.recv().await {
if let RoomEvent::TrackSubscribed { track: _, publication, participant } = event {
log::info!(
"Subscriber received track from {} ({:?})",
participant.identity(),
publication.source()
);
assert_eq!(publication.source(), TrackSource::Microphone);
return Ok(());
}
}
Err(anyhow!("Never received track subscription"))
};
timeout(Duration::from_secs(10), wait_for_track).await??;
pub_room.close().await?;
sub_room.close().await?;
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_device_switching() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_device_switching") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
let playout_devices: Vec<_> = audio.playout_devices().collect();
log::info!(
"Device switching test: {} recording, {} playout devices",
recording_devices.len(),
playout_devices.len()
);
let can_switch_recording = recording_devices.len() >= 2;
let can_switch_playout = playout_devices.len() >= 2;
if !can_switch_recording && !can_switch_playout {
log::info!("Skipping device switching test (need at least 2 devices)");
drop(audio);
return Ok(());
}
let mut rooms = test_rooms(1).await?;
let (room, _events) = rooms.pop().unwrap();
if !recording_devices.is_empty() {
let track = LocalAudioTrack::create_audio_track("microphone", audio.rtc_source());
room.local_participant()
.publish_track(
LocalTrack::Audio(track),
TrackPublishOptions { source: TrackSource::Microphone, ..Default::default() },
)
.await?;
}
if can_switch_recording {
log::info!("Switching recording device to {}", recording_devices[1].name);
audio.switch_recording_device(&recording_devices[1].id)?;
tokio::time::sleep(Duration::from_millis(100)).await;
log::info!("Switching recording device to {}", recording_devices[0].name);
audio.switch_recording_device(&recording_devices[0].id)?;
}
if can_switch_playout {
log::info!("Switching playout device to {}", playout_devices[1].name);
audio.switch_playout_device(&playout_devices[1].id)?;
tokio::time::sleep(Duration::from_millis(100)).await;
log::info!("Switching playout device to {}", playout_devices[0].name);
audio.switch_playout_device(&playout_devices[0].id)?;
}
room.close().await?;
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_reset_platform_audio() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_reset_platform_audio") else {
return Ok(());
};
let _recording: Vec<_> = audio.recording_devices().collect();
let _playout: Vec<_> = audio.playout_devices().collect();
reset_platform_audio();
let audio2 = PlatformAudio::new()?;
assert_eq!(audio2.ref_count(), 1);
drop(audio);
drop(audio2);
log::info!("reset_platform_audio works correctly");
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_hardware_availability() -> Result<()> {
use livekit::reset_platform_audio;
use livekit::AudioProcessingType;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_hardware_availability") else {
return Ok(());
};
let hw_aec = audio.is_hardware_aec_available();
let hw_agc = audio.is_hardware_agc_available();
let hw_ns = audio.is_hardware_ns_available();
log::info!(
"Hardware audio processing availability: AEC={}, AGC={}, NS={}",
hw_aec,
hw_agc,
hw_ns
);
let aec_type = audio.active_aec_type();
let agc_type = audio.active_agc_type();
let ns_type = audio.active_ns_type();
log::info!("Active audio processing: AEC={:?}, AGC={:?}, NS={:?}", aec_type, agc_type, ns_type);
if hw_aec {
assert_eq!(aec_type, AudioProcessingType::Hardware);
} else {
assert_eq!(aec_type, AudioProcessingType::Software);
}
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_configure_processing() -> Result<()> {
use livekit::reset_platform_audio;
use livekit::AudioProcessingOptions;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_configure_processing") else {
return Ok(());
};
audio.configure_audio_processing(AudioProcessingOptions::default())?;
log::info!("Configured with default options");
let custom_opts = AudioProcessingOptions {
echo_cancellation: true,
noise_suppression: true,
auto_gain_control: true,
prefer_hardware_processing: false,
};
audio.configure_audio_processing(custom_opts)?;
log::info!("Configured with custom options (software preferred)");
let hw_opts = AudioProcessingOptions {
echo_cancellation: true,
noise_suppression: true,
auto_gain_control: true,
prefer_hardware_processing: true,
};
audio.configure_audio_processing(hw_opts)?;
log::info!("Configured with hardware preference");
let minimal_opts = AudioProcessingOptions {
echo_cancellation: false,
noise_suppression: true,
auto_gain_control: false,
prefer_hardware_processing: false,
};
audio.configure_audio_processing(minimal_opts)?;
log::info!("Configured with minimal options");
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_individual_controls() -> Result<()> {
use livekit::reset_platform_audio;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_individual_controls") else {
return Ok(());
};
audio.set_echo_cancellation(true, false)?;
log::info!("AEC enabled (software)");
audio.set_echo_cancellation(true, true)?;
log::info!("AEC enabled (hardware preferred)");
audio.set_echo_cancellation(false, false)?;
log::info!("AEC disabled");
audio.set_auto_gain_control(true, false)?;
log::info!("AGC enabled (software)");
audio.set_auto_gain_control(false, false)?;
log::info!("AGC disabled");
audio.set_noise_suppression(true, false)?;
log::info!("NS enabled (software)");
audio.set_noise_suppression(false, false)?;
log::info!("NS disabled");
drop(audio);
Ok(())
}
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_processing_with_room() -> Result<()> {
use livekit::reset_platform_audio;
use livekit::AudioProcessingOptions;
reset_platform_audio();
let Some(audio) = try_create_platform_audio("test_platform_audio_processing_with_room") else {
return Ok(());
};
let recording_devices: Vec<_> = audio.recording_devices().collect();
if recording_devices.is_empty() {
log::info!("Skipping test - no microphone available");
drop(audio);
return Ok(());
}
audio.configure_audio_processing(AudioProcessingOptions {
echo_cancellation: true,
noise_suppression: true,
auto_gain_control: true,
prefer_hardware_processing: false, })?;
log::info!("Audio processing configured, connecting to room...");
let mut rooms = test_rooms(1).await?;
let (room, _events) = rooms.pop().unwrap();
audio.set_recording_device(&recording_devices[0].id)?;
let track = LocalAudioTrack::create_audio_track("microphone", audio.rtc_source());
room.local_participant()
.publish_track(
LocalTrack::Audio(track),
TrackPublishOptions { source: TrackSource::Microphone, ..Default::default() },
)
.await?;
log::info!("Published audio track with configured processing");
let publications = room.local_participant().track_publications();
assert!(
publications.values().any(|p| p.source() == TrackSource::Microphone),
"Microphone track should be published"
);
audio.configure_audio_processing(AudioProcessingOptions {
echo_cancellation: true,
noise_suppression: false, auto_gain_control: true,
prefer_hardware_processing: false,
})?;
log::info!("Reconfigured audio processing while connected");
room.close().await?;
drop(audio);
log::info!("Audio processing with room test completed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_platform_ref_counting() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_ref_count = pcf.platform_adm_ref_count();
log::info!("Initial platform_adm_ref_count: {}", initial_ref_count);
let is_active_before = pcf.is_platform_adm_active();
log::info!("is_platform_adm_active before acquire: {}", is_active_before);
if !try_acquire_platform_adm(pcf, "test_adm_proxy_platform_ref_counting") {
return Ok(());
}
let ref_count_after_acquire = pcf.platform_adm_ref_count();
assert_eq!(ref_count_after_acquire, initial_ref_count + 1, "ref_count should increment by 1");
assert!(pcf.is_platform_adm_active(), "Platform ADM should be active after acquire");
log::info!(
"After acquire: ref_count={}, is_active={}",
ref_count_after_acquire,
pcf.is_platform_adm_active()
);
let acquired2 = pcf.acquire_platform_adm();
assert!(acquired2, "second acquire_platform_adm should succeed");
let ref_count_after_second_acquire = pcf.platform_adm_ref_count();
assert_eq!(
ref_count_after_second_acquire,
initial_ref_count + 2,
"ref_count should be initial + 2"
);
log::info!("After second acquire: ref_count={}", ref_count_after_second_acquire);
pcf.release_platform_adm();
let ref_count_after_release = pcf.platform_adm_ref_count();
assert_eq!(
ref_count_after_release,
initial_ref_count + 1,
"ref_count should be initial + 1 after one release"
);
assert!(pcf.is_platform_adm_active(), "Platform ADM should still be active (ref_count > 0)");
log::info!("After first release: ref_count={}", ref_count_after_release);
pcf.release_platform_adm();
let final_ref_count = pcf.platform_adm_ref_count();
assert_eq!(final_ref_count, initial_ref_count, "ref_count should return to initial value");
log::info!(
"After second release: ref_count={}, is_active={}",
final_ref_count,
pcf.is_platform_adm_active()
);
if initial_ref_count == 0 {
assert!(
!pcf.is_platform_adm_active(),
"Platform ADM should not be active when ref_count = 0"
);
}
log::info!("ADM proxy platform ref counting test passed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_recording_enabled_flag() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_enabled = pcf.adm_recording_enabled();
log::info!("Initial adm_recording_enabled: {}", initial_enabled);
pcf.set_adm_recording_enabled(false);
assert!(!pcf.adm_recording_enabled(), "Recording should be disabled");
log::info!("After set_adm_recording_enabled(false): {}", pcf.adm_recording_enabled());
pcf.set_adm_recording_enabled(true);
assert!(pcf.adm_recording_enabled(), "Recording should be enabled");
log::info!("After set_adm_recording_enabled(true): {}", pcf.adm_recording_enabled());
pcf.set_adm_recording_enabled(false);
pcf.set_adm_recording_enabled(true);
pcf.set_adm_recording_enabled(false);
assert!(!pcf.adm_recording_enabled(), "Recording should be disabled after toggles");
pcf.set_adm_recording_enabled(initial_enabled);
assert_eq!(
pcf.adm_recording_enabled(),
initial_enabled,
"Recording should be restored to initial state"
);
log::info!("ADM recording enabled flag test passed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_playout_enabled_flag() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_enabled = pcf.adm_playout_enabled();
log::info!("Initial adm_playout_enabled: {}", initial_enabled);
pcf.set_adm_playout_enabled(false);
assert!(!pcf.adm_playout_enabled(), "Playout should be disabled (synthetic mode)");
log::info!("After set_adm_playout_enabled(false): {}", pcf.adm_playout_enabled());
pcf.set_adm_playout_enabled(true);
assert!(pcf.adm_playout_enabled(), "Playout should be enabled (platform mode)");
log::info!("After set_adm_playout_enabled(true): {}", pcf.adm_playout_enabled());
pcf.set_adm_playout_enabled(false);
pcf.set_adm_playout_enabled(true);
pcf.set_adm_playout_enabled(false);
assert!(!pcf.adm_playout_enabled(), "Playout should be disabled after toggles");
pcf.set_adm_playout_enabled(initial_enabled);
assert_eq!(
pcf.adm_playout_enabled(),
initial_enabled,
"Playout should be restored to initial state"
);
log::info!("ADM playout enabled flag test passed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_playout_mode_switching() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_playout_enabled = pcf.adm_playout_enabled();
let initial_ref_count = pcf.platform_adm_ref_count();
log::info!("=== Phase 1: Setup - Acquire Platform ADM first ===");
if !try_acquire_platform_adm(pcf, "test_adm_proxy_playout_mode_switching") {
return Ok(());
}
assert!(pcf.is_platform_adm_active());
log::info!("Platform ADM acquired");
pcf.set_adm_playout_enabled(true);
assert!(pcf.adm_playout_enabled());
let init_result = pcf.init_playout();
log::info!("init_playout() result: {}", init_result);
let playout_available = init_result;
if playout_available {
let start_result = pcf.start_playout();
log::info!("start_playout() result: {}", start_result);
log::info!("playout_is_initialized: {}", pcf.playout_is_initialized());
} else {
log::info!("Playout not available on this environment, testing mode switches without active playout");
}
log::info!("=== Phase 2: Switch to synthetic mode ===");
pcf.set_adm_playout_enabled(false);
assert!(!pcf.adm_playout_enabled());
log::info!("Switched to synthetic playout mode (Dummy ADM)");
tokio::time::sleep(Duration::from_millis(50)).await;
log::info!(
"playout_is_initialized after switch to synthetic: {}",
pcf.playout_is_initialized()
);
log::info!("=== Phase 3: Switch back to platform mode ===");
pcf.set_adm_playout_enabled(true);
assert!(pcf.adm_playout_enabled());
log::info!("Switched back to platform playout mode");
tokio::time::sleep(Duration::from_millis(50)).await;
log::info!("playout_is_initialized after switch to platform: {}", pcf.playout_is_initialized());
log::info!("=== Phase 4: Cleanup ===");
pcf.stop_playout();
log::info!("Stopped playout");
pcf.release_platform_adm();
pcf.set_adm_playout_enabled(initial_playout_enabled);
while pcf.platform_adm_ref_count() > initial_ref_count {
pcf.release_platform_adm();
}
log::info!("ADM playout mode switching test passed - no crashes during mode switches");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_recording_mode_switching() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_recording_enabled = pcf.adm_recording_enabled();
let initial_ref_count = pcf.platform_adm_ref_count();
log::info!("=== Phase 1: Setup - no Platform ADM ===");
pcf.set_adm_recording_enabled(false);
log::info!("recording_is_initialized (no platform ADM): {}", pcf.recording_is_initialized());
log::info!("=== Phase 2: Acquire Platform ADM and enable recording ===");
if !try_acquire_platform_adm(pcf, "test_adm_proxy_recording_mode_switching") {
return Ok(());
}
assert!(pcf.is_platform_adm_active());
pcf.set_adm_recording_enabled(true);
assert!(pcf.adm_recording_enabled());
let init_result = pcf.init_recording();
log::info!("init_recording() result: {}", init_result);
if init_result && pcf.recording_devices() > 0 {
let start_result = pcf.start_recording();
log::info!("start_recording() result: {}", start_result);
assert!(pcf.recording_is_initialized(), "Recording should be initialized");
log::info!("Recording is active on Platform ADM");
log::info!("=== Phase 3: Disable recording - triggers mode switch ===");
pcf.set_adm_recording_enabled(false);
assert!(!pcf.adm_recording_enabled());
tokio::time::sleep(Duration::from_millis(50)).await;
log::info!("recording_is_initialized after disable: {}", pcf.recording_is_initialized());
pcf.stop_recording();
} else {
log::info!("Skipping recording test - no recording devices or init failed");
}
log::info!("=== Phase 4: Cleanup ===");
pcf.release_platform_adm();
pcf.set_adm_recording_enabled(initial_recording_enabled);
while pcf.platform_adm_ref_count() > initial_ref_count {
pcf.release_platform_adm();
}
log::info!("ADM recording mode switching test passed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_platform_audio_adm_lifecycle() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::reset_platform_audio;
use livekit::rtc_engine::lk_runtime::LkRuntime;
reset_platform_audio();
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
log::info!("=== Initial state (no PlatformAudio) ===");
let initial_cpp_ref_count = pcf.platform_adm_ref_count();
let initial_is_active = pcf.is_platform_adm_active();
log::info!("Initial C++ ref_count={}, is_active={}", initial_cpp_ref_count, initial_is_active);
if initial_cpp_ref_count == 0 {
assert!(!initial_is_active, "Platform ADM should not be active when ref_count is 0");
}
log::info!("=== Create first PlatformAudio ===");
let audio1 = match PlatformAudio::new() {
Ok(audio) => audio,
Err(e) => {
log::info!("Skipping test - PlatformAudio::new() failed (no audio devices?): {}", e);
return Ok(());
}
};
let rust_ref_count_1 = audio1.ref_count();
let cpp_ref_count_after_first = pcf.platform_adm_ref_count();
let is_active_after_first = pcf.is_platform_adm_active();
log::info!(
"After first create: C++ ref_count={}, is_active={}, Rust ref_count={}",
cpp_ref_count_after_first,
is_active_after_first,
rust_ref_count_1
);
assert!(
cpp_ref_count_after_first > initial_cpp_ref_count,
"PlatformAudio should increment C++ platform ADM ref count"
);
assert!(is_active_after_first, "Platform ADM should be active after PlatformAudio creation");
assert_eq!(rust_ref_count_1, 1, "First PlatformAudio Rust ref_count should be 1");
log::info!("=== Create second PlatformAudio (shares handle with first) ===");
let audio2 = PlatformAudio::new().expect("Second PlatformAudio should succeed if first did");
let rust_ref_count_2 = audio2.ref_count();
log::info!(
"After second create: C++ ref_count={}, Rust ref_count={}",
pcf.platform_adm_ref_count(),
rust_ref_count_2
);
assert_eq!(rust_ref_count_2, 2, "Second PlatformAudio should share handle (Rust ref_count=2)");
assert_eq!(audio1.ref_count(), 2, "First audio should also see Rust ref_count=2");
log::info!("=== Drop first PlatformAudio (second still holds reference) ===");
drop(audio1);
assert!(
pcf.is_platform_adm_active(),
"Platform ADM should still be active (audio2 still exists)"
);
assert_eq!(audio2.ref_count(), 1, "After dropping audio1, Rust ref_count should be 1");
log::info!(
"After drop first: C++ ref_count={}, Rust ref_count={}",
pcf.platform_adm_ref_count(),
audio2.ref_count()
);
log::info!("=== Drop second PlatformAudio (should release Platform ADM) ===");
drop(audio2);
let final_cpp_ref_count = pcf.platform_adm_ref_count();
let final_is_active = pcf.is_platform_adm_active();
log::info!(
"After drop second: C++ ref_count={}, is_active={}",
final_cpp_ref_count,
final_is_active
);
assert_eq!(
final_cpp_ref_count, initial_cpp_ref_count,
"C++ ref count should return to initial value after all PlatformAudio dropped"
);
if initial_cpp_ref_count == 0 {
assert!(
!final_is_active,
"Platform ADM should not be active when all PlatformAudio instances are dropped"
);
}
log::info!("PlatformAudio ADM lifecycle test passed");
Ok(())
}
#[test_log::test(tokio::test)]
#[serial]
async fn test_adm_proxy_rapid_mode_changes() -> Result<()> {
use libwebrtc::peer_connection_factory::native::PeerConnectionFactoryExt;
use livekit::rtc_engine::lk_runtime::LkRuntime;
let runtime = LkRuntime::instance();
let pcf = runtime.pc_factory();
let initial_playout_enabled = pcf.adm_playout_enabled();
let initial_recording_enabled = pcf.adm_recording_enabled();
let initial_ref_count = pcf.platform_adm_ref_count();
log::info!("=== Setup: Acquire Platform ADM ===");
if !try_acquire_platform_adm(pcf, "test_adm_proxy_rapid_mode_changes") {
return Ok(());
}
pcf.init_playout();
pcf.start_playout();
log::info!("=== Rapid playout mode changes ===");
for i in 0..10 {
pcf.set_adm_playout_enabled(i % 2 == 0);
}
log::info!("Completed 10 rapid playout mode changes");
log::info!("=== Rapid recording mode changes ===");
for i in 0..10 {
pcf.set_adm_recording_enabled(i % 2 == 0);
}
log::info!("Completed 10 rapid recording mode changes");
log::info!("=== Rapid acquire/release cycles ===");
for _ in 0..5 {
pcf.acquire_platform_adm();
pcf.release_platform_adm();
}
log::info!("Completed 5 rapid acquire/release cycles");
let current_ref_count = pcf.platform_adm_ref_count();
let is_active = pcf.is_platform_adm_active();
assert_eq!(current_ref_count, initial_ref_count + 1, "Ref count should be initial + 1");
assert!(is_active, "Platform ADM should be active");
log::info!("=== Cleanup ===");
pcf.stop_playout();
pcf.release_platform_adm();
pcf.set_adm_playout_enabled(initial_playout_enabled);
pcf.set_adm_recording_enabled(initial_recording_enabled);
while pcf.platform_adm_ref_count() > initial_ref_count {
pcf.release_platform_adm();
}
log::info!("ADM rapid mode changes test passed - no crashes or state corruption");
Ok(())
}