logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
[package]
name = "livekit_client"
version = "0.1.0"
edition.workspace = true
description = "Logic for using LiveKit with GPUI"
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/lib.rs"
doctest = false
[[example]]
name = "test_app"
[features]
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
gpui = { workspace = true, features = ["screen-capture", "x11", "wayland"] }
gpui_tokio.workspace = true
http_client_tls.workspace = true
image.workspace = true
livekit_api.workspace = true
log.workspace = true
nanoid.workspace = true
parking_lot.workspace = true
postage.workspace = true
rodio.workspace = true
serde.workspace = true
serde_urlencoded.workspace = true
settings.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc.workspace = true
livekit.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
tokio = { workspace = true, features = ["time"] }
webrtc-sys.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
scap.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
cocoa.workspace = true
core-foundation.workspace = true
core-video.workspace = true
coreaudio-rs = "0.12.1"
objc.workspace = true
[dev-dependencies]
collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
gpui_platform.workspace = true
simplelog.workspace = true
[build-dependencies]
serde.workspace = true
serde_json.workspace = true
[package.metadata.cargo-machete]
ignored = ["serde_json"]

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,392 @@
use std::sync::Arc;
use futures::StreamExt;
use gpui::{
AppContext as _, AsyncApp, Bounds, Context, Entity, InteractiveElement, KeyBinding, Menu,
MenuItem, ParentElement, Pixels, Render, ScreenCaptureStream, SharedString,
StatefulInteractiveElement as _, Styled, Task, Window, WindowBounds, WindowHandle,
WindowOptions, actions, bounds, div, point,
prelude::{FluentBuilder as _, IntoElement},
px, rgb, size,
};
use livekit_client::{
AudioStream, LocalTrackPublication, Participant, ParticipantIdentity, RemoteParticipant,
RemoteTrackPublication, RemoteVideoTrack, RemoteVideoTrackView, Room, RoomEvent,
};
use livekit_api::token::{self, VideoGrant};
use log::LevelFilter;
use simplelog::SimpleLogger;
actions!(livekit_client, [Quit]);
fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
gpui_platform::application().run(|cx| {
#[cfg(any(test, feature = "test-support"))]
println!("USING TEST LIVEKIT");
#[cfg(not(any(test, feature = "test-support")))]
println!("USING REAL LIVEKIT");
gpui_tokio::init(cx);
cx.activate(true);
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus([Menu::new("Zed").items([MenuItem::action("Quit", Quit)])]);
let livekit_url = std::env::var("LIVEKIT_URL").unwrap_or("http://localhost:7880".into());
let livekit_key = std::env::var("LIVEKIT_KEY").unwrap_or("devkey".into());
let livekit_secret = std::env::var("LIVEKIT_SECRET").unwrap_or("secret".into());
let height = px(800.);
let width = px(800.);
cx.spawn(async move |cx| {
let mut windows = Vec::new();
for i in 0..2 {
let token = token::create(
&livekit_key,
&livekit_secret,
Some(&format!("test-participant-{i}")),
VideoGrant::to_join("wtej-trty"),
)
.unwrap();
let bounds = bounds(point(width * i, px(0.0)), size(width, height));
let window = LivekitWindow::new(livekit_url.clone(), token, bounds, cx).await;
windows.push(window);
}
})
.detach();
});
}
fn quit(_: &Quit, cx: &mut gpui::App) {
cx.quit();
}
struct LivekitWindow {
room: Arc<livekit_client::Room>,
microphone_track: Option<LocalTrackPublication>,
screen_share_track: Option<LocalTrackPublication>,
microphone_stream: Option<livekit_client::AudioStream>,
screen_share_stream: Option<Box<dyn ScreenCaptureStream>>,
remote_participants: Vec<(ParticipantIdentity, ParticipantState)>,
_events_task: Task<()>,
}
#[derive(Default)]
struct ParticipantState {
audio_output_stream: Option<(RemoteTrackPublication, AudioStream)>,
muted: bool,
screen_share_output_view: Option<(RemoteVideoTrack, Entity<RemoteVideoTrackView>)>,
speaking: bool,
}
impl LivekitWindow {
async fn new(
url: String,
token: String,
bounds: Bounds<Pixels>,
cx: &mut AsyncApp,
) -> WindowHandle<Self> {
let (room, mut events) =
Room::connect(url.clone(), token, cx)
.await
.unwrap_or_else(|err| {
eprintln!(
"Failed to connect to {url}: {err}.\nTry `foreman start` to run the livekit server"
);
std::process::exit(1)
});
cx.update(|cx| {
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|window, cx| {
cx.new(|cx| {
let _events_task = cx.spawn_in(window, async move |this, cx| {
while let Some(event) = events.next().await {
cx.update(|window, cx| {
this.update(cx, |this: &mut LivekitWindow, cx| {
this.handle_room_event(event, window, cx)
})
})
.ok();
}
});
Self {
room: Arc::new(room),
microphone_track: None,
microphone_stream: None,
screen_share_track: None,
screen_share_stream: None,
remote_participants: Vec::new(),
_events_task,
}
})
},
)
.unwrap()
})
}
fn handle_room_event(&mut self, event: RoomEvent, window: &mut Window, cx: &mut Context<Self>) {
eprintln!("event: {event:?}");
match event {
RoomEvent::TrackUnpublished {
publication,
participant,
} => {
let output = self.remote_participant(participant);
let unpublish_sid = publication.sid();
if output
.audio_output_stream
.as_ref()
.is_some_and(|(track, _)| track.sid() == unpublish_sid)
{
output.audio_output_stream.take();
}
if output
.screen_share_output_view
.as_ref()
.is_some_and(|(track, _)| track.sid() == unpublish_sid)
{
output.screen_share_output_view.take();
}
cx.notify();
}
RoomEvent::TrackSubscribed {
publication,
participant,
track,
} => {
let room = self.room.clone();
let output = self.remote_participant(participant);
match track {
livekit_client::RemoteTrack::Audio(track) => {
output.audio_output_stream = Some((
publication,
room.play_remote_audio_track(&track, cx).unwrap(),
));
}
livekit_client::RemoteTrack::Video(track) => {
output.screen_share_output_view = Some((
track.clone(),
cx.new(|cx| RemoteVideoTrackView::new(track, window, cx)),
));
}
}
cx.notify();
}
RoomEvent::TrackMuted { participant, .. } => {
if let Participant::Remote(participant) = participant {
self.remote_participant(participant).muted = true;
cx.notify();
}
}
RoomEvent::TrackUnmuted { participant, .. } => {
if let Participant::Remote(participant) = participant {
self.remote_participant(participant).muted = false;
cx.notify();
}
}
RoomEvent::ActiveSpeakersChanged { speakers } => {
for (identity, output) in &mut self.remote_participants {
output.speaking = speakers.iter().any(|speaker| {
if let Participant::Remote(speaker) = speaker {
speaker.identity() == *identity
} else {
false
}
});
}
cx.notify();
}
_ => {}
}
cx.notify();
}
fn remote_participant(&mut self, participant: RemoteParticipant) -> &mut ParticipantState {
match self
.remote_participants
.binary_search_by_key(&&participant.identity(), |row| &row.0)
{
Ok(ix) => &mut self.remote_participants[ix].1,
Err(ix) => {
self.remote_participants
.insert(ix, (participant.identity(), ParticipantState::default()));
&mut self.remote_participants[ix].1
}
}
}
fn toggle_mute(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(track) = &self.microphone_track {
if track.is_muted() {
track.unmute(cx);
} else {
track.mute(cx);
}
cx.notify();
} else {
let room = self.room.clone();
cx.spawn_in(window, async move |this, cx| {
let (publication, stream, _input_lag_us) = room
.publish_local_microphone_track("test_user".to_string(), false, cx)
.await
.unwrap();
this.update(cx, |this, cx| {
this.microphone_track = Some(publication);
this.microphone_stream = Some(stream);
cx.notify();
})
})
.detach();
}
}
fn toggle_screen_share(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(track) = self.screen_share_track.take() {
self.screen_share_stream.take();
let participant = self.room.local_participant();
cx.spawn(async move |_, cx| {
participant.unpublish_track(track.sid(), cx).await.unwrap();
})
.detach();
cx.notify();
} else {
let participant = self.room.local_participant();
let sources = cx.screen_capture_sources();
cx.spawn_in(window, async move |this, cx| {
let sources = sources.await.unwrap()?;
let source = sources.into_iter().next().unwrap();
let (publication, stream) = participant
.publish_screenshare_track(&*source, cx)
.await
.unwrap();
this.update(cx, |this, cx| {
this.screen_share_track = Some(publication);
this.screen_share_stream = Some(stream);
cx.notify();
})
})
.detach();
}
}
fn toggle_remote_audio_for_participant(
&mut self,
identity: &ParticipantIdentity,
cx: &mut Context<Self>,
) -> Option<()> {
let participant = self.remote_participants.iter().find_map(|(id, state)| {
if id == identity { Some(state) } else { None }
})?;
let publication = &participant.audio_output_stream.as_ref()?.0;
publication.set_enabled(!publication.is_enabled(), cx);
cx.notify();
Some(())
}
}
impl Render for LivekitWindow {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn button() -> gpui::Div {
div()
.w(px(180.0))
.h(px(30.0))
.px_2()
.m_2()
.bg(rgb(0x8888ff))
}
div()
.bg(rgb(0xffffff))
.size_full()
.flex()
.flex_col()
.child(
div().bg(rgb(0xffd4a8)).flex().flex_row().children([
button()
.id("toggle-mute")
.child(if let Some(track) = &self.microphone_track {
if track.is_muted() { "Unmute" } else { "Mute" }
} else {
"Publish mic"
})
.on_click(cx.listener(|this, _, window, cx| this.toggle_mute(window, cx))),
button()
.id("toggle-screen-share")
.child(if self.screen_share_track.is_none() {
"Share screen"
} else {
"Unshare screen"
})
.on_click(
cx.listener(|this, _, window, cx| this.toggle_screen_share(window, cx)),
),
]),
)
.child(
div()
.id("remote-participants")
.overflow_y_scroll()
.flex()
.flex_col()
.flex_grow()
.children(self.remote_participants.iter().map(|(identity, state)| {
div()
.h(px(1080.0))
.flex()
.flex_col()
.m_2()
.px_2()
.bg(rgb(0x8888ff))
.child(SharedString::from(if state.speaking {
format!("{} (speaking)", &identity.0)
} else if state.muted {
format!("{} (muted)", &identity.0)
} else {
identity.0.clone()
}))
.when_some(state.audio_output_stream.as_ref(), |el, state| {
el.child(
button()
.id(identity.0.clone())
.child(if state.0.is_enabled() {
"Deafen"
} else {
"Undeafen"
})
.on_click(cx.listener({
let identity = identity.clone();
move |this, _, _, cx| {
this.toggle_remote_audio_for_participant(
&identity, cx,
);
}
})),
)
})
.children(state.screen_share_output_view.as_ref().map(|e| e.1.clone()))
})),
)
}
}

View File

@@ -0,0 +1,253 @@
use anyhow::Context as _;
use collections::HashMap;
use cpal::DeviceId;
mod remote_video_track_view;
pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent};
use rodio::DeviceTrait as _;
mod record;
pub use record::CaptureInput;
#[cfg(any(
rust_analyzer,
not(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
))
))]
mod livekit_client;
#[cfg(any(
rust_analyzer,
not(any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
))
))]
pub use livekit_client::*;
#[cfg(all(
not(rust_analyzer),
any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)
))]
mod mock_client;
#[cfg(all(
not(rust_analyzer),
any(
test,
feature = "test-support",
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)
))]
pub mod test;
#[cfg(all(
not(rust_analyzer),
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 {
Local(LocalParticipant),
Remote(RemoteParticipant),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub enum ConnectionQuality {
Excellent,
Good,
Poor,
Lost,
}
#[derive(Debug, Clone)]
pub enum TrackPublication {
Local(LocalTrackPublication),
Remote(RemoteTrackPublication),
}
impl TrackPublication {
pub fn sid(&self) -> TrackSid {
match self {
TrackPublication::Local(local) => local.sid(),
TrackPublication::Remote(remote) => remote.sid(),
}
}
pub fn is_muted(&self) -> bool {
match self {
TrackPublication::Local(local) => local.is_muted(),
TrackPublication::Remote(remote) => remote.is_muted(),
}
}
}
#[derive(Clone, Debug)]
pub enum RemoteTrack {
Audio(RemoteAudioTrack),
Video(RemoteVideoTrack),
}
impl RemoteTrack {
pub fn sid(&self) -> TrackSid {
match self {
RemoteTrack::Audio(remote_audio_track) => remote_audio_track.sid(),
RemoteTrack::Video(remote_video_track) => remote_video_track.sid(),
}
}
}
#[derive(Clone, Debug)]
pub enum LocalTrack {
Audio(LocalAudioTrack),
Video(LocalVideoTrack),
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum RoomEvent {
ParticipantConnected(RemoteParticipant),
ParticipantDisconnected(RemoteParticipant),
LocalTrackPublished {
publication: LocalTrackPublication,
track: LocalTrack,
participant: LocalParticipant,
},
LocalTrackUnpublished {
publication: LocalTrackPublication,
participant: LocalParticipant,
},
LocalTrackSubscribed {
track: LocalTrack,
},
TrackSubscribed {
track: RemoteTrack,
publication: RemoteTrackPublication,
participant: RemoteParticipant,
},
TrackUnsubscribed {
track: RemoteTrack,
publication: RemoteTrackPublication,
participant: RemoteParticipant,
},
TrackSubscriptionFailed {
participant: RemoteParticipant,
// error: livekit::track::TrackError,
track_sid: TrackSid,
},
TrackPublished {
publication: RemoteTrackPublication,
participant: RemoteParticipant,
},
TrackUnpublished {
publication: RemoteTrackPublication,
participant: RemoteParticipant,
},
TrackMuted {
participant: Participant,
publication: TrackPublication,
},
TrackUnmuted {
participant: Participant,
publication: TrackPublication,
},
RoomMetadataChanged {
old_metadata: String,
metadata: String,
},
ParticipantMetadataChanged {
participant: Participant,
old_metadata: String,
metadata: String,
},
ParticipantNameChanged {
participant: Participant,
old_name: String,
name: String,
},
ParticipantAttributesChanged {
participant: Participant,
changed_attributes: HashMap<String, String>,
},
ActiveSpeakersChanged {
speakers: Vec<Participant>,
},
ConnectionQualityChanged {
participant: Participant,
quality: ConnectionQuality,
},
ConnectionStateChanged(ConnectionState),
Connected {
participants_with_tracks: Vec<(RemoteParticipant, Vec<RemoteTrackPublication>)>,
},
Disconnected {
reason: &'static str,
},
Reconnecting,
Reconnected,
}
pub(crate) fn default_device(
input: bool,
device_id: Option<&DeviceId>,
) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> {
let device = audio::resolve_device(device_id, input)?;
let config = if input {
device
.default_input_config()
.context("failed to get default input config")?
} else {
device
.default_output_config()
.context("failed to get default output config")?
};
Ok((device, config))
}
pub(crate) fn get_sample_data(
sample_format: cpal::SampleFormat,
data: &cpal::Data,
) -> anyhow::Result<Vec<i16>> {
match sample_format {
cpal::SampleFormat::I8 => Ok(convert_sample_data::<i8, i16>(data)),
cpal::SampleFormat::I16 => Ok(data.as_slice::<i16>().unwrap().to_vec()),
cpal::SampleFormat::I24 => Ok(convert_sample_data::<cpal::I24, i16>(data)),
cpal::SampleFormat::I32 => Ok(convert_sample_data::<i32, i16>(data)),
cpal::SampleFormat::I64 => Ok(convert_sample_data::<i64, i16>(data)),
cpal::SampleFormat::U8 => Ok(convert_sample_data::<u8, i16>(data)),
cpal::SampleFormat::U16 => Ok(convert_sample_data::<u16, i16>(data)),
cpal::SampleFormat::U32 => Ok(convert_sample_data::<u32, i16>(data)),
cpal::SampleFormat::U64 => Ok(convert_sample_data::<u64, i16>(data)),
cpal::SampleFormat::F32 => Ok(convert_sample_data::<f32, i16>(data)),
cpal::SampleFormat::F64 => Ok(convert_sample_data::<f64, i16>(data)),
_ => anyhow::bail!("Unsupported sample format"),
}
}
pub(crate) fn convert_sample_data<
TSource: cpal::SizedSample,
TDest: cpal::SizedSample + cpal::FromSample<TSource>,
>(
data: &cpal::Data,
) -> Vec<TDest> {
data.as_slice::<TSource>()
.unwrap()
.iter()
.map(|e| e.to_sample::<TDest>())
.collect()
}

View File

@@ -0,0 +1,562 @@
use anyhow::{Context as _, Result};
use audio::AudioSettings;
use collections::HashMap;
use futures::{SinkExt, channel::mpsc};
use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
use gpui_tokio::Tokio;
use playback::capture_local_video_track;
use settings::Settings;
use std::sync::{Arc, atomic::AtomicU64};
#[cfg(target_os = "linux")]
mod linux;
mod playback;
use crate::{ConnectionQuality, LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication};
pub use livekit::SessionStats;
pub use livekit::webrtc::stats::RtcStats;
pub use playback::AudioStream;
pub(crate) use playback::{RemoteVideoFrame, play_remote_video_track};
#[derive(Clone, Debug)]
pub struct RemoteVideoTrack(livekit::track::RemoteVideoTrack);
#[derive(Clone, Debug)]
pub struct RemoteAudioTrack(livekit::track::RemoteAudioTrack);
#[derive(Clone, Debug)]
pub struct RemoteTrackPublication(livekit::publication::RemoteTrackPublication);
#[derive(Clone, Debug)]
pub struct RemoteParticipant(livekit::participant::RemoteParticipant);
#[derive(Clone, Debug)]
pub struct LocalVideoTrack(livekit::track::LocalVideoTrack);
#[derive(Clone, Debug)]
pub struct LocalAudioTrack(livekit::track::LocalAudioTrack);
#[derive(Clone, Debug)]
pub struct LocalTrackPublication(livekit::publication::LocalTrackPublication);
#[derive(Clone, Debug)]
pub struct LocalParticipant(livekit::participant::LocalParticipant);
pub struct Room {
room: livekit::Room,
_task: Task<()>,
playback: playback::AudioStack,
}
pub type TrackSid = livekit::id::TrackSid;
pub type ConnectionState = livekit::ConnectionState;
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct ParticipantIdentity(pub String);
impl Room {
pub async fn connect(
url: String,
token: String,
cx: &mut AsyncApp,
) -> Result<(Self, mpsc::UnboundedReceiver<RoomEvent>)> {
let mut config = livekit::RoomOptions::default();
config.tls_config = livekit::TlsConfig(Some(http_client_tls::tls_config()));
let (room, mut events) = Tokio::spawn(cx, async move {
livekit::Room::connect(&url, &token, config).await
})
.await??;
let (mut tx, rx) = mpsc::unbounded();
let task = cx.background_executor().spawn(async move {
while let Some(event) = events.recv().await {
if let Some(event) = room_event_from_livekit(event) {
tx.send(event).await.ok();
}
}
});
Ok((
Self {
room,
_task: task,
playback: playback::AudioStack::new(cx.background_executor().clone()),
},
rx,
))
}
pub fn local_participant(&self) -> LocalParticipant {
LocalParticipant(self.room.local_participant())
}
pub fn remote_participants(&self) -> HashMap<ParticipantIdentity, RemoteParticipant> {
self.room
.remote_participants()
.into_iter()
.map(|(k, v)| (ParticipantIdentity(k.0), RemoteParticipant(v)))
.collect()
}
pub fn connection_state(&self) -> ConnectionState {
self.room.connection_state()
}
pub fn name(&self) -> String {
self.room.name()
}
pub async fn sid(&self) -> String {
self.room.sid().await.to_string()
}
pub async fn publish_local_microphone_track(
&self,
user_name: String,
is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, playback::AudioStream, Arc<AtomicU64>)> {
let (track, stream, input_lag_us) = self
.playback
.capture_local_microphone_track(user_name, is_staff, &cx)?;
let publication = self
.local_participant()
.publish_track(
livekit::track::LocalTrack::Audio(track.0),
livekit::options::TrackPublishOptions {
source: livekit::track::TrackSource::Microphone,
..Default::default()
},
cx,
)
.await?;
Ok((publication, stream, input_lag_us))
}
pub async fn unpublish_local_track(
&self,
sid: TrackSid,
cx: &mut AsyncApp,
) -> Result<LocalTrackPublication> {
self.local_participant().unpublish_track(sid, cx).await
}
pub fn play_remote_audio_track(
&self,
track: &RemoteAudioTrack,
cx: &mut App,
) -> Result<playback::AudioStream> {
let output_audio_device = AudioSettings::get_global(cx).output_audio_device.clone();
Ok(self
.playback
.play_remote_audio_track(&track.0, output_audio_device))
}
pub async fn get_stats(&self) -> Result<livekit::SessionStats> {
self.room.get_stats().await.map_err(anyhow::Error::from)
}
/// Returns a `Task` that fetches room stats on the Tokio runtime.
///
/// LiveKit's SDK is Tokio-based, so the stats fetch must run within
/// a Tokio context rather than on GPUI's smol-based background executor.
pub fn stats_task(&self, cx: &impl gpui::AppContext) -> Task<Result<livekit::SessionStats>> {
let inner = self.room.clone();
Tokio::spawn_result(cx, async move {
inner.get_stats().await.map_err(anyhow::Error::from)
})
}
}
impl LocalParticipant {
pub fn connection_quality(&self) -> ConnectionQuality {
connection_quality_from_livekit(self.0.connection_quality())
}
pub fn audio_level(&self) -> f32 {
self.0.audio_level()
}
pub async fn publish_screenshare_track(
&self,
source: &dyn ScreenCaptureSource,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, Box<dyn ScreenCaptureStream>)> {
let (track, stream) = capture_local_video_track(source, cx).await?;
let options = livekit::options::TrackPublishOptions {
source: livekit::track::TrackSource::Screenshare,
video_codec: livekit::options::VideoCodec::VP8,
..Default::default()
};
let publication = self
.publish_track(livekit::track::LocalTrack::Video(track.0), options, cx)
.await?;
Ok((publication, stream))
}
async fn publish_track(
&self,
track: livekit::track::LocalTrack,
options: livekit::options::TrackPublishOptions,
cx: &mut AsyncApp,
) -> Result<LocalTrackPublication> {
let participant = self.0.clone();
Tokio::spawn(cx, async move {
participant.publish_track(track, options).await
})
.await?
.map(LocalTrackPublication)
.context("publishing a track")
}
pub async fn unpublish_track(
&self,
sid: TrackSid,
cx: &mut AsyncApp,
) -> Result<LocalTrackPublication> {
let participant = self.0.clone();
Tokio::spawn(cx, async move { participant.unpublish_track(&sid).await })
.await?
.map(LocalTrackPublication)
.context("unpublishing a track")
}
#[cfg(target_os = "linux")]
pub async fn publish_screenshare_track_wayland(
&self,
cx: &mut AsyncApp,
) -> Result<(
LocalTrackPublication,
Box<dyn ScreenCaptureStream>,
futures::channel::oneshot::Receiver<()>,
)> {
let (track, stop_flag, feed_task, failure_rx) =
linux::start_wayland_desktop_capture(cx).await?;
let options = livekit::options::TrackPublishOptions {
source: livekit::track::TrackSource::Screenshare,
video_codec: livekit::options::VideoCodec::VP8,
..Default::default()
};
let publication = self
.publish_track(livekit::track::LocalTrack::Video(track.0), options, cx)
.await?;
Ok((
publication,
Box::new(linux::WaylandScreenCaptureStream::new(stop_flag, feed_task)),
failure_rx,
))
}
}
impl LocalTrackPublication {
pub fn mute(&self, cx: &App) {
let track = self.0.clone();
Tokio::spawn(cx, async move {
track.mute();
})
.detach();
}
pub fn unmute(&self, cx: &App) {
let track = self.0.clone();
Tokio::spawn(cx, async move {
track.unmute();
})
.detach();
}
pub fn sid(&self) -> TrackSid {
self.0.sid()
}
pub fn is_muted(&self) -> bool {
self.0.is_muted()
}
}
impl RemoteParticipant {
pub fn connection_quality(&self) -> ConnectionQuality {
connection_quality_from_livekit(self.0.connection_quality())
}
pub fn audio_level(&self) -> f32 {
self.0.audio_level()
}
pub fn identity(&self) -> ParticipantIdentity {
ParticipantIdentity(self.0.identity().0)
}
pub fn track_publications(&self) -> HashMap<TrackSid, RemoteTrackPublication> {
self.0
.track_publications()
.into_iter()
.map(|(sid, publication)| (sid, RemoteTrackPublication(publication)))
.collect()
}
}
impl RemoteAudioTrack {
pub fn sid(&self) -> TrackSid {
self.0.sid()
}
}
impl RemoteVideoTrack {
pub fn sid(&self) -> TrackSid {
self.0.sid()
}
}
impl RemoteTrackPublication {
pub fn is_muted(&self) -> bool {
self.0.is_muted()
}
pub fn is_enabled(&self) -> bool {
self.0.is_enabled()
}
pub fn track(&self) -> Option<RemoteTrack> {
self.0.track().map(remote_track_from_livekit)
}
pub fn is_audio(&self) -> bool {
self.0.kind() == livekit::track::TrackKind::Audio
}
pub fn set_enabled(&self, enabled: bool, cx: &App) {
let track = self.0.clone();
Tokio::spawn(cx, async move { track.set_enabled(enabled) }).detach();
}
pub fn sid(&self) -> TrackSid {
self.0.sid()
}
}
impl Participant {
pub fn identity(&self) -> ParticipantIdentity {
match self {
Participant::Local(local_participant) => {
ParticipantIdentity(local_participant.0.identity().0)
}
Participant::Remote(remote_participant) => {
ParticipantIdentity(remote_participant.0.identity().0)
}
}
}
pub fn connection_quality(&self) -> ConnectionQuality {
match self {
Participant::Local(local_participant) => local_participant.connection_quality(),
Participant::Remote(remote_participant) => remote_participant.connection_quality(),
}
}
pub fn audio_level(&self) -> f32 {
match self {
Participant::Local(local_participant) => local_participant.audio_level(),
Participant::Remote(remote_participant) => remote_participant.audio_level(),
}
}
}
fn connection_quality_from_livekit(
quality: livekit::prelude::ConnectionQuality,
) -> ConnectionQuality {
match quality {
livekit::prelude::ConnectionQuality::Excellent => ConnectionQuality::Excellent,
livekit::prelude::ConnectionQuality::Good => ConnectionQuality::Good,
livekit::prelude::ConnectionQuality::Poor => ConnectionQuality::Poor,
livekit::prelude::ConnectionQuality::Lost => ConnectionQuality::Lost,
}
}
fn participant_from_livekit(participant: livekit::participant::Participant) -> Participant {
match participant {
livekit::participant::Participant::Local(local) => {
Participant::Local(LocalParticipant(local))
}
livekit::participant::Participant::Remote(remote) => {
Participant::Remote(RemoteParticipant(remote))
}
}
}
fn publication_from_livekit(
publication: livekit::publication::TrackPublication,
) -> TrackPublication {
match publication {
livekit::publication::TrackPublication::Local(local) => {
TrackPublication::Local(LocalTrackPublication(local))
}
livekit::publication::TrackPublication::Remote(remote) => {
TrackPublication::Remote(RemoteTrackPublication(remote))
}
}
}
fn remote_track_from_livekit(track: livekit::track::RemoteTrack) -> RemoteTrack {
match track {
livekit::track::RemoteTrack::Audio(audio) => RemoteTrack::Audio(RemoteAudioTrack(audio)),
livekit::track::RemoteTrack::Video(video) => RemoteTrack::Video(RemoteVideoTrack(video)),
}
}
fn local_track_from_livekit(track: livekit::track::LocalTrack) -> LocalTrack {
match track {
livekit::track::LocalTrack::Audio(audio) => LocalTrack::Audio(LocalAudioTrack(audio)),
livekit::track::LocalTrack::Video(video) => LocalTrack::Video(LocalVideoTrack(video)),
}
}
fn room_event_from_livekit(event: livekit::RoomEvent) -> Option<RoomEvent> {
let event = match event {
livekit::RoomEvent::ParticipantConnected(remote_participant) => {
RoomEvent::ParticipantConnected(RemoteParticipant(remote_participant))
}
livekit::RoomEvent::ParticipantDisconnected(remote_participant) => {
RoomEvent::ParticipantDisconnected(RemoteParticipant(remote_participant))
}
livekit::RoomEvent::LocalTrackPublished {
publication,
track,
participant,
} => RoomEvent::LocalTrackPublished {
publication: LocalTrackPublication(publication),
track: local_track_from_livekit(track),
participant: LocalParticipant(participant),
},
livekit::RoomEvent::LocalTrackUnpublished {
publication,
participant,
} => RoomEvent::LocalTrackUnpublished {
publication: LocalTrackPublication(publication),
participant: LocalParticipant(participant),
},
livekit::RoomEvent::LocalTrackSubscribed { track } => RoomEvent::LocalTrackSubscribed {
track: local_track_from_livekit(track),
},
livekit::RoomEvent::TrackSubscribed {
track,
publication,
participant,
} => RoomEvent::TrackSubscribed {
track: remote_track_from_livekit(track),
publication: RemoteTrackPublication(publication),
participant: RemoteParticipant(participant),
},
livekit::RoomEvent::TrackUnsubscribed {
track,
publication,
participant,
} => RoomEvent::TrackUnsubscribed {
track: remote_track_from_livekit(track),
publication: RemoteTrackPublication(publication),
participant: RemoteParticipant(participant),
},
livekit::RoomEvent::TrackSubscriptionFailed {
participant,
error: _,
track_sid,
} => RoomEvent::TrackSubscriptionFailed {
participant: RemoteParticipant(participant),
track_sid,
},
livekit::RoomEvent::TrackPublished {
publication,
participant,
} => RoomEvent::TrackPublished {
publication: RemoteTrackPublication(publication),
participant: RemoteParticipant(participant),
},
livekit::RoomEvent::TrackUnpublished {
publication,
participant,
} => RoomEvent::TrackUnpublished {
publication: RemoteTrackPublication(publication),
participant: RemoteParticipant(participant),
},
livekit::RoomEvent::TrackMuted {
participant,
publication,
} => RoomEvent::TrackMuted {
publication: publication_from_livekit(publication),
participant: participant_from_livekit(participant),
},
livekit::RoomEvent::TrackUnmuted {
participant,
publication,
} => RoomEvent::TrackUnmuted {
publication: publication_from_livekit(publication),
participant: participant_from_livekit(participant),
},
livekit::RoomEvent::RoomMetadataChanged {
old_metadata,
metadata,
} => RoomEvent::RoomMetadataChanged {
old_metadata,
metadata,
},
livekit::RoomEvent::ParticipantMetadataChanged {
participant,
old_metadata,
metadata,
} => RoomEvent::ParticipantMetadataChanged {
participant: participant_from_livekit(participant),
old_metadata,
metadata,
},
livekit::RoomEvent::ParticipantNameChanged {
participant,
old_name,
name,
} => RoomEvent::ParticipantNameChanged {
participant: participant_from_livekit(participant),
old_name,
name,
},
livekit::RoomEvent::ParticipantAttributesChanged {
participant,
changed_attributes,
} => RoomEvent::ParticipantAttributesChanged {
participant: participant_from_livekit(participant),
changed_attributes: changed_attributes.into_iter().collect(),
},
livekit::RoomEvent::ActiveSpeakersChanged { speakers } => {
RoomEvent::ActiveSpeakersChanged {
speakers: speakers.into_iter().map(participant_from_livekit).collect(),
}
}
livekit::RoomEvent::Connected {
participants_with_tracks,
} => RoomEvent::Connected {
participants_with_tracks: participants_with_tracks
.into_iter()
.map({
|(p, t)| {
(
RemoteParticipant(p),
t.into_iter().map(RemoteTrackPublication).collect(),
)
}
})
.collect(),
},
livekit::RoomEvent::Disconnected { reason } => RoomEvent::Disconnected {
reason: reason.as_str_name(),
},
livekit::RoomEvent::Reconnecting => RoomEvent::Reconnecting,
livekit::RoomEvent::Reconnected => RoomEvent::Reconnected,
livekit::RoomEvent::ConnectionQualityChanged {
quality,
participant,
} => RoomEvent::ConnectionQualityChanged {
participant: participant_from_livekit(participant),
quality: connection_quality_from_livekit(quality),
},
_ => {
log::trace!("dropping livekit event: {:?}", event);
return None;
}
};
Some(event)
}

View File

@@ -0,0 +1,203 @@
use anyhow::Result;
use futures::StreamExt as _;
use futures::channel::oneshot;
use gpui::{AsyncApp, ScreenCaptureStream};
use livekit::track;
use livekit::webrtc::{
prelude::NV12Buffer,
video_frame::{VideoFrame, VideoRotation},
video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource},
};
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
};
static NEXT_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1);
const PIPEWIRE_TIMEOUT_S: u64 = 30;
pub struct WaylandScreenCaptureStream {
id: u64,
stop_flag: Arc<AtomicBool>,
_capture_task: gpui::Task<()>,
}
impl WaylandScreenCaptureStream {
pub fn new(stop_flag: Arc<AtomicBool>, capture_task: gpui::Task<()>) -> Self {
Self {
id: NEXT_WAYLAND_SHARE_ID.fetch_add(1, Ordering::Relaxed),
stop_flag,
_capture_task: capture_task,
}
}
}
impl ScreenCaptureStream for WaylandScreenCaptureStream {
fn metadata(&self) -> Result<gpui::SourceMetadata> {
Ok(gpui::SourceMetadata {
id: self.id,
label: None,
is_main: None,
resolution: gpui::size(gpui::DevicePixels(1), gpui::DevicePixels(1)),
})
}
}
impl Drop for WaylandScreenCaptureStream {
fn drop(&mut self) {
self.stop_flag.store(true, Ordering::Release);
}
}
pub(crate) async fn start_wayland_desktop_capture(
cx: &mut AsyncApp,
) -> Result<(
crate::LocalVideoTrack,
Arc<AtomicBool>,
gpui::Task<()>,
oneshot::Receiver<()>,
)> {
use futures::channel::mpsc;
use gpui::FutureExt as _;
use libwebrtc::desktop_capturer::{
CaptureError, DesktopCaptureSourceType, DesktopCapturer, DesktopCapturerOptions,
DesktopFrame,
};
use libwebrtc::native::yuv_helper::argb_to_nv12;
use std::time::Duration;
use webrtc_sys::webrtc::ffi as webrtc_ffi;
fn webrtc_log_callback(message: String, severity: webrtc_ffi::LoggingSeverity) {
match severity {
webrtc_ffi::LoggingSeverity::Error => log::error!("[webrtc] {}", message.trim()),
_ => log::debug!("[webrtc] {}", message.trim()),
}
}
let _webrtc_log_sink = webrtc_ffi::new_log_sink(webrtc_log_callback);
log::debug!("Wayland desktop capture: WebRTC internal logging enabled");
let stop_flag = Arc::new(AtomicBool::new(false));
let (mut video_source_tx, mut video_source_rx) = mpsc::channel::<NativeVideoSource>(1);
let (failure_tx, failure_rx) = oneshot::channel::<()>();
let mut options = DesktopCapturerOptions::new(DesktopCaptureSourceType::Generic);
options.set_include_cursor(true);
let mut capturer = DesktopCapturer::new(options).ok_or_else(|| {
anyhow::anyhow!(
"Failed to create desktop capturer. \
Check that xdg-desktop-portal is installed and running."
)
})?;
let permanent_error = Arc::new(AtomicBool::new(false));
let stop_cb = stop_flag.clone();
let permanent_error_cb = permanent_error.clone();
capturer.start_capture(None, {
let mut video_source: Option<NativeVideoSource> = None;
let mut current_width: u32 = 0;
let mut current_height: u32 = 0;
let mut video_frame = VideoFrame {
rotation: VideoRotation::VideoRotation0,
buffer: NV12Buffer::new(1, 1),
timestamp_us: 0,
};
move |result: Result<DesktopFrame, CaptureError>| {
let frame = match result {
Ok(frame) => frame,
Err(CaptureError::Temporary) => return,
Err(CaptureError::Permanent) => {
log::error!("Wayland desktop capture encountered a permanent error");
permanent_error_cb.store(true, Ordering::Release);
stop_cb.store(true, Ordering::Release);
return;
}
};
let width = frame.width() as u32;
let height = frame.height() as u32;
if width != current_width || height != current_height {
current_width = width;
current_height = height;
video_frame.buffer = NV12Buffer::new(width, height);
}
let (stride_y, stride_uv) = video_frame.buffer.strides();
let (data_y, data_uv) = video_frame.buffer.data_mut();
argb_to_nv12(
frame.data(),
frame.stride(),
data_y,
stride_y,
data_uv,
stride_uv,
width as i32,
height as i32,
);
if let Some(source) = &video_source {
source.capture_frame(&video_frame);
} else {
let source = NativeVideoSource::new(VideoResolution { width, height }, true);
source.capture_frame(&video_frame);
video_source_tx.try_send(source.clone()).ok();
video_source = Some(source);
}
}
});
log::info!("Wayland desktop capture: starting capture loop");
let stop = stop_flag.clone();
let tokio_task = gpui_tokio::Tokio::spawn(cx, async move {
loop {
if stop.load(Ordering::Acquire) {
break;
}
capturer.capture_frame();
tokio::time::sleep(Duration::from_millis(33)).await;
}
drop(capturer);
if permanent_error.load(Ordering::Acquire) {
log::error!("Wayland screen capture ended due to a permanent capture error");
let _ = failure_tx.send(());
}
});
let capture_task = cx.background_executor().spawn(async move {
if let Err(error) = tokio_task.await {
log::error!("Wayland capture task failed: {error}");
}
});
let executor = cx.background_executor().clone();
let video_source = video_source_rx
.next()
.with_timeout(Duration::from_secs(PIPEWIRE_TIMEOUT_S), &executor)
.await
.map_err(|_| {
stop_flag.store(true, Ordering::Relaxed);
log::error!("Wayland desktop capture timed out.");
anyhow::anyhow!(
"Screen sharing timed out waiting for the first frame. \
Check that xdg-desktop-portal and PipeWire are running, \
and that your portal backend matches your compositor."
)
})?
.ok_or_else(|| {
stop_flag.store(true, Ordering::Relaxed);
anyhow::anyhow!(
"Screen sharing was canceled or the portal denied permission. \
You can try again from the screen share button."
)
})?;
let track = super::LocalVideoTrack(track::LocalVideoTrack::create_video_track(
"screen share",
RtcVideoSource::Native(video_source),
));
Ok((track, stop_flag, capture_task, failure_rx))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
use crate::test;
pub(crate) mod participant;
pub(crate) mod publication;
pub(crate) mod track;
pub type RemoteVideoTrack = track::RemoteVideoTrack;
pub type RemoteAudioTrack = track::RemoteAudioTrack;
pub type RemoteTrackPublication = publication::RemoteTrackPublication;
pub type RemoteParticipant = participant::RemoteParticipant;
pub type LocalVideoTrack = track::LocalVideoTrack;
pub type LocalAudioTrack = track::LocalAudioTrack;
pub type LocalTrackPublication = publication::LocalTrackPublication;
pub type LocalParticipant = participant::LocalParticipant;
pub type Room = test::Room;
pub use test::{ConnectionState, ParticipantIdentity, RtcStats, SessionStats, TrackSid};
pub struct AudioStream {}
#[cfg(not(target_os = "macos"))]
pub type RemoteVideoFrame = std::sync::Arc<gpui::RenderImage>;
#[cfg(target_os = "macos")]
#[derive(Clone)]
pub(crate) struct RemoteVideoFrame {}
#[cfg(target_os = "macos")]
impl Into<gpui::SurfaceSource> for RemoteVideoFrame {
fn into(self) -> gpui::SurfaceSource {
unimplemented!()
}
}
pub(crate) fn play_remote_video_track(
_track: &crate::RemoteVideoTrack,
_: &gpui::BackgroundExecutor,
) -> impl futures::Stream<Item = RemoteVideoFrame> + use<> {
futures::stream::pending()
}

View File

@@ -0,0 +1,222 @@
use crate::{
AudioStream, ConnectionQuality, LocalAudioTrack, LocalTrackPublication, LocalVideoTrack,
Participant, ParticipantIdentity, RemoteTrack, RemoteTrackPublication, TrackSid,
test::{Room, WeakRoom},
};
use anyhow::Result;
use collections::HashMap;
use gpui::{
AsyncApp, DevicePixels, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, size,
};
use std::sync::{Arc, atomic::AtomicU64};
#[derive(Clone, Debug)]
pub struct LocalParticipant {
pub(crate) identity: ParticipantIdentity,
pub(crate) room: Room,
}
#[derive(Clone, Debug)]
pub struct RemoteParticipant {
pub(crate) identity: ParticipantIdentity,
pub(crate) room: WeakRoom,
}
impl Participant {
pub fn identity(&self) -> ParticipantIdentity {
match self {
Participant::Local(participant) => participant.identity.clone(),
Participant::Remote(participant) => participant.identity.clone(),
}
}
pub fn connection_quality(&self) -> ConnectionQuality {
match self {
Participant::Local(p) => p.connection_quality(),
Participant::Remote(p) => p.connection_quality(),
}
}
pub fn audio_level(&self) -> f32 {
match self {
Participant::Local(p) => p.audio_level(),
Participant::Remote(p) => p.audio_level(),
}
}
}
impl LocalParticipant {
pub fn connection_quality(&self) -> ConnectionQuality {
ConnectionQuality::Excellent
}
pub fn audio_level(&self) -> f32 {
0.0
}
pub async fn unpublish_track(&self, track: TrackSid, _cx: &AsyncApp) -> Result<()> {
self.room
.test_server()
.unpublish_track(self.room.token(), &track)
.await
}
pub(crate) async fn publish_microphone_track(
&self,
_cx: &AsyncApp,
) -> Result<(LocalTrackPublication, AudioStream, Arc<AtomicU64>)> {
let this = self.clone();
let server = this.room.test_server();
let sid = server
.publish_audio_track(this.room.token(), &LocalAudioTrack {})
.await?;
Ok((
LocalTrackPublication {
room: self.room.downgrade(),
sid,
},
AudioStream {},
Arc::new(AtomicU64::new(0)),
))
}
pub async fn publish_screenshare_track(
&self,
_source: &dyn ScreenCaptureSource,
_cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, Box<dyn ScreenCaptureStream>)> {
let this = self.clone();
let server = this.room.test_server();
let sid = server
.publish_video_track(this.room.token(), LocalVideoTrack {})
.await?;
Ok((
LocalTrackPublication {
room: self.room.downgrade(),
sid,
},
Box::new(TestScreenCaptureStream {}),
))
}
#[cfg(target_os = "linux")]
pub async fn publish_screenshare_track_wayland(
&self,
_cx: &mut AsyncApp,
) -> Result<(
LocalTrackPublication,
Box<dyn ScreenCaptureStream>,
futures::channel::oneshot::Receiver<()>,
)> {
let (_failure_tx, failure_rx) = futures::channel::oneshot::channel();
let this = self.clone();
let server = this.room.test_server();
let sid = server
.publish_video_track(this.room.token(), LocalVideoTrack {})
.await?;
Ok((
LocalTrackPublication {
room: self.room.downgrade(),
sid,
},
Box::new(TestWaylandScreenCaptureStream::new()),
failure_rx,
))
}
}
impl RemoteParticipant {
pub fn connection_quality(&self) -> ConnectionQuality {
ConnectionQuality::Excellent
}
pub fn audio_level(&self) -> f32 {
0.0
}
pub fn track_publications(&self) -> HashMap<TrackSid, RemoteTrackPublication> {
if let Some(room) = self.room.upgrade() {
let server = room.test_server();
let audio = server
.audio_tracks(room.token())
.unwrap()
.into_iter()
.filter(|track| track.publisher_id() == self.identity)
.map(|track| {
(
track.sid(),
RemoteTrackPublication {
sid: track.sid(),
room: self.room.clone(),
track: RemoteTrack::Audio(track),
},
)
});
let video = server
.video_tracks(room.token())
.unwrap()
.into_iter()
.filter(|track| track.publisher_id() == self.identity)
.map(|track| {
(
track.sid(),
RemoteTrackPublication {
sid: track.sid(),
room: self.room.clone(),
track: RemoteTrack::Video(track),
},
)
});
audio.chain(video).collect()
} else {
HashMap::default()
}
}
pub fn identity(&self) -> ParticipantIdentity {
self.identity.clone()
}
}
struct TestScreenCaptureStream;
impl ScreenCaptureStream for TestScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
id: 0,
is_main: None,
label: None,
resolution: size(DevicePixels(1), DevicePixels(1)),
})
}
}
#[cfg(target_os = "linux")]
static NEXT_TEST_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1);
#[cfg(target_os = "linux")]
struct TestWaylandScreenCaptureStream {
id: u64,
}
#[cfg(target_os = "linux")]
impl TestWaylandScreenCaptureStream {
fn new() -> Self {
Self {
id: NEXT_TEST_WAYLAND_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
}
}
}
#[cfg(target_os = "linux")]
impl ScreenCaptureStream for TestWaylandScreenCaptureStream {
fn metadata(&self) -> Result<SourceMetadata> {
Ok(SourceMetadata {
id: self.id,
is_main: None,
label: None,
resolution: size(DevicePixels(1), DevicePixels(1)),
})
}
}

View File

@@ -0,0 +1,91 @@
use gpui::App;
use crate::{RemoteTrack, TrackSid, test::WeakRoom};
#[derive(Clone, Debug)]
pub struct LocalTrackPublication {
pub(crate) sid: TrackSid,
pub(crate) room: WeakRoom,
}
#[derive(Clone, Debug)]
pub struct RemoteTrackPublication {
pub(crate) sid: TrackSid,
pub(crate) room: WeakRoom,
pub(crate) track: RemoteTrack,
}
impl LocalTrackPublication {
pub fn sid(&self) -> TrackSid {
self.sid.clone()
}
pub fn mute(&self, _cx: &App) {
self.set_mute(true)
}
pub fn unmute(&self, _cx: &App) {
self.set_mute(false)
}
fn set_mute(&self, mute: bool) {
if let Some(room) = self.room.upgrade() {
room.test_server()
.set_track_muted(&room.token(), &self.sid, mute)
.ok();
}
}
pub fn is_muted(&self) -> bool {
if let Some(room) = self.room.upgrade() {
room.test_server()
.is_track_muted(&room.token(), &self.sid)
.unwrap_or(false)
} else {
false
}
}
}
impl RemoteTrackPublication {
pub fn sid(&self) -> TrackSid {
self.sid.clone()
}
pub fn track(&self) -> Option<RemoteTrack> {
Some(self.track.clone())
}
pub fn is_audio(&self) -> bool {
matches!(self.track, RemoteTrack::Audio(_))
}
pub fn is_muted(&self) -> bool {
if let Some(room) = self.room.upgrade() {
room.test_server()
.is_track_muted(&room.token(), &self.sid)
.unwrap_or(false)
} else {
false
}
}
pub fn is_enabled(&self) -> bool {
if let Some(room) = self.room.upgrade() {
!room.0.lock().paused_audio_tracks.contains(&self.sid)
} else {
false
}
}
pub fn set_enabled(&self, enabled: bool, _cx: &App) {
if let Some(room) = self.room.upgrade() {
let paused_audio_tracks = &mut room.0.lock().paused_audio_tracks;
if enabled {
paused_audio_tracks.remove(&self.sid);
} else {
paused_audio_tracks.insert(self.sid.clone());
}
}
}
}

View File

@@ -0,0 +1,56 @@
use std::sync::Arc;
use crate::{
ParticipantIdentity, TrackSid,
test::{TestServerAudioTrack, TestServerVideoTrack, WeakRoom},
};
#[derive(Clone, Debug)]
pub struct LocalVideoTrack {}
#[derive(Clone, Debug)]
pub struct LocalAudioTrack {}
#[derive(Clone, Debug)]
pub struct RemoteVideoTrack {
pub(crate) server_track: Arc<TestServerVideoTrack>,
pub(crate) _room: WeakRoom,
}
#[derive(Clone, Debug)]
pub struct RemoteAudioTrack {
pub(crate) server_track: Arc<TestServerAudioTrack>,
pub(crate) room: WeakRoom,
}
impl RemoteAudioTrack {
pub fn sid(&self) -> TrackSid {
self.server_track.sid.clone()
}
pub fn publisher_id(&self) -> ParticipantIdentity {
self.server_track.publisher_id.clone()
}
pub fn enabled(&self) -> bool {
if let Some(room) = self.room.upgrade() {
!room
.0
.lock()
.paused_audio_tracks
.contains(&self.server_track.sid)
} else {
false
}
}
}
impl RemoteVideoTrack {
pub fn sid(&self) -> TrackSid {
self.server_track.sid.clone()
}
pub fn publisher_id(&self) -> ParticipantIdentity {
self.server_track.publisher_id.clone()
}
}

View File

@@ -0,0 +1,102 @@
use std::{
env,
num::NonZero,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::{Context, Result};
use cpal::DeviceId;
use cpal::traits::{DeviceTrait, StreamTrait};
use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter};
use util::ResultExt;
pub struct CaptureInput {
pub name: String,
pub input_device: Option<DeviceId>,
config: cpal::SupportedStreamConfig,
samples: Arc<Mutex<Vec<i16>>>,
_stream: cpal::Stream,
}
impl CaptureInput {
pub fn start(input_device: Option<DeviceId>) -> anyhow::Result<Self> {
let (device, config) = crate::default_device(true, input_device.as_ref())?;
let name = device
.description()
.map(|desc| desc.name().to_string())
.unwrap_or("<unknown>".to_string());
log::info!("Using microphone: {}", name);
let samples = Arc::new(Mutex::new(Vec::new()));
let stream = start_capture(device, config.clone(), samples.clone())?;
Ok(Self {
name,
input_device,
_stream: stream,
config,
samples,
})
}
pub fn finish(self) -> Result<PathBuf> {
let name = self.name;
let mut path = env::current_dir().context("Could not get current dir")?;
path.push(&format!("test_recording_{name}.wav"));
log::info!("Test recording written to: {}", path.display());
write_out(self.samples, self.config, &path)?;
Ok(path)
}
}
fn start_capture(
device: cpal::Device,
config: cpal::SupportedStreamConfig,
samples: Arc<Mutex<Vec<i16>>>,
) -> Result<cpal::Stream> {
let stream = device
.build_input_stream_raw(
&config.config(),
config.sample_format(),
move |data, _: &_| {
let data = crate::get_sample_data(config.sample_format(), data).log_err();
let Some(data) = data else {
return;
};
samples
.try_lock()
.expect("Only locked after stream ends")
.extend_from_slice(&data);
},
|err| log::error!("error capturing audio track: {:?}", err),
Some(Duration::from_millis(100)),
)
.context("failed to build input stream")?;
stream.play()?;
Ok(stream)
}
fn write_out(
samples: Arc<Mutex<Vec<i16>>>,
config: cpal::SupportedStreamConfig,
path: &Path,
) -> Result<()> {
let samples = std::mem::take(
&mut *samples
.try_lock()
.expect("Stream has ended, callback cant hold the lock"),
);
let samples: Vec<f32> = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect();
let mut samples = SamplesBuffer::new(
NonZero::new(config.channels()).expect("config channel is never zero"),
NonZero::new(config.sample_rate()).expect("config sample_rate is never zero"),
samples,
);
match rodio::wav_to_file(&mut samples, path) {
Ok(_) => Ok(()),
Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)),
}
}

View File

@@ -0,0 +1,109 @@
use super::RemoteVideoTrack;
use futures::StreamExt as _;
use gpui::{
AppContext as _, Context, Empty, Entity, EventEmitter, IntoElement, Render, Task, Window,
};
pub struct RemoteVideoTrackView {
track: RemoteVideoTrack,
latest_frame: Option<crate::RemoteVideoFrame>,
#[cfg(not(target_os = "macos"))]
current_rendered_frame: Option<crate::RemoteVideoFrame>,
#[cfg(not(target_os = "macos"))]
previous_rendered_frame: Option<crate::RemoteVideoFrame>,
_maintain_frame: Task<()>,
}
#[derive(Debug)]
pub enum RemoteVideoTrackViewEvent {
Close,
}
impl RemoteVideoTrackView {
pub fn new(track: RemoteVideoTrack, window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.focus_handle();
let frames = crate::play_remote_video_track(&track, cx.background_executor());
#[cfg(not(target_os = "macos"))]
{
use util::ResultExt;
let window_handle = window.window_handle();
cx.on_release(move |this, cx| {
if let Some(frame) = this.previous_rendered_frame.take() {
window_handle
.update(cx, |_, window, _cx| window.drop_image(frame).log_err())
.ok();
}
if let Some(frame) = this.current_rendered_frame.take() {
window_handle
.update(cx, |_, window, _cx| window.drop_image(frame).log_err())
.ok();
}
})
.detach();
}
Self {
track,
latest_frame: None,
_maintain_frame: cx.spawn_in(window, async move |this, cx| {
futures::pin_mut!(frames);
while let Some(frame) = frames.next().await {
this.update(cx, |this, cx| {
this.latest_frame = Some(frame);
cx.notify();
})
.ok();
}
this.update(cx, |_this, cx| cx.emit(RemoteVideoTrackViewEvent::Close))
.ok();
}),
#[cfg(not(target_os = "macos"))]
current_rendered_frame: None,
#[cfg(not(target_os = "macos"))]
previous_rendered_frame: None,
}
}
pub fn clone(&self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Self> {
cx.new(|cx| Self::new(self.track.clone(), window, cx))
}
}
impl EventEmitter<RemoteVideoTrackViewEvent> for RemoteVideoTrackView {}
impl Render for RemoteVideoTrackView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
#[cfg(target_os = "macos")]
if let Some(latest_frame) = &self.latest_frame {
use gpui::Styled as _;
return gpui::surface(latest_frame.clone())
.size_full()
.into_any_element();
}
#[cfg(not(target_os = "macos"))]
if let Some(latest_frame) = &self.latest_frame {
use gpui::Styled as _;
if let Some(current_rendered_frame) = self.current_rendered_frame.take() {
if let Some(frame) = self.previous_rendered_frame.take() {
// Only drop the frame if it's not also the current frame.
if frame.id != current_rendered_frame.id {
use util::ResultExt as _;
_window.drop_image(frame).log_err();
}
}
self.previous_rendered_frame = Some(current_rendered_frame)
}
self.current_rendered_frame = Some(latest_frame.clone());
use gpui::ParentElement;
return ui::h_flex()
.size_full()
.child(gpui::img(latest_frame.clone()).size_full())
.into_any_element();
}
Empty.into_any_element()
}
}

View File

@@ -0,0 +1,855 @@
use crate::{AudioStream, Participant, RemoteTrack, RoomEvent, TrackPublication};
use crate::mock_client::{participant::*, publication::*, track::*};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::{BTreeMap, HashMap, HashSet, btree_map::Entry as BTreeEntry, hash_map::Entry};
use gpui::{App, AsyncApp, BackgroundExecutor};
use livekit_api::{proto, token};
use parking_lot::Mutex;
use postage::{mpsc, sink::Sink};
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicU64, Ordering::SeqCst},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct ParticipantIdentity(pub String);
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct TrackSid(pub(crate) String);
impl std::fmt::Display for TrackSid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl TryFrom<String> for TrackSid {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(TrackSid(value))
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum ConnectionState {
Connected,
Disconnected,
}
#[derive(Clone, Debug, Default)]
pub struct SessionStats {
pub publisher_stats: Vec<RtcStats>,
pub subscriber_stats: Vec<RtcStats>,
}
#[derive(Clone, Debug)]
pub enum RtcStats {}
static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
pub struct TestServer {
pub url: String,
pub api_key: String,
pub secret_key: String,
rooms: Mutex<HashMap<String, TestServerRoom>>,
executor: BackgroundExecutor,
}
impl TestServer {
pub fn create(
url: String,
api_key: String,
secret_key: String,
executor: BackgroundExecutor,
) -> Result<Arc<TestServer>> {
let mut servers = SERVERS.lock();
if let BTreeEntry::Vacant(e) = servers.entry(url.clone()) {
let server = Arc::new(TestServer {
url,
api_key,
secret_key,
rooms: Default::default(),
executor,
});
e.insert(server.clone());
Ok(server)
} else {
anyhow::bail!("a server with url {url:?} already exists");
}
}
fn get(url: &str) -> Result<Arc<TestServer>> {
Ok(SERVERS
.lock()
.get(url)
.context("no server found for url")?
.clone())
}
pub fn teardown(&self) -> Result<()> {
SERVERS
.lock()
.remove(&self.url)
.with_context(|| format!("server with url {:?} does not exist", self.url))?;
Ok(())
}
pub fn create_api_client(&self) -> TestApiClient {
TestApiClient {
url: self.url.clone(),
}
}
pub async fn create_room(&self, room: String) -> Result<()> {
self.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
if let Entry::Vacant(e) = server_rooms.entry(room.clone()) {
e.insert(Default::default());
Ok(())
} else {
anyhow::bail!("{room:?} already exists");
}
}
async fn delete_room(&self, room: String) -> Result<()> {
self.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
server_rooms
.remove(&room)
.with_context(|| format!("room {room:?} does not exist"))?;
Ok(())
}
async fn join_room(&self, token: String, client_room: Room) -> Result<ParticipantIdentity> {
self.simulate_random_delay().await;
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = (*server_rooms).entry(room_name.to_string()).or_default();
if let Entry::Vacant(e) = room.client_rooms.entry(identity.clone()) {
for server_track in &room.video_tracks {
let track = RemoteTrack::Video(RemoteVideoTrack {
server_track: server_track.clone(),
_room: client_room.downgrade(),
});
client_room
.0
.lock()
.updates_tx
.blocking_send(RoomEvent::TrackSubscribed {
track: track.clone(),
publication: RemoteTrackPublication {
sid: server_track.sid.clone(),
room: client_room.downgrade(),
track,
},
participant: RemoteParticipant {
room: client_room.downgrade(),
identity: server_track.publisher_id.clone(),
},
})
.unwrap();
}
for server_track in &room.audio_tracks {
let track = RemoteTrack::Audio(RemoteAudioTrack {
server_track: server_track.clone(),
room: client_room.downgrade(),
});
client_room
.0
.lock()
.updates_tx
.blocking_send(RoomEvent::TrackSubscribed {
track: track.clone(),
publication: RemoteTrackPublication {
sid: server_track.sid.clone(),
room: client_room.downgrade(),
track,
},
participant: RemoteParticipant {
room: client_room.downgrade(),
identity: server_track.publisher_id.clone(),
},
})
.unwrap();
}
e.insert(client_room);
Ok(identity)
} else {
anyhow::bail!("{identity:?} attempted to join room {room_name:?} twice");
}
}
async fn leave_room(&self, token: String) -> Result<()> {
self.simulate_random_delay().await;
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name:?} does not exist"))?;
room.client_rooms.remove(&identity).with_context(|| {
format!("{identity:?} attempted to leave room {room_name:?} before joining it")
})?;
Ok(())
}
fn remote_participants(
&self,
token: String,
) -> Result<HashMap<ParticipantIdentity, RemoteParticipant>> {
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let local_identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap().to_string();
if let Some(server_room) = self.rooms.lock().get(&room_name) {
let room = server_room
.client_rooms
.get(&local_identity)
.unwrap()
.downgrade();
Ok(server_room
.client_rooms
.iter()
.filter(|(identity, _)| *identity != &local_identity)
.map(|(identity, _)| {
(
identity.clone(),
RemoteParticipant {
room: room.clone(),
identity: identity.clone(),
},
)
})
.collect())
} else {
Ok(Default::default())
}
}
async fn remove_participant(
&self,
room_name: String,
identity: ParticipantIdentity,
) -> Result<()> {
self.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
room.client_rooms
.remove(&identity)
.with_context(|| format!("participant {identity:?} did not join room {room_name:?}"))?;
Ok(())
}
async fn update_participant(
&self,
room_name: String,
identity: String,
permission: proto::ParticipantPermission,
) -> Result<()> {
self.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
room.participant_permissions
.insert(ParticipantIdentity(identity), permission);
Ok(())
}
pub async fn disconnect_client(&self, client_identity: String) {
let client_identity = ParticipantIdentity(client_identity);
self.simulate_random_delay().await;
let mut server_rooms = self.rooms.lock();
for room in server_rooms.values_mut() {
if let Some(room) = room.client_rooms.remove(&client_identity) {
let mut room = room.0.lock();
room.connection_state = ConnectionState::Disconnected;
room.updates_tx
.blocking_send(RoomEvent::Disconnected {
reason: "SIGNAL_CLOSED",
})
.ok();
}
}
}
pub(crate) async fn publish_video_track(
&self,
token: String,
_local_track: LocalVideoTrack,
) -> Result<TrackSid> {
self.simulate_random_delay().await;
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
let can_publish = room
.participant_permissions
.get(&identity)
.map(|permission| permission.can_publish)
.or(claims.video.can_publish)
.unwrap_or(true);
anyhow::ensure!(can_publish, "user is not allowed to publish");
let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap();
let server_track = Arc::new(TestServerVideoTrack {
sid: sid.clone(),
publisher_id: identity.clone(),
});
room.video_tracks.push(server_track.clone());
for (room_identity, client_room) in &room.client_rooms {
if *room_identity != identity {
let track = RemoteTrack::Video(RemoteVideoTrack {
server_track: server_track.clone(),
_room: client_room.downgrade(),
});
let publication = RemoteTrackPublication {
sid: sid.clone(),
room: client_room.downgrade(),
track: track.clone(),
};
let participant = RemoteParticipant {
identity: identity.clone(),
room: client_room.downgrade(),
};
client_room
.0
.lock()
.updates_tx
.blocking_send(RoomEvent::TrackSubscribed {
track,
publication,
participant,
})
.unwrap();
}
}
Ok(sid)
}
pub(crate) async fn publish_audio_track(
&self,
token: String,
_local_track: &LocalAudioTrack,
) -> Result<TrackSid> {
self.simulate_random_delay().await;
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
let can_publish = room
.participant_permissions
.get(&identity)
.map(|permission| permission.can_publish)
.or(claims.video.can_publish)
.unwrap_or(true);
anyhow::ensure!(can_publish, "user is not allowed to publish");
let sid: TrackSid = format!("TR_{}", nanoid::nanoid!(17)).try_into().unwrap();
let server_track = Arc::new(TestServerAudioTrack {
sid: sid.clone(),
publisher_id: identity.clone(),
muted: AtomicBool::new(false),
});
room.audio_tracks.push(server_track.clone());
for (room_identity, client_room) in &room.client_rooms {
if *room_identity != identity {
let track = RemoteTrack::Audio(RemoteAudioTrack {
server_track: server_track.clone(),
room: client_room.downgrade(),
});
let publication = RemoteTrackPublication {
sid: sid.clone(),
room: client_room.downgrade(),
track: track.clone(),
};
let participant = RemoteParticipant {
identity: identity.clone(),
room: client_room.downgrade(),
};
client_room
.0
.lock()
.updates_tx
.blocking_send(RoomEvent::TrackSubscribed {
track,
publication,
participant,
})
.ok();
}
}
Ok(sid)
}
pub(crate) async fn unpublish_track(&self, token: String, track_sid: &TrackSid) -> Result<()> {
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
if let Some(video_to_unpublish) = room.video_tracks.iter().position(|t| t.sid == *track_sid)
{
let video_to_unpublish = room.video_tracks.remove(video_to_unpublish);
for client_room in room
.client_rooms
.iter()
.filter(|(id, _)| **id != identity)
.map(|(_, room)| room)
{
let track = RemoteTrack::Video(RemoteVideoTrack {
server_track: video_to_unpublish.clone(),
_room: client_room.downgrade(),
});
let publication = RemoteTrackPublication {
sid: track_sid.clone(),
room: client_room.downgrade(),
track: track.clone(),
};
let participant = RemoteParticipant {
identity: identity.clone(),
room: client_room.downgrade(),
};
let event = RoomEvent::TrackUnsubscribed {
track,
publication,
participant,
};
client_room.0.lock().updates_tx.blocking_send(event).ok();
}
}
if let Some(audio_to_unpublish) = room.audio_tracks.iter().position(|t| t.sid == *track_sid)
{
let audio_to_unpublish = room.audio_tracks.remove(audio_to_unpublish);
for client_room in room
.client_rooms
.iter()
.filter(|(id, _)| **id != identity)
.map(|(_, room)| room)
{
let track = RemoteTrack::Audio(RemoteAudioTrack {
server_track: audio_to_unpublish.clone(),
room: client_room.downgrade(),
});
let publication = RemoteTrackPublication {
sid: track_sid.clone(),
room: client_room.downgrade(),
track: track.clone(),
};
let participant = RemoteParticipant {
identity: identity.clone(),
room: client_room.downgrade(),
};
let event = RoomEvent::TrackUnsubscribed {
track,
publication,
participant,
};
client_room.0.lock().updates_tx.blocking_send(event).ok();
}
}
Ok(())
}
pub(crate) fn set_track_muted(
&self,
token: &str,
track_sid: &TrackSid,
muted: bool,
) -> Result<()> {
let claims = livekit_api::token::validate(token, &self.secret_key)?;
let room_name = claims.video.room.unwrap();
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
if let Some(track) = room
.audio_tracks
.iter_mut()
.find(|track| track.sid == *track_sid)
{
track.muted.store(muted, SeqCst);
for (id, client_room) in room.client_rooms.iter() {
if *id != identity {
let participant = Participant::Remote(RemoteParticipant {
identity: identity.clone(),
room: client_room.downgrade(),
});
let track = RemoteTrack::Audio(RemoteAudioTrack {
server_track: track.clone(),
room: client_room.downgrade(),
});
let publication = TrackPublication::Remote(RemoteTrackPublication {
sid: track_sid.clone(),
room: client_room.downgrade(),
track,
});
let event = if muted {
RoomEvent::TrackMuted {
participant,
publication,
}
} else {
RoomEvent::TrackUnmuted {
participant,
publication,
}
};
client_room
.0
.lock()
.updates_tx
.blocking_send(event)
.unwrap();
}
}
}
Ok(())
}
pub(crate) fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> {
let claims = livekit_api::token::validate(token, &self.secret_key).ok()?;
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
let room = server_rooms.get_mut(&*room_name)?;
room.audio_tracks.iter().find_map(|track| {
if track.sid == *track_sid {
Some(track.muted.load(SeqCst))
} else {
None
}
})
}
pub(crate) fn video_tracks(&self, token: String) -> Result<Vec<RemoteVideoTrack>> {
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let room_name = claims.video.room.unwrap();
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
let client_room = room
.client_rooms
.get(&identity)
.context("not a participant in room")?;
Ok(room
.video_tracks
.iter()
.map(|track| RemoteVideoTrack {
server_track: track.clone(),
_room: client_room.downgrade(),
})
.collect())
}
pub(crate) fn audio_tracks(&self, token: String) -> Result<Vec<RemoteAudioTrack>> {
let claims = livekit_api::token::validate(&token, &self.secret_key)?;
let room_name = claims.video.room.unwrap();
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let mut server_rooms = self.rooms.lock();
let room = server_rooms
.get_mut(&*room_name)
.with_context(|| format!("room {room_name} does not exist"))?;
let client_room = room
.client_rooms
.get(&identity)
.context("not a participant in room")?;
Ok(room
.audio_tracks
.iter()
.map(|track| RemoteAudioTrack {
server_track: track.clone(),
room: client_room.downgrade(),
})
.collect())
}
async fn simulate_random_delay(&self) {
#[cfg(any(test, feature = "test-support"))]
self.executor.simulate_random_delay().await;
}
}
#[derive(Default, Debug)]
struct TestServerRoom {
client_rooms: HashMap<ParticipantIdentity, Room>,
video_tracks: Vec<Arc<TestServerVideoTrack>>,
audio_tracks: Vec<Arc<TestServerAudioTrack>>,
participant_permissions: HashMap<ParticipantIdentity, proto::ParticipantPermission>,
}
#[derive(Debug)]
pub(crate) struct TestServerVideoTrack {
pub(crate) sid: TrackSid,
pub(crate) publisher_id: ParticipantIdentity,
// frames_rx: async_broadcast::Receiver<Frame>,
}
#[derive(Debug)]
pub(crate) struct TestServerAudioTrack {
pub(crate) sid: TrackSid,
pub(crate) publisher_id: ParticipantIdentity,
pub(crate) muted: AtomicBool,
}
pub struct TestApiClient {
url: String,
}
#[async_trait]
impl livekit_api::Client for TestApiClient {
fn url(&self) -> &str {
&self.url
}
async fn create_room(&self, name: String) -> Result<()> {
let server = TestServer::get(&self.url)?;
server.create_room(name).await?;
Ok(())
}
async fn delete_room(&self, name: String) -> Result<()> {
let server = TestServer::get(&self.url)?;
server.delete_room(name).await?;
Ok(())
}
async fn remove_participant(&self, room: String, identity: String) -> Result<()> {
let server = TestServer::get(&self.url)?;
server
.remove_participant(room, ParticipantIdentity(identity))
.await?;
Ok(())
}
async fn update_participant(
&self,
room: String,
identity: String,
permission: livekit_api::proto::ParticipantPermission,
) -> Result<()> {
let server = TestServer::get(&self.url)?;
server
.update_participant(room, identity, permission)
.await?;
Ok(())
}
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
let server = TestServer::get(&self.url)?;
token::create(
&server.api_key,
&server.secret_key,
Some(identity),
token::VideoGrant::to_join(room),
)
}
fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
let server = TestServer::get(&self.url)?;
token::create(
&server.api_key,
&server.secret_key,
Some(identity),
token::VideoGrant::for_guest(room),
)
}
}
pub(crate) struct RoomState {
pub(crate) url: String,
pub(crate) token: String,
pub(crate) local_identity: ParticipantIdentity,
pub(crate) connection_state: ConnectionState,
pub(crate) paused_audio_tracks: HashSet<TrackSid>,
pub(crate) updates_tx: mpsc::Sender<RoomEvent>,
}
#[derive(Clone, Debug)]
pub struct Room(pub(crate) Arc<Mutex<RoomState>>);
#[derive(Clone, Debug)]
pub(crate) struct WeakRoom(Weak<Mutex<RoomState>>);
impl std::fmt::Debug for RoomState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Room")
.field("url", &self.url)
.field("token", &self.token)
.field("local_identity", &self.local_identity)
.field("connection_state", &self.connection_state)
.field("paused_audio_tracks", &self.paused_audio_tracks)
.finish()
}
}
impl Room {
pub(crate) fn downgrade(&self) -> WeakRoom {
WeakRoom(Arc::downgrade(&self.0))
}
pub fn connection_state(&self) -> ConnectionState {
self.0.lock().connection_state
}
pub fn local_participant(&self) -> LocalParticipant {
let identity = self.0.lock().local_identity.clone();
LocalParticipant {
identity,
room: self.clone(),
}
}
pub async fn connect(
url: String,
token: String,
_cx: &mut AsyncApp,
) -> Result<(Self, mpsc::Receiver<RoomEvent>)> {
let server = TestServer::get(&url)?;
let (updates_tx, updates_rx) = mpsc::channel(1024);
let this = Self(Arc::new(Mutex::new(RoomState {
local_identity: ParticipantIdentity(String::new()),
url: url.to_string(),
token: token.to_string(),
connection_state: ConnectionState::Disconnected,
paused_audio_tracks: Default::default(),
updates_tx,
})));
let identity = server
.join_room(token.to_string(), this.clone())
.await
.context("room join")?;
{
let mut state = this.0.lock();
state.local_identity = identity;
state.connection_state = ConnectionState::Connected;
}
Ok((this, updates_rx))
}
pub fn remote_participants(&self) -> HashMap<ParticipantIdentity, RemoteParticipant> {
self.test_server()
.remote_participants(self.0.lock().token.clone())
.unwrap()
}
pub(crate) fn test_server(&self) -> Arc<TestServer> {
TestServer::get(&self.0.lock().url).unwrap()
}
pub(crate) fn token(&self) -> String {
self.0.lock().token.clone()
}
pub fn name(&self) -> String {
"test_room".to_string()
}
pub async fn sid(&self) -> String {
"RM_test_session".to_string()
}
pub fn play_remote_audio_track(
&self,
_track: &RemoteAudioTrack,
_cx: &App,
) -> anyhow::Result<AudioStream> {
Ok(AudioStream {})
}
pub async fn unpublish_local_track(&self, sid: TrackSid, cx: &mut AsyncApp) -> Result<()> {
self.local_participant().unpublish_track(sid, cx).await
}
pub async fn publish_local_microphone_track(
&self,
_track_name: String,
_is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, AudioStream, Arc<AtomicU64>)> {
self.local_participant().publish_microphone_track(cx).await
}
pub async fn get_stats(&self) -> Result<SessionStats> {
Ok(SessionStats::default())
}
pub fn stats_task(&self, _cx: &impl gpui::AppContext) -> gpui::Task<Result<SessionStats>> {
gpui::Task::ready(Ok(SessionStats::default()))
}
}
impl Drop for RoomState {
fn drop(&mut self) {
if self.connection_state == ConnectionState::Connected
&& let Ok(server) = TestServer::get(&self.url)
{
let executor = server.executor.clone();
let token = self.token.clone();
executor
.spawn(async move { server.leave_room(token).await.ok() })
.detach();
}
}
}
impl WeakRoom {
pub(crate) fn upgrade(&self) -> Option<Room> {
self.0.upgrade().map(Room)
}
}