diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index f9a635a16a2eaf2a4facbd1f25bf6eb0f9fe7a87..2165cf39136a1ed7268fbf6ea670d825b2b50bcc 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -384,17 +384,29 @@ pub fn open_input_stream( Ok(stream) } -pub fn open_output_stream(device_id: Option) -> anyhow::Result { - let output_handle = if let Some(id) = device_id { - if let Some(device) = default_host().device_by_id(&id) { - DeviceSinkBuilder::from_device(device)?.open_stream() - } else { - DeviceSinkBuilder::open_default_sink() +pub fn resolve_device(device_id: Option<&DeviceId>, input: bool) -> anyhow::Result { + if let Some(id) = device_id { + if let Some(device) = default_host().device_by_id(id) { + return Ok(device); } + log::warn!("Selected audio device not found, falling back to default"); + } + if input { + default_host() + .default_input_device() + .context("no audio input device available") } else { - DeviceSinkBuilder::open_default_sink() - }; - let mut output_handle = output_handle.context("Could not open output stream")?; + default_host() + .default_output_device() + .context("no audio output device available") + } +} + +pub fn open_output_stream(device_id: Option) -> anyhow::Result { + let device = resolve_device(device_id.as_ref(), false)?; + let mut output_handle = DeviceSinkBuilder::from_device(device)? + .open_stream() + .context("Could not open output stream")?; output_handle.log_on_drop(false); log::info!("Output stream: {:?}", output_handle); Ok(output_handle) diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs index 4f60a6d63aef1d2c2d7fb4761a6fc2e2eaf3d8c7..8425ed5eaa713053f44b26e199a66b76bf9b57a6 100644 --- a/crates/audio/src/audio_settings.rs +++ b/crates/audio/src/audio_settings.rs @@ -42,12 +42,8 @@ pub struct AudioSettings { /// /// You need to rejoin a call for this setting to apply pub legacy_audio_compatible: bool, - /// Requires 'rodio_audio: true' - /// /// Select specific output audio device. pub output_audio_device: Option, - /// Requires 'rodio_audio: true' - /// /// Select specific input audio device. pub input_audio_device: Option, } diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index be008d8db5108fb087415edb9d2de91bad19ab97..352776cf6bbe02381957a197eca9a64fff094892 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -1,8 +1,8 @@ use anyhow::Context as _; use collections::HashMap; +use cpal::DeviceId; mod remote_video_track_view; -use cpal::traits::HostTrait as _; pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent}; use rodio::DeviceTrait as _; @@ -192,24 +192,18 @@ pub enum RoomEvent { pub(crate) fn default_device( input: bool, + device_id: Option<&DeviceId>, ) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> { - let device; - let config; - if input { - device = cpal::default_host() - .default_input_device() - .context("no audio input device available")?; - config = device + let device = audio::resolve_device(device_id, input)?; + let config = if input { + device .default_input_config() - .context("failed to get default input config")?; + .context("failed to get default input config")? } else { - device = cpal::default_host() - .default_output_device() - .context("no audio output device available")?; - config = device + device .default_output_config() - .context("failed to get default output config")?; - } + .context("failed to get default output config")? + }; Ok((device, config)) } diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 1db9a12ef2b7f3b4f3de1cba6c61a30db12a5bd9..863cf0dc527300f1e85df6867d99e367b5c7fa15 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -150,7 +150,10 @@ impl Room { info!("Using experimental.rodio_audio audio pipeline for output"); playback::play_remote_audio_track(&track.0, speaker, cx) } else if speaker.sends_legacy_audio { - Ok(self.playback.play_remote_audio_track(&track.0)) + let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone(); + Ok(self + .playback + .play_remote_audio_track(&track.0, output_audio_device)) } else { Err(anyhow!("Client version too old to play audio in call")) } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index df62479f022be5295a3de44f40fabf48aed515f2..0ebb282dd7ec494886fe1ffc90fe1f8688a762da 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -1,6 +1,7 @@ use anyhow::{Context as _, Result}; use audio::{AudioSettings, CHANNEL_COUNT, LEGACY_CHANNEL_COUNT, LEGACY_SAMPLE_RATE, SAMPLE_RATE}; +use cpal::DeviceId; use cpal::traits::{DeviceTrait, StreamTrait as _}; use futures::channel::mpsc::UnboundedSender; use futures::{Stream, StreamExt as _}; @@ -91,8 +92,9 @@ impl AudioStack { pub(crate) fn play_remote_audio_track( &self, track: &livekit::track::RemoteAudioTrack, + output_audio_device: Option, ) -> AudioStream { - let output_task = self.start_output(); + let output_task = self.start_output(output_audio_device); let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); let source = AudioMixerSource { @@ -130,7 +132,7 @@ impl AudioStack { } } - fn start_output(&self) -> Arc> { + fn start_output(&self, output_audio_device: Option) -> Arc> { if let Some(task) = self._output_task.borrow().upgrade() { return task; } @@ -143,6 +145,7 @@ impl AudioStack { mixer, LEGACY_SAMPLE_RATE.get(), LEGACY_CHANNEL_COUNT.get().into(), + output_audio_device, ) .await .log_err(); @@ -219,12 +222,16 @@ impl AudioStack { Ok(()) }) } else { + let input_audio_device = + AudioSettings::try_read_global(cx, |settings| settings.input_audio_device.clone()) + .flatten(); self.executor.spawn(async move { Self::capture_input( apm, frame_tx, LEGACY_SAMPLE_RATE.get(), LEGACY_CHANNEL_COUNT.get().into(), + input_audio_device, ) .await }) @@ -247,6 +254,7 @@ impl AudioStack { mixer: Arc>, sample_rate: u32, num_channels: u32, + output_audio_device: Option, ) -> Result<()> { // Prevent App Nap from throttling audio playback on macOS. // This guard is held for the entire duration of audio output. @@ -255,7 +263,8 @@ impl AudioStack { loop { let mut device_change_listener = DeviceChangeListener::new(false)?; - let (output_device, output_config) = crate::default_device(false)?; + let (output_device, output_config) = + crate::default_device(false, output_audio_device.as_ref())?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let mixer = mixer.clone(); let apm = apm.clone(); @@ -327,10 +336,11 @@ impl AudioStack { frame_tx: UnboundedSender>, sample_rate: u32, num_channels: u32, + input_audio_device: Option, ) -> Result<()> { loop { let mut device_change_listener = DeviceChangeListener::new(true)?; - let (device, config) = crate::default_device(true)?; + let (device, config) = crate::default_device(true, input_audio_device.as_ref())?; let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>(); let apm = apm.clone(); let frame_tx = frame_tx.clone(); diff --git a/crates/livekit_client/src/record.rs b/crates/livekit_client/src/record.rs index c23ab2b938178e9b634f8e0d4d298f2c86450b51..c0fe9eb7218ad8550f7b63042d0e11c2cb53ee20 100644 --- a/crates/livekit_client/src/record.rs +++ b/crates/livekit_client/src/record.rs @@ -7,20 +7,22 @@ use std::{ }; use anyhow::{Context, Result}; +use cpal::DeviceId; use cpal::traits::{DeviceTrait, StreamTrait}; use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter}; use util::ResultExt; pub struct CaptureInput { pub name: String, + pub input_device: Option, config: cpal::SupportedStreamConfig, samples: Arc>>, _stream: cpal::Stream, } impl CaptureInput { - pub fn start() -> anyhow::Result { - let (device, config) = crate::default_device(true)?; + pub fn start(input_device: Option) -> anyhow::Result { + let (device, config) = crate::default_device(true, input_device.as_ref())?; let name = device .description() .map(|desc| desc.name().to_string()) @@ -32,6 +34,7 @@ impl CaptureInput { Ok(Self { name, + input_device, _stream: stream, config, samples,