Detailed changes
@@ -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]
@@ -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",
@@ -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
@@ -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,
+ })
+ }
+}
@@ -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),
+};
@@ -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);
}
@@ -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);
}
}
}
@@ -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
@@ -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
@@ -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 = [
@@ -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| {
@@ -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 {
@@ -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(
@@ -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 {
@@ -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
@@ -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
@@ -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,
@@ -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"] }