More audio settings, limit replay to staff

David Kleingeld created

Change summary

.config/hakari.toml                                  |   2 
Cargo.lock                                           |  10 
crates/audio/Cargo.toml                              |   3 
crates/audio/src/audio.rs                            | 161 +++++-----
crates/audio/src/audio_settings.rs                   |  77 ++++
crates/audio/src/replays.rs                          |   5 
crates/audio/src/rodio_ext.rs                        | 202 +++++++++++--
crates/call/Cargo.toml                               |   1 
crates/call/src/call_impl/room.rs                    |   9 
crates/livekit_client/Cargo.toml                     |   9 
crates/livekit_client/examples/test_app.rs           |   2 
crates/livekit_client/src/lib.rs                     |  66 ++--
crates/livekit_client/src/livekit_client.rs          |   5 
crates/livekit_client/src/livekit_client/playback.rs |  59 +++
crates/livekit_client/src/test.rs                    |   3 
crates/zed/Cargo.toml                                |   1 
crates/zed/src/zed.rs                                | 114 -------
tooling/workspace-hack/Cargo.toml                    |  14 
18 files changed, 437 insertions(+), 306 deletions(-)

Detailed changes

.config/hakari.toml 🔗

@@ -26,7 +26,7 @@ third-party = [
     # build of remote_server should not include scap / its x11 dependency
     { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
     # build of remote_server should not need to include on libalsa through rodio
-    # { name = "rodio" },
+    { name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
 ]
 
 [final-excludes]

Cargo.lock 🔗

@@ -1388,7 +1388,6 @@ dependencies = [
  "async-tar",
  "collections",
  "crossbeam",
- "futures 0.3.31",
  "gpui",
  "libwebrtc",
  "log",
@@ -2618,6 +2617,7 @@ dependencies = [
  "audio",
  "client",
  "collections",
+ "feature_flags",
  "fs",
  "futures 0.3.31",
  "gpui",
@@ -9418,7 +9418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
 dependencies = [
  "cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -9672,6 +9672,7 @@ dependencies = [
  "scap",
  "serde",
  "serde_json",
+ "serde_urlencoded",
  "settings",
  "sha2",
  "simplelog",
@@ -13882,7 +13883,6 @@ dependencies = [
  "rtrb",
  "symphonia",
  "thiserror 2.0.12",
- "tracing",
 ]
 
 [[package]]
@@ -18960,7 +18960,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -19997,6 +19997,7 @@ dependencies = [
  "libsqlite3-sys",
  "linux-raw-sys 0.4.15",
  "linux-raw-sys 0.9.4",
+ "livekit-runtime",
  "log",
  "lyon",
  "lyon_path",
@@ -20551,7 +20552,6 @@ dependencies = [
  "languages",
  "libc",
  "line_ending_selector",
- "livekit_client",
  "log",
  "markdown",
  "markdown_preview",

crates/audio/Cargo.toml 🔗

@@ -19,9 +19,8 @@ collections.workspace = true
 crossbeam.workspace = true
 gpui.workspace = true
 log.workspace = true
-futures.workspace = true
 parking_lot.workspace = true
-rodio = { workspace = true, features = [ "wav", "playback" ] }
+rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
 schemars.workspace = true
 serde.workspace = true
 settings.workspace = true

crates/audio/src/audio.rs 🔗

@@ -1,8 +1,7 @@
 use anyhow::{Context as _, Result};
 use collections::HashMap;
-use futures::channel::mpsc::UnboundedSender;
 use gpui::{App, AsyncApp, BackgroundExecutor, BorrowAppContext, Global};
-use libwebrtc::{native::apm, prelude::AudioFrame};
+use libwebrtc::native::apm;
 use log::info;
 use parking_lot::Mutex;
 use rodio::{
@@ -14,18 +13,13 @@ use rodio::{
 };
 use settings::Settings;
 use std::{
-    borrow::Cow,
     io::Cursor,
     num::NonZero,
     path::PathBuf,
-    sync::{
-        Arc,
-        mpsc::{TryRecvError, channel},
-    },
-    thread,
+    sync::{Arc, atomic::Ordering},
     time::Duration,
 };
-use util::{ResultExt, debug_panic};
+use util::ResultExt;
 
 mod audio_settings;
 mod replays;
@@ -33,6 +27,8 @@ mod rodio_ext;
 pub use audio_settings::AudioSettings;
 pub use rodio_ext::RodioExt;
 
+use crate::audio_settings::LIVE_SETTINGS;
+
 // NOTE: We used to use WebRTC's mixer which only supported
 // 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
 // for audio output devices like speakers/bluetooth, we just hard-code
@@ -42,13 +38,14 @@ pub use rodio_ext::RodioExt;
 // that in the future.
 pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
 pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
-const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
+pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
     (SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
 
 pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
 
 pub fn init(cx: &mut App) {
     AudioSettings::register(cx);
+    LIVE_SETTINGS.initialize(cx);
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
@@ -142,96 +139,72 @@ impl Audio {
         self.replays.replays_to_tar(executor)
     }
 
-    pub fn open_microphone(
-        cx: AsyncApp,
-        frame_tx: UnboundedSender<AudioFrame<'static>>,
-    ) -> anyhow::Result<()> {
-        let (apm, mut replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
-            (Arc::clone(&audio.echo_canceller), audio.replays.clone())
-        })?;
-
-        let (stream_error_tx, stream_error_rx) = channel();
-        thread::spawn(move || {
-            let stream = rodio::microphone::MicrophoneBuilder::new()
-                .default_device()?
-                .default_config()?
-                .prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
-                .prefer_channel_counts([nz!(1), nz!(2)])
-                .prefer_buffer_sizes(512..)
-                .open_stream()?;
-            info!("Opened microphone: {:?}", stream.config());
-
-            let stream = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
-                .limit(LimitSettings::live_performance())
-                .process_buffer::<BUFFER_SIZE, _>(|buffer| {
-                    let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
-                    if let Err(e) = apm
-                        .lock()
-                        .process_stream(
-                            &mut int_buffer,
-                            SAMPLE_RATE.get() as i32,
-                            CHANNEL_COUNT.get() as i32,
-                        )
-                        .context("livekit audio processor error")
-                    {
-                        let _ = stream_error_tx.send(e);
-                    } else {
-                        for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
-                            *sample = (*processed).to_sample();
-                        }
-                    }
-                })
-                .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
-                .periodic_access(Duration::from_millis(100), move |agc_source| {
-                    agc_source.set_enabled(true); // todo dvdsk how to get settings in here?
-                });
-
-            // todo dvdsk keep the above here, move the rest back to livekit?
-            let (replay, mut stream) = stream.replayable(REPLAY_DURATION);
-            replays.add_voip_stream("local microphone".to_string(), replay);
-
-            loop {
-                let sampled: Vec<_> = stream
-                    .by_ref()
-                    .take(BUFFER_SIZE)
-                    .map(|s| s.to_sample())
-                    .collect();
+    pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
+        let stream = rodio::microphone::MicrophoneBuilder::new()
+            .default_device()?
+            .default_config()?
+            .prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
+            .prefer_channel_counts([nz!(1), nz!(2)])
+            .prefer_buffer_sizes(512..)
+            .open_stream()?;
+        info!("Opened microphone: {:?}", stream.config());
 
-                match stream_error_rx.try_recv() {
-                    Ok(apm_error) => return Err::<(), _>(apm_error),
-                    Err(TryRecvError::Disconnected) => {
-                        debug_panic!("Stream should end on its own without sending an error")
+        let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
+            .limit(LimitSettings::live_performance())
+            .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
+                let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
+                if voip_parts
+                    .echo_canceller
+                    .lock()
+                    .process_stream(
+                        &mut int_buffer,
+                        SAMPLE_RATE.get() as i32,
+                        CHANNEL_COUNT.get() as i32,
+                    )
+                    .context("livekit audio processor error")
+                    .log_err()
+                    .is_some()
+                {
+                    for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
+                        *sample = (*processed).to_sample();
                     }
-                    Err(TryRecvError::Empty) => (),
                 }
+            })
+            .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+            .periodic_access(Duration::from_millis(100), move |agc_source| {
+                agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
+            })
+            .replayable(REPLAY_DURATION)
+            .expect("REPLAY_DURATION is longer then 100ms");
 
-                frame_tx
-                    .unbounded_send(AudioFrame {
-                        sample_rate: SAMPLE_RATE.get(),
-                        num_channels: CHANNEL_COUNT.get() as u32,
-                        samples_per_channel: sampled.len() as u32 / CHANNEL_COUNT.get() as u32,
-                        data: Cow::Owned(sampled),
-                    })
-                    .context("Failed to send audio frame")?
-            }
-        });
-
-        Ok(())
+        voip_parts
+            .replays
+            .add_voip_stream("local microphone".to_string(), replay);
+        Ok(stream)
     }
 
     pub fn play_voip_stream(
-        stream_source: impl rodio::Source + Send + 'static,
-        stream_name: String,
+        source: impl rodio::Source + Send + 'static,
+        speaker_name: String,
+        is_staff: bool,
         cx: &mut App,
     ) -> anyhow::Result<()> {
-        let (replay_source, source) = stream_source.replayable(REPLAY_DURATION);
+        let (replay_source, source) = source
+            .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+            .periodic_access(Duration::from_millis(100), move |agc_source| {
+                agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
+            })
+            .replayable(REPLAY_DURATION)
+            .expect("REPLAY_DURATION is longer then 100ms");
 
         cx.update_default_global(|this: &mut Self, _cx| {
             let output_mixer = this
                 .ensure_output_exists()
                 .context("Could not get output mixer")?;
             output_mixer.add(source);
-            this.replays.add_voip_stream(stream_name, replay_source);
+            if is_staff {
+                this.replays.add_voip_stream(speaker_name, replay_source);
+            }
             Ok(())
         })
     }
@@ -275,3 +248,21 @@ impl Audio {
         Ok(source)
     }
 }
+
+pub struct VoipParts {
+    echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
+    replays: replays::Replays,
+}
+
+impl VoipParts {
+    pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
+        let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
+            (Arc::clone(&audio.echo_canceller), audio.replays.clone())
+        })?;
+
+        Ok(Self {
+            echo_canceller: apm,
+            replays,
+        })
+    }
+}

crates/audio/src/audio_settings.rs 🔗

@@ -1,17 +1,29 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+
 use anyhow::Result;
 use gpui::App;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
 
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
 pub struct AudioSettings {
     /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
     pub rodio_audio: bool, // default is false
-    /// Opt into the new audio systems automatic gain control
-    #[serde(rename = "experimental.automatic_volume", default)]
-    pub automatic_volume: bool,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control for your microphone.
+    /// This affects how loud you sound to others.
+    #[serde(rename = "experimental.control_input_volume", default)]
+    pub control_input_volume: bool,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control on everyone in the
+    /// call. This makes call members who are too quite louder and those who are
+    /// too loud quieter. This only affects how things sound for you.
+    #[serde(rename = "experimental.control_output_volume", default)]
+    pub control_output_volume: bool,
 }
 
 /// Configuration of audio in Zed.
@@ -19,13 +31,22 @@ pub struct AudioSettings {
 #[serde(default)]
 #[settings_key(key = "audio")]
 pub struct AudioSettingsContent {
-    /// Whether to use the experimental audio system
+    /// Opt into the new audio system.
     #[serde(rename = "experimental.rodio_audio", default)]
-    pub rodio_audio: bool,
-    /// Whether the experimental audio systems should automatically
-    /// manage the volume of calls
-    #[serde(rename = "experimental.automatic_volume", default)]
-    pub automatic_volume: bool,
+    pub rodio_audio: bool, // default is false
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control for your microphone.
+    /// This affects how loud you sound to others.
+    #[serde(rename = "experimental.control_input_volume", default)]
+    pub control_input_volume: bool,
+    /// Requires 'rodio_audio: true'
+    ///
+    /// Use the new audio systems automatic gain control on everyone in the
+    /// call. This makes call members who are too quite louder and those who are
+    /// too loud quieter. This only affects how things sound for you.
+    #[serde(rename = "experimental.control_output_volume", default)]
+    pub control_output_volume: bool,
 }
 
 impl Settings for AudioSettings {
@@ -37,3 +58,39 @@ impl Settings for AudioSettings {
 
     fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
 }
+
+pub(crate) struct LiveSettings {
+    pub(crate) control_input_volume: AtomicBool,
+    pub(crate) control_output_volume: AtomicBool,
+}
+
+impl LiveSettings {
+    pub(crate) fn initialize(&self, cx: &mut App) {
+        cx.observe_global::<SettingsStore>(move |cx| {
+            LIVE_SETTINGS.control_input_volume.store(
+                AudioSettings::get_global(cx).control_input_volume,
+                Ordering::Relaxed,
+            );
+            LIVE_SETTINGS.control_output_volume.store(
+                AudioSettings::get_global(cx).control_output_volume,
+                Ordering::Relaxed,
+            );
+        })
+        .detach();
+
+        let init_settings = AudioSettings::get_global(cx);
+        LIVE_SETTINGS
+            .control_input_volume
+            .store(init_settings.control_input_volume, Ordering::Relaxed);
+        LIVE_SETTINGS
+            .control_output_volume
+            .store(init_settings.control_output_volume, Ordering::Relaxed);
+    }
+}
+
+/// Allows access to settings from the audio thread. Updated by
+/// observer of SettingsStore.
+pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
+    control_input_volume: AtomicBool::new(true),
+    control_output_volume: AtomicBool::new(true),
+};

crates/audio/src/replays.rs 🔗

@@ -14,12 +14,9 @@ use crate::{REPLAY_DURATION, rodio_ext::Replay};
 pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);
 
 impl Replays {
-    pub(crate) fn add_voip_stream(&mut self, stream_name: String, source: Replay) {
+    pub(crate) fn add_voip_stream(&self, stream_name: String, source: Replay) {
         let mut map = self.0.lock();
         map.retain(|_, replay| replay.source_is_active());
-        // on the old pipeline all the streams are named microphone
-        // make sure names dont collide in that case by adding a number.
-        let stream_name = stream_name + &map.len().to_string();
         map.insert(stream_name, source);
     }
 

crates/audio/src/rodio_ext.rs 🔗

@@ -1,7 +1,7 @@
 use std::{
     sync::{
-        Arc,
-        atomic::{AtomicUsize, Ordering},
+        Arc, Mutex,
+        atomic::{AtomicBool, Ordering},
     },
     time::Duration,
 };
@@ -9,6 +9,9 @@ use std::{
 use crossbeam::queue::ArrayQueue;
 use rodio::{ChannelCount, Sample, SampleRate, Source};
 
+#[derive(Debug)]
+pub struct ReplayDurationTooShort;
+
 pub trait RodioExt: Source + Sized {
     fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
     where
@@ -16,7 +19,11 @@ pub trait RodioExt: Source + Sized {
     fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
     where
         F: FnMut(&[Sample; N]);
-    fn replayable(self, duration: Duration) -> (Replay, Replayable<Self>);
+    fn replayable(
+        self,
+        duration: Duration,
+    ) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
+    fn take_samples(self, n: usize) -> TakeSamples<Self>;
 }
 
 impl<S: Source> RodioExt for S {
@@ -42,32 +49,100 @@ impl<S: Source> RodioExt for S {
             free: 0,
         }
     }
-    fn replayable(self, duration: Duration) -> (Replay, Replayable<Self>) {
-        let samples_per_second = self.sample_rate().get() * self.channels().get() as u32;
+    /// Maintains a live replay with a history of at least `duration` seconds.
+    ///
+    /// Note:
+    /// History can be 100ms longer if the source drops before or while the
+    /// replay is being read
+    ///
+    /// # Errors
+    /// If duration is smaller then 100ms
+    fn replayable(
+        self,
+        duration: Duration,
+    ) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
+        if duration < Duration::from_millis(100) {
+            return Err(ReplayDurationTooShort);
+        }
+
+        let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
         let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
         let samples_to_queue =
             (samples_to_queue as usize).next_multiple_of(self.channels().get().into());
 
         let chunk_size =
-            samples_to_queue.min(1000usize.next_multiple_of(self.channels().get().into()));
+            (samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
         let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
 
+        let is_active = Arc::new(AtomicBool::new(true));
         let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
-        (
+        Ok((
             Replay {
                 rx: Arc::clone(&queue),
                 buffer: Vec::new().into_iter(),
                 sleep_duration: duration / 2,
                 sample_rate: self.sample_rate(),
                 channel_count: self.channels(),
+                source_is_active: is_active.clone(),
             },
             Replayable {
                 tx: queue,
                 inner: self,
                 buffer: Vec::with_capacity(chunk_size),
                 chunk_size,
+                is_active,
             },
-        )
+        ))
+    }
+    fn take_samples(self, n: usize) -> TakeSamples<S> {
+        TakeSamples {
+            inner: self,
+            left_to_take: n,
+        }
+    }
+}
+
+pub struct TakeSamples<S> {
+    inner: S,
+    left_to_take: usize,
+}
+
+impl<S: Source> Iterator for TakeSamples<S> {
+    type Item = Sample;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.left_to_take == 0 {
+            None
+        } else {
+            self.left_to_take -= 1;
+            self.inner.next()
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (0, Some(self.left_to_take))
+    }
+}
+
+impl<S: Source> Source for TakeSamples<S> {
+    fn current_span_len(&self) -> Option<usize> {
+        None // does not support spans
+    }
+
+    fn channels(&self) -> ChannelCount {
+        self.inner.channels()
+    }
+
+    fn sample_rate(&self) -> SampleRate {
+        self.inner.sample_rate()
+    }
+
+    fn total_duration(&self) -> Option<Duration> {
+        Some(Duration::from_secs_f64(
+            self.left_to_take as f64
+                / self.sample_rate().get() as f64
+                / self.channels().get() as f64,
+        ))
     }
 }
 
@@ -79,7 +154,7 @@ struct ReplayQueue {
     /// the normal chunk size. This is always equal to the
     /// size of the last element in the queue.
     /// (so normally chunk_size)
-    last_chunk_len: AtomicUsize,
+    last_chunk: Mutex<Vec<Sample>>,
 }
 
 impl ReplayQueue {
@@ -87,21 +162,29 @@ impl ReplayQueue {
         Self {
             inner: ArrayQueue::new(queue_len),
             normal_chunk_len: chunk_size,
-            last_chunk_len: AtomicUsize::new(chunk_size),
+            last_chunk: Mutex::new(Vec::new()),
         }
     }
+    /// Returns the length in samples
     fn len(&self) -> usize {
         self.inner.len().saturating_sub(1) * self.normal_chunk_len
-            + self.last_chunk_len.load(Ordering::Acquire)
+            + self
+                .last_chunk
+                .lock()
+                .expect("Self::push_last can not poison this lock")
+                .len()
     }
 
     fn pop(&self) -> Option<Vec<Sample>> {
-        self.inner.pop()
+        self.inner.pop() // removes element that was inserted first
     }
 
-    fn push_last(&self, samples: Vec<Sample>) {
-        self.last_chunk_len.store(samples.len(), Ordering::Release);
-        let _pushed_out_of_ringbuf = self.inner.force_push(samples);
+    fn push_last(&self, mut samples: Vec<Sample>) {
+        let mut last_chunk = self
+            .last_chunk
+            .lock()
+            .expect("Self::len can not poison this lock");
+        std::mem::swap(&mut *last_chunk, &mut samples);
     }
 
     fn push_normal(&self, samples: Vec<Sample>) {
@@ -148,6 +231,10 @@ where
         self.next = 0;
         Some(self.buffer[0])
     }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.inner.size_hint()
+    }
 }
 
 impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
@@ -156,7 +243,6 @@ where
     F: FnMut(&mut [Sample; N]),
 {
     fn current_span_len(&self) -> Option<usize> {
-        // TODO dvdsk this should be a spanless Source
         None
     }
 
@@ -209,6 +295,10 @@ where
 
         Some(sample)
     }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.inner.size_hint()
+    }
 }
 
 impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
@@ -217,7 +307,6 @@ where
     F: FnMut(&[Sample; N]),
 {
     fn current_span_len(&self) -> Option<usize> {
-        // TODO dvdsk this should be a spanless Source
         None
     }
 
@@ -240,6 +329,7 @@ pub struct Replayable<S: Source> {
     buffer: Vec<Sample>,
     chunk_size: usize,
     tx: Arc<ReplayQueue>,
+    is_active: Arc<AtomicBool>,
 }
 
 impl<S: Source> Iterator for Replayable<S> {
@@ -255,14 +345,18 @@ impl<S: Source> Iterator for Replayable<S> {
         } else {
             let last_chunk = std::mem::take(&mut self.buffer);
             self.tx.push_last(last_chunk);
+            self.is_active.store(false, Ordering::Relaxed);
             None
         }
     }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        self.inner.size_hint()
+    }
 }
 
 impl<S: Source> Source for Replayable<S> {
     fn current_span_len(&self) -> Option<usize> {
-        // Todo dvdsk should be spanless too
         self.inner.current_span_len()
     }
 
@@ -286,22 +380,28 @@ pub struct Replay {
     sleep_duration: Duration,
     sample_rate: SampleRate,
     channel_count: ChannelCount,
+    source_is_active: Arc<AtomicBool>,
 }
 
 impl Replay {
     pub fn source_is_active(&self) -> bool {
-        Arc::strong_count(&self.rx) == 2
+        // - source could return None and not drop
+        // - source could be dropped before returning None
+        self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
     }
 
-    /// Returns duration of what is in the buffer and
-    /// can be returned without blocking.
+    /// Duration of what is in the buffer and can be returned without blocking.
     pub fn duration_ready(&self) -> Duration {
         let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
-        let samples_queued = self.rx.len() + self.buffer.len();
 
-        let seconds_queued = samples_queued as f64 / samples_per_second as f64;
+        let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
         Duration::from_secs_f64(seconds_queued)
     }
+
+    /// Number of samples in the buffer and can be returned without blocking.
+    pub fn samples_ready(&self) -> usize {
+        self.rx.len() + self.buffer.len()
+    }
 }
 
 impl Iterator for Replay {
@@ -325,6 +425,10 @@ impl Iterator for Replay {
             std::thread::sleep(self.sleep_duration);
         }
     }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        ((self.rx.len() + self.buffer.len()), None)
+    }
 }
 
 impl Source for Replay {
@@ -424,7 +528,9 @@ mod tests {
         fn continues_after_history() {
             let input = test_source();
 
-            let (mut replay, mut source) = input.replayable(Duration::from_secs(3));
+            let (mut replay, mut source) = input
+                .replayable(Duration::from_secs(3))
+                .expect("longer then 100ms");
 
             source.by_ref().take(3).count();
             let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
@@ -439,31 +545,49 @@ mod tests {
         fn keeps_only_latest() {
             let input = test_source();
 
-            let (mut replay, mut source) = input.replayable(Duration::from_secs(2));
+            let (mut replay, mut source) = input
+                .replayable(Duration::from_secs(2))
+                .expect("longer then 100ms");
 
             source.by_ref().take(5).count(); // get all items but do not end the source
             let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
-            // Note we do not get the last element, it has not been send yet
-            // due to buffering.
-            assert_eq!(&yielded, &SAMPLES[2..4]);
-
+            assert_eq!(&yielded, &SAMPLES[3..5]);
             source.count(); // exhaust source
-            let yielded: Vec<Sample> = replay.collect();
-            assert_eq!(&yielded, &[SAMPLES[4]]);
+            assert_eq!(replay.next(), None);
         }
 
         #[test]
         fn keeps_correct_amount_of_seconds() {
-            let input = StaticSamplesBuffer::new(nz!(16_000), nz!(1), &[0.0; 40_000]);
+            let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
 
-            let (replay, mut source) = input.replayable(Duration::from_secs(2));
+            let (replay, mut source) = input
+                .replayable(Duration::from_secs(2))
+                .expect("longer then 100ms");
 
-            source.by_ref().count();
-            let n_yielded = replay.count();
-            assert_eq!(
-                n_yielded as u32,
-                source.sample_rate().get() * source.channels().get() as u32 * 2
-            );
+            // exhaust but do not yet end source
+            source.by_ref().take(40_000).count();
+
+            // take all samples we can without blocking
+            let ready = replay.samples_ready();
+            let n_yielded = replay.take_samples(ready).count();
+
+            let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
+            let margin = 16_000 / 10; // 100ms
+            assert!(n_yielded as u32 >= max - margin);
+        }
+
+        #[test]
+        fn samples_ready() {
+            let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
+            let (mut replay, source) = input
+                .replayable(Duration::from_secs(2))
+                .expect("longer then 100ms");
+            assert_eq!(replay.by_ref().samples_ready(), 0);
+
+            source.take(8000).count(); // half a second
+            let margin = 16_000 / 10; // 100ms
+            let ready = replay.samples_ready();
+            assert!(ready >= 8000 - margin);
         }
     }
 }

crates/call/Cargo.toml 🔗

@@ -29,6 +29,7 @@ client.workspace = true
 collections.workspace = true
 fs.workspace = true
 futures.workspace = true
+feature_flags.workspace = true
 gpui = { workspace = true, features = ["screen-capture"] }
 language.workspace = true
 log.workspace = true

crates/call/src/call_impl/room.rs 🔗

@@ -9,6 +9,7 @@ use client::{
     proto::{self, PeerId},
 };
 use collections::{BTreeMap, HashMap, HashSet};
+use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use futures::StreamExt;
 use gpui::{
@@ -1322,16 +1323,18 @@ impl Room {
             return Task::ready(Err(anyhow!("live-kit was not initialized")));
         };
 
+        let is_staff = cx.is_staff();
         let user_name = self
             .user_store
             .read(cx)
             .current_user()
-            .map(|user| user.name.clone())
-            .flatten()
+            .and_then(|user| user.name.clone())
             .unwrap_or_else(|| "unknown".to_string());
 
         cx.spawn(async move |this, cx| {
-            let publication = room.publish_local_microphone_track(&user_name, cx).await;
+            let publication = room
+                .publish_local_microphone_track(user_name, is_staff, cx)
+                .await;
             this.update(cx, |this, cx| {
                 let live_kit = this
                     .live_kit

crates/livekit_client/Cargo.toml 🔗

@@ -22,10 +22,10 @@ test-support = ["collections/test-support", "gpui/test-support"]
 [dependencies]
 anyhow.workspace = true
 async-trait.workspace = true
+audio.workspace = true
 collections.workspace = true
 cpal.workspace = true
 futures.workspace = true
-audio.workspace = true
 gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
 gpui_tokio.workspace = true
 http_client_tls.workspace = true
@@ -35,14 +35,15 @@ log.workspace = true
 nanoid.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
-smallvec.workspace = true
+rodio = { workspace = true, features = ["wav_output", "recording"] }
+serde.workspace = true
+serde_urlencoded.workspace = true
 settings.workspace = true
+smallvec.workspace = true
 tokio-tungstenite.workspace = true
 util.workspace = true
 workspace-hack.workspace = true
 
-rodio = { workspace = true, features = ["wav_output", "recording"] }
-
 [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
 libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
 livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [

crates/livekit_client/examples/test_app.rs 🔗

@@ -256,7 +256,7 @@ impl LivekitWindow {
             let room = self.room.clone();
             cx.spawn_in(window, async move |this, cx| {
                 let (publication, stream) = room
-                    .publish_local_microphone_track("test_user", cx)
+                    .publish_local_microphone_track("test_user".to_string(), false, cx)
                     .await
                     .unwrap();
                 this.update(cx, |this, cx| {

crates/livekit_client/src/lib.rs 🔗

@@ -9,19 +9,19 @@ use rodio::DeviceTrait as _;
 mod record;
 pub use record::CaptureInput;
 
-// #[cfg(not(any(
-//     test,
-//     feature = "test-support",
-//     all(target_os = "windows", target_env = "gnu"),
-//     target_os = "freebsd"
-// )))]
+#[cfg(not(any(
+    test,
+    feature = "test-support",
+    all(target_os = "windows", target_env = "gnu"),
+    target_os = "freebsd"
+)))]
 mod livekit_client;
-// #[cfg(not(any(
-//     test,
-//     feature = "test-support",
-//     all(target_os = "windows", target_env = "gnu"),
-//     target_os = "freebsd"
-// )))]
+#[cfg(not(any(
+    test,
+    feature = "test-support",
+    all(target_os = "windows", target_env = "gnu"),
+    target_os = "freebsd"
+)))]
 pub use livekit_client::*;
 
 // If you need proper LSP in livekit_client you've got to comment
@@ -29,27 +29,27 @@ pub use livekit_client::*;
 // - the mods: mock_client & test and their conditional blocks
 // - the pub use mock_client::* and their conditional blocks
 
-// #[cfg(any(
-//     test,
-//     feature = "test-support",
-//     all(target_os = "windows", target_env = "gnu"),
-//     target_os = "freebsd"
-// ))]
-// mod mock_client;
-// #[cfg(any(
-//     test,
-//     feature = "test-support",
-//     all(target_os = "windows", target_env = "gnu"),
-//     target_os = "freebsd"
-// ))]
-// pub mod test;
-// #[cfg(any(
-//     test,
-//     feature = "test-support",
-//     all(target_os = "windows", target_env = "gnu"),
-//     target_os = "freebsd"
-// ))]
-// pub use mock_client::*;
+#[cfg(any(
+    test,
+    feature = "test-support",
+    all(target_os = "windows", target_env = "gnu"),
+    target_os = "freebsd"
+))]
+mod mock_client;
+#[cfg(any(
+    test,
+    feature = "test-support",
+    all(target_os = "windows", target_env = "gnu"),
+    target_os = "freebsd"
+))]
+pub mod test;
+#[cfg(any(
+    test,
+    feature = "test-support",
+    all(target_os = "windows", target_env = "gnu"),
+    target_os = "freebsd"
+))]
+pub use mock_client::*;
 
 #[derive(Debug, Clone)]
 pub enum Participant {

crates/livekit_client/src/livekit_client.rs 🔗

@@ -97,12 +97,13 @@ impl Room {
 
     pub async fn publish_local_microphone_track(
         &self,
-        user_name: &str,
+        user_name: String,
+        is_staff: bool,
         cx: &mut AsyncApp,
     ) -> Result<(LocalTrackPublication, playback::AudioStream)> {
         let (track, stream) = self
             .playback
-            .capture_local_microphone_track(user_name, &cx)?;
+            .capture_local_microphone_track(user_name, is_staff, &cx)?;
         let publication = self
             .local_participant()
             .publish_track(

crates/livekit_client/src/livekit_client/playback.rs 🔗

@@ -22,6 +22,7 @@ use livekit::webrtc::{
 use log::info;
 use parking_lot::Mutex;
 use rodio::Source;
+use serde::{Deserialize, Serialize};
 use settings::Settings;
 use std::cell::RefCell;
 use std::sync::Weak;
@@ -55,7 +56,13 @@ pub(crate) fn play_remote_audio_track(
                 s.stop();
             }
         });
-    audio::Audio::play_voip_stream(stream, track.name(), cx).context("Could not play audio")?;
+
+    let speaker: Speaker = serde_urlencoded::from_str(&track.name()).unwrap_or_else(|_| Speaker {
+        name: track.name(),
+        is_staff: false,
+    });
+    audio::Audio::play_voip_stream(stream, speaker.name, speaker.is_staff, cx)
+        .context("Could not play audio")?;
 
     let on_drop = util::defer(move || {
         stop_handle_clone.store(true, Ordering::Relaxed);
@@ -141,7 +148,8 @@ impl AudioStack {
 
     pub(crate) fn capture_local_microphone_track(
         &self,
-        user_name: &str,
+        user_name: String,
+        is_staff: bool,
         cx: &AsyncApp,
     ) -> Result<(crate::LocalAudioTrack, AudioStream)> {
         let source = NativeAudioSource::new(
@@ -152,8 +160,14 @@ impl AudioStack {
             10,
         );
 
+        let track_name = serde_urlencoded::to_string(Speaker {
+            name: user_name,
+            is_staff,
+        })
+        .context("Could not encode user information in track name")?;
+
         let track = track::LocalAudioTrack::create_audio_track(
-            user_name,
+            &track_name,
             RtcAudioSource::Native(source.clone()),
         );
 
@@ -170,9 +184,14 @@ impl AudioStack {
         let rodio_pipeline =
             AudioSettings::try_read_global(cx, |setting| setting.rodio_audio).unwrap_or_default();
         let capture_task = if rodio_pipeline {
-            // TODO global might not yet have been initialized
             info!("Using experimental.rodio_audio audio pipeline");
-            audio::Audio::open_microphone(cx.clone(), frame_tx)?;
+            let voip_parts = audio::VoipParts::new(cx)?;
+            thread::spawn(move || {
+                // microphone is non send on mac
+                let microphone = audio::Audio::open_microphone(voip_parts)?;
+                send_to_livekit(frame_tx, microphone);
+                Ok::<(), anyhow::Error>(())
+            });
             Task::ready(Ok(()))
         } else {
             self.executor.spawn(async move {
@@ -357,6 +376,36 @@ impl AudioStack {
     }
 }
 
+#[derive(Serialize, Deserialize)]
+struct Speaker {
+    name: String,
+    is_staff: bool,
+}
+
+fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphone: impl Source) {
+    use cpal::Sample;
+    loop {
+        let sampled: Vec<_> = microphone
+            .by_ref()
+            .take(audio::BUFFER_SIZE)
+            .map(|s| s.to_sample())
+            .collect();
+
+        if frame_tx
+            .unbounded_send(AudioFrame {
+                sample_rate: SAMPLE_RATE.get(),
+                num_channels: CHANNEL_COUNT.get() as u32,
+                samples_per_channel: sampled.len() as u32 / CHANNEL_COUNT.get() as u32,
+                data: Cow::Owned(sampled),
+            })
+            .is_err()
+        {
+            // must rx has dropped or is not consuming
+            break;
+        }
+    }
+}
+
 use super::LocalVideoTrack;
 
 pub enum AudioStream {

crates/livekit_client/src/test.rs 🔗

@@ -728,7 +728,8 @@ impl Room {
 
     pub async fn publish_local_microphone_track(
         &self,
-        _track_name: &str,
+        _track_name: String,
+        _is_staff: bool,
         cx: &mut AsyncApp,
     ) -> Result<(LocalTrackPublication, AudioStream)> {
         self.local_participant().publish_microphone_track(cx).await

crates/zed/Cargo.toml 🔗

@@ -85,7 +85,6 @@ inspector_ui.workspace = true
 install_cli.workspace = true
 jj_ui.workspace = true
 journal.workspace = true
-livekit_client.workspace = true
 language.workspace = true
 language_extension.workspace = true
 language_model.workspace = true

crates/zed/src/zed.rs 🔗

@@ -59,7 +59,7 @@ use settings::{
     initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
     update_settings_file,
 };
-use std::time::{Duration, Instant};
+use std::time::Duration;
 use std::{
     borrow::Cow,
     path::{Path, PathBuf},
@@ -128,10 +128,9 @@ actions!(
 actions!(
     dev,
     [
-        /// Record 10s of audio from your current microphone
-        CaptureAudio,
-        /// Stores last 30s of audio from everyone on the current call
-        /// in a tar file in the current working directory.
+        /// Stores last 30s of audio from zed staff using the experimental rodio
+        /// audio system (including yourself) on the current call in a tar file
+        /// in the current working directory.
         CaptureRecentAudio,
     ]
 );
@@ -927,9 +926,6 @@ fn register_actions(
                 }
             }
         })
-        .register_action(|workspace, _: &CaptureAudio, window, cx| {
-            capture_audio(workspace, window, cx);
-        })
         .register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
             capture_recent_audio(workspace, window, cx);
         });
@@ -1843,108 +1839,6 @@ fn open_settings_file(
     .detach_and_log_err(cx);
 }
 
-fn capture_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
-    #[derive(Default)]
-    enum State {
-        Recording(livekit_client::CaptureInput),
-        Failed(String),
-        Finished(PathBuf),
-        // Used during state switch. Should never occur naturally.
-        #[default]
-        Invalid,
-    }
-
-    struct CaptureAudioNotification {
-        focus_handle: gpui::FocusHandle,
-        start_time: Instant,
-        state: State,
-    }
-
-    impl gpui::EventEmitter<DismissEvent> for CaptureAudioNotification {}
-    impl gpui::EventEmitter<SuppressEvent> for CaptureAudioNotification {}
-    impl gpui::Focusable for CaptureAudioNotification {
-        fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
-            self.focus_handle.clone()
-        }
-    }
-    impl workspace::notifications::Notification for CaptureAudioNotification {}
-
-    const AUDIO_RECORDING_TIME_SECS: u64 = 10;
-
-    impl Render for CaptureAudioNotification {
-        fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-            let elapsed = self.start_time.elapsed().as_secs();
-            let message = match &self.state {
-                State::Recording(capture) => format!(
-                    "Recording {} seconds of audio from input: '{}'",
-                    AUDIO_RECORDING_TIME_SECS - elapsed,
-                    capture.name,
-                ),
-                State::Failed(e) => format!("Error capturing audio: {e}"),
-                State::Finished(path) => format!("Audio recorded to {}", path.display()),
-                State::Invalid => "Error invalid state".to_string(),
-            };
-
-            NotificationFrame::new()
-                .with_title(Some("Recording Audio"))
-                .show_suppress_button(false)
-                .on_close(cx.listener(|_, _, _, cx| {
-                    cx.emit(DismissEvent);
-                }))
-                .with_content(message)
-        }
-    }
-
-    impl CaptureAudioNotification {
-        fn finish(&mut self) {
-            let state = std::mem::take(&mut self.state);
-            self.state = if let State::Recording(capture) = state {
-                match capture.finish() {
-                    Ok(path) => State::Finished(path),
-                    Err(e) => State::Failed(e.to_string()),
-                }
-            } else {
-                state
-            };
-        }
-
-        fn new(cx: &mut Context<Self>) -> Self {
-            cx.spawn(async move |this, cx| {
-                for _ in 0..10 {
-                    cx.background_executor().timer(Duration::from_secs(1)).await;
-                    this.update(cx, |_, cx| {
-                        cx.notify();
-                    })?;
-                }
-
-                this.update(cx, |this, cx| {
-                    this.finish();
-                    cx.notify();
-                })?;
-
-                anyhow::Ok(())
-            })
-            .detach();
-
-            let state = match livekit_client::CaptureInput::start() {
-                Ok(capture_input) => State::Recording(capture_input),
-                Err(err) => State::Failed(format!("Error starting audio capture: {}", err)),
-            };
-
-            Self {
-                focus_handle: cx.focus_handle(),
-                start_time: Instant::now(),
-                state,
-            }
-        }
-    }
-
-    workspace.show_notification(NotificationId::unique::<CaptureAudio>(), cx, |cx| {
-        cx.new(CaptureAudioNotification::new)
-    });
-}
-
-// TODO dvdsk Move this and capture audio somewhere else?
 fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
     struct CaptureRecentAudioNotification {
         focus_handle: gpui::FocusHandle,

tooling/workspace-hack/Cargo.toml 🔗

@@ -47,6 +47,7 @@ clap_builder = { version = "4", default-features = false, features = ["cargo", "
 concurrent-queue = { version = "2" }
 cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
 crc32fast = { version = "1" }
+crossbeam-channel = { version = "0.5" }
 crossbeam-epoch = { version = "0.9" }
 crossbeam-utils = { version = "0.8" }
 deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
@@ -176,6 +177,7 @@ clap_builder = { version = "4", default-features = false, features = ["cargo", "
 concurrent-queue = { version = "2" }
 cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
 crc32fast = { version = "1" }
+crossbeam-channel = { version = "0.5" }
 crossbeam-epoch = { version = "0.9" }
 crossbeam-utils = { version = "0.8" }
 deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
@@ -291,6 +293,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 objc2 = { version = "0.6" }
@@ -319,6 +322,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 objc2 = { version = "0.6" }
@@ -348,6 +352,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 objc2 = { version = "0.6" }
@@ -376,6 +381,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 naga = { version = "25", features = ["msl-out", "wgsl-in"] }
 nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] }
 objc2 = { version = "0.6" }
@@ -413,6 +419,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
@@ -453,6 +460,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
@@ -491,6 +499,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
@@ -531,6 +540,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
@@ -560,6 +570,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 ring = { version = "0.17", features = ["std"] }
 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
 scopeguard = { version = "1" }
@@ -583,6 +594,7 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
 getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
 hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
 ring = { version = "0.17", features = ["std"] }
 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
@@ -616,6 +628,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }
@@ -656,6 +669,7 @@ inout = { version = "0.1", default-features = false, features = ["block-padding"
 itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
 linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] }
 linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] }
+livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" }
 mio = { version = "1", features = ["net", "os-ext"] }
 naga = { version = "25", features = ["spv-out", "wgsl-in"] }
 nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] }