logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
57
crates/call/Cargo.toml
Normal file
57
crates/call/Cargo.toml
Normal file
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "call"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/call.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"client/test-support",
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"livekit_client/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
audio.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
gpui_tokio.workspace = true
|
||||
language.workspace = true
|
||||
livekit_client.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
project.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
telemetry.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
livekit_client = { workspace = true, features = ["test-support"] }
|
||||
1
crates/call/LICENSE-GPL
Symbolic link
1
crates/call/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
5
crates/call/src/call.rs
Normal file
5
crates/call/src/call.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod call_settings;
|
||||
|
||||
mod call_impl;
|
||||
|
||||
pub use call_impl::*;
|
||||
232
crates/call/src/call_impl/diagnostics.rs
Normal file
232
crates/call/src/call_impl/diagnostics.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use gpui::{Context, Task, WeakEntity};
|
||||
use livekit_client::ConnectionQuality;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::room::Room;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CallStats {
|
||||
pub connection_quality: Option<ConnectionQuality>,
|
||||
pub effective_quality: Option<ConnectionQuality>,
|
||||
pub latency_ms: Option<f64>,
|
||||
pub jitter_ms: Option<f64>,
|
||||
pub packet_loss_pct: Option<f64>,
|
||||
pub input_lag: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct CallDiagnostics {
|
||||
stats: CallStats,
|
||||
room: WeakEntity<Room>,
|
||||
poll_task: Option<Task<()>>,
|
||||
stats_update_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl CallDiagnostics {
|
||||
pub fn new(room: WeakEntity<Room>, cx: &mut Context<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
stats: CallStats::default(),
|
||||
room,
|
||||
poll_task: None,
|
||||
stats_update_task: None,
|
||||
};
|
||||
this.start_polling(cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> &CallStats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
fn start_polling(&mut self, cx: &mut Context<Self>) {
|
||||
self.poll_task = Some(cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
if this.update(cx, |this, cx| this.poll_stats(cx)).is_err() {
|
||||
break;
|
||||
}
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn poll_stats(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(room) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection_quality = room.read(cx).connection_quality();
|
||||
self.stats.connection_quality = Some(connection_quality);
|
||||
self.stats.input_lag = room.read(cx).input_lag();
|
||||
|
||||
let stats_future = room.read(cx).get_stats(cx);
|
||||
|
||||
let background_task = cx.background_executor().spawn(async move {
|
||||
let session_stats = stats_future.await;
|
||||
session_stats.map(|stats| compute_network_stats(&stats))
|
||||
});
|
||||
|
||||
self.stats_update_task = Some(cx.spawn(async move |this, cx| {
|
||||
let result = background_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some(computed) = result {
|
||||
this.stats.latency_ms = computed.latency_ms;
|
||||
this.stats.jitter_ms = computed.jitter_ms;
|
||||
this.stats.packet_loss_pct = computed.packet_loss_pct;
|
||||
}
|
||||
let quality = this
|
||||
.stats
|
||||
.connection_quality
|
||||
.unwrap_or(ConnectionQuality::Lost);
|
||||
this.stats.effective_quality =
|
||||
Some(effective_connection_quality(quality, &this.stats));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
struct ComputedNetworkStats {
|
||||
latency_ms: Option<f64>,
|
||||
jitter_ms: Option<f64>,
|
||||
packet_loss_pct: Option<f64>,
|
||||
}
|
||||
|
||||
fn compute_network_stats(stats: &livekit_client::SessionStats) -> ComputedNetworkStats {
|
||||
let mut min_rtt: Option<f64> = None;
|
||||
let mut max_jitter: Option<f64> = None;
|
||||
let mut total_packets_received: u64 = 0;
|
||||
let mut total_packets_lost: i64 = 0;
|
||||
|
||||
let all_stats = stats
|
||||
.publisher_stats
|
||||
.iter()
|
||||
.chain(stats.subscriber_stats.iter());
|
||||
|
||||
for stat in all_stats {
|
||||
extract_metrics(
|
||||
stat,
|
||||
&mut min_rtt,
|
||||
&mut max_jitter,
|
||||
&mut total_packets_received,
|
||||
&mut total_packets_lost,
|
||||
);
|
||||
}
|
||||
|
||||
let total_expected = total_packets_received as i64 + total_packets_lost;
|
||||
let packet_loss_pct = if total_expected > 0 {
|
||||
Some((total_packets_lost as f64 / total_expected as f64) * 100.0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
ComputedNetworkStats {
|
||||
latency_ms: min_rtt.map(|rtt| rtt * 1000.0),
|
||||
jitter_ms: max_jitter.map(|j| j * 1000.0),
|
||||
packet_loss_pct,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(rust_analyzer),
|
||||
any(
|
||||
test,
|
||||
feature = "test-support",
|
||||
all(target_os = "windows", target_env = "gnu"),
|
||||
target_os = "freebsd"
|
||||
)
|
||||
))]
|
||||
fn extract_metrics(
|
||||
_stat: &livekit_client::RtcStats,
|
||||
_min_rtt: &mut Option<f64>,
|
||||
_max_jitter: &mut Option<f64>,
|
||||
_total_packets_received: &mut u64,
|
||||
_total_packets_lost: &mut i64,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
rust_analyzer,
|
||||
not(any(
|
||||
test,
|
||||
feature = "test-support",
|
||||
all(target_os = "windows", target_env = "gnu"),
|
||||
target_os = "freebsd"
|
||||
))
|
||||
))]
|
||||
fn extract_metrics(
|
||||
stat: &livekit_client::RtcStats,
|
||||
min_rtt: &mut Option<f64>,
|
||||
max_jitter: &mut Option<f64>,
|
||||
total_packets_received: &mut u64,
|
||||
total_packets_lost: &mut i64,
|
||||
) {
|
||||
use livekit_client::RtcStats;
|
||||
|
||||
match stat {
|
||||
RtcStats::CandidatePair(pair) => {
|
||||
let rtt = pair.candidate_pair.current_round_trip_time;
|
||||
if rtt > 0.0 {
|
||||
*min_rtt = Some(match *min_rtt {
|
||||
Some(current) => current.min(rtt),
|
||||
None => rtt,
|
||||
});
|
||||
}
|
||||
}
|
||||
RtcStats::InboundRtp(inbound) => {
|
||||
let jitter = inbound.received.jitter;
|
||||
if jitter > 0.0 {
|
||||
*max_jitter = Some(match *max_jitter {
|
||||
Some(current) => current.max(jitter),
|
||||
None => jitter,
|
||||
});
|
||||
}
|
||||
*total_packets_received += inbound.received.packets_received;
|
||||
*total_packets_lost += inbound.received.packets_lost;
|
||||
}
|
||||
RtcStats::RemoteInboundRtp(remote_inbound) => {
|
||||
let rtt = remote_inbound.remote_inbound.round_trip_time;
|
||||
if rtt > 0.0 {
|
||||
*min_rtt = Some(match *min_rtt {
|
||||
Some(current) => current.min(rtt),
|
||||
None => rtt,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn metric_quality(value: f64, warn_threshold: f64, error_threshold: f64) -> ConnectionQuality {
|
||||
if value < warn_threshold {
|
||||
ConnectionQuality::Excellent
|
||||
} else if value < error_threshold {
|
||||
ConnectionQuality::Poor
|
||||
} else {
|
||||
ConnectionQuality::Lost
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the effective connection quality by taking the worst of the
|
||||
/// LiveKit-reported quality and each individual metric rating.
|
||||
fn effective_connection_quality(
|
||||
livekit_quality: ConnectionQuality,
|
||||
stats: &CallStats,
|
||||
) -> ConnectionQuality {
|
||||
let mut worst = livekit_quality;
|
||||
|
||||
if let Some(latency) = stats.latency_ms {
|
||||
worst = worst.max(metric_quality(latency, 100.0, 300.0));
|
||||
}
|
||||
if let Some(jitter) = stats.jitter_ms {
|
||||
worst = worst.max(metric_quality(jitter, 30.0, 75.0));
|
||||
}
|
||||
if let Some(loss) = stats.packet_loss_pct {
|
||||
worst = worst.max(metric_quality(loss, 1.0, 5.0));
|
||||
}
|
||||
if let Some(lag) = stats.input_lag {
|
||||
let lag_ms = lag.as_secs_f64() * 1000.0;
|
||||
worst = worst.max(metric_quality(lag_ms, 20.0, 50.0));
|
||||
}
|
||||
|
||||
worst
|
||||
}
|
||||
811
crates/call/src/call_impl/mod.rs
Normal file
811
crates/call/src/call_impl/mod.rs
Normal file
@@ -0,0 +1,811 @@
|
||||
pub mod diagnostics;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use audio::Audio;
|
||||
use client::{ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, proto};
|
||||
use collections::HashSet;
|
||||
use futures::{Future, FutureExt, channel::oneshot, future::Shared};
|
||||
use gpui::{
|
||||
AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task,
|
||||
TaskExt, WeakEntity, Window,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use workspace::{
|
||||
ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, MultiWorkspace, MultiWorkspaceEvent, Pane,
|
||||
RemoteCollaborator, SharedScreen, Workspace,
|
||||
};
|
||||
|
||||
pub use livekit_client::{RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent};
|
||||
pub use room::Room;
|
||||
|
||||
use crate::call_settings::CallSettings;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
|
||||
let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx));
|
||||
let active_call_handle = active_call.downgrade();
|
||||
|
||||
cx.observe_new(move |_multi_workspace: &mut MultiWorkspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
let active_call_handle = active_call_handle.clone();
|
||||
cx.subscribe_in(
|
||||
&cx.entity(),
|
||||
window,
|
||||
move |multi_workspace, _, event: &MultiWorkspaceEvent, window, cx| {
|
||||
if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged { .. })
|
||||
&& window.is_window_active()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let project = multi_workspace.workspace().read(cx).project().clone();
|
||||
if let Ok(task) = active_call_handle.update(cx, |active_call, cx| {
|
||||
active_call.set_location(Some(&project), cx)
|
||||
}) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.set_global(GlobalAnyActiveCall(Arc::new(ActiveCallEntity(active_call))))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ActiveCallEntity(Entity<ActiveCall>);
|
||||
|
||||
impl AnyActiveCall for ActiveCallEntity {
|
||||
fn entity(&self) -> gpui::AnyEntity {
|
||||
self.0.clone().into_any()
|
||||
}
|
||||
|
||||
fn is_in_room(&self, cx: &App) -> bool {
|
||||
self.0.read(cx).room().is_some()
|
||||
}
|
||||
|
||||
fn room_id(&self, cx: &App) -> Option<u64> {
|
||||
Some(self.0.read(cx).room()?.read(cx).id())
|
||||
}
|
||||
|
||||
fn channel_id(&self, cx: &App) -> Option<ChannelId> {
|
||||
self.0.read(cx).room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
fn hang_up(&self, cx: &mut App) -> Task<Result<()>> {
|
||||
self.0.update(cx, |this, cx| this.hang_up(cx))
|
||||
}
|
||||
|
||||
fn unshare_project(&self, project: Entity<Project>, cx: &mut App) -> Result<()> {
|
||||
self.0
|
||||
.update(cx, |this, cx| this.unshare_project(project, cx))
|
||||
}
|
||||
|
||||
fn remote_participant_for_peer_id(
|
||||
&self,
|
||||
peer_id: proto::PeerId,
|
||||
cx: &App,
|
||||
) -> Option<workspace::RemoteCollaborator> {
|
||||
let room = self.0.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
Some(RemoteCollaborator {
|
||||
user: participant.user.clone(),
|
||||
peer_id: participant.peer_id,
|
||||
location: participant.location,
|
||||
participant_index: participant.participant_index,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_sharing_project(&self, cx: &App) -> bool {
|
||||
self.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map_or(false, |room| room.read(cx).is_sharing_project())
|
||||
}
|
||||
|
||||
fn is_sharing_screen(&self, cx: &App) -> bool {
|
||||
self.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map_or(false, |room| room.read(cx).is_sharing_screen())
|
||||
}
|
||||
|
||||
fn has_remote_participants(&self, cx: &App) -> bool {
|
||||
self.0.read(cx).room().map_or(false, |room| {
|
||||
!room.read(cx).remote_participants().is_empty()
|
||||
})
|
||||
}
|
||||
|
||||
fn local_participant_is_guest(&self, cx: &App) -> bool {
|
||||
self.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map_or(false, |room| room.read(cx).local_participant_is_guest())
|
||||
}
|
||||
|
||||
fn client(&self, cx: &App) -> Arc<Client> {
|
||||
self.0.read(cx).client()
|
||||
}
|
||||
|
||||
fn share_on_join(&self, cx: &App) -> bool {
|
||||
CallSettings::get_global(cx).share_on_join
|
||||
}
|
||||
|
||||
fn join_channel(&self, channel_id: ChannelId, cx: &mut App) -> Task<Result<bool>> {
|
||||
let task = self
|
||||
.0
|
||||
.update(cx, |this, cx| this.join_channel(channel_id, cx));
|
||||
cx.spawn(async move |_cx| {
|
||||
let result = task.await?;
|
||||
Ok(result.is_some())
|
||||
})
|
||||
}
|
||||
|
||||
fn room_update_completed(&self, cx: &mut App) -> Task<()> {
|
||||
let Some(room) = self.0.read(cx).room().cloned() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
let future = room.update(cx, |room, _cx| room.room_update_completed());
|
||||
cx.spawn(async move |_cx| {
|
||||
future.await;
|
||||
})
|
||||
}
|
||||
|
||||
fn most_active_project(&self, cx: &App) -> Option<(u64, u64)> {
|
||||
let room = self.0.read(cx).room()?;
|
||||
room.read(cx).most_active_project(cx)
|
||||
}
|
||||
|
||||
fn share_project(&self, project: Entity<Project>, cx: &mut App) -> Task<Result<u64>> {
|
||||
self.0
|
||||
.update(cx, |this, cx| this.share_project(project, cx))
|
||||
}
|
||||
|
||||
fn join_project(
|
||||
&self,
|
||||
project_id: u64,
|
||||
language_registry: Arc<language::LanguageRegistry>,
|
||||
fs: Arc<dyn fs::Fs>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Project>>> {
|
||||
let Some(room) = self.0.read(cx).room().cloned() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("not in a call")));
|
||||
};
|
||||
room.update(cx, |room, cx| {
|
||||
room.join_project(project_id, language_registry, fs, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn peer_id_for_user_in_room(&self, user_id: u64, cx: &App) -> Option<proto::PeerId> {
|
||||
let room = self.0.read(cx).room()?.read(cx);
|
||||
room.remote_participants()
|
||||
.values()
|
||||
.find(|p| p.user.legacy_id == user_id)
|
||||
.map(|p| p.peer_id)
|
||||
}
|
||||
|
||||
fn subscribe(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
handler: Box<
|
||||
dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>),
|
||||
>,
|
||||
) -> Subscription {
|
||||
cx.subscribe_in(
|
||||
&self.0,
|
||||
window,
|
||||
move |workspace, _, event: &room::Event, window, cx| {
|
||||
let mapped = match event {
|
||||
room::Event::ParticipantLocationChanged { participant_id } => {
|
||||
Some(ActiveCallEvent::ParticipantLocationChanged {
|
||||
participant_id: *participant_id,
|
||||
})
|
||||
}
|
||||
room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
Some(ActiveCallEvent::RemoteVideoTracksChanged {
|
||||
participant_id: *participant_id,
|
||||
})
|
||||
}
|
||||
room::Event::LocalScreenShareStarted => {
|
||||
Some(ActiveCallEvent::LocalScreenShareStarted)
|
||||
}
|
||||
room::Event::LocalScreenShareStopped => {
|
||||
Some(ActiveCallEvent::LocalScreenShareStopped)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(event) = mapped {
|
||||
handler(workspace, &event, window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn create_shared_screen(
|
||||
&self,
|
||||
peer_id: client::proto::PeerId,
|
||||
pane: &Entity<Pane>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<workspace::SharedScreen>> {
|
||||
let room = self.0.read(cx).room()?.clone();
|
||||
let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?;
|
||||
let track = participant.video_tracks.values().next()?.clone();
|
||||
let user = participant.user.clone();
|
||||
|
||||
for item in pane.read(cx).items_of_type::<SharedScreen>() {
|
||||
if item.read(cx).peer_id == peer_id {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
Some(cx.new(|cx: &mut Context<SharedScreen>| {
|
||||
let my_sid = track.sid();
|
||||
cx.subscribe(
|
||||
&room,
|
||||
move |_: &mut SharedScreen,
|
||||
_: Entity<Room>,
|
||||
ev: &room::Event,
|
||||
cx: &mut Context<SharedScreen>| {
|
||||
if let room::Event::RemoteVideoTrackUnsubscribed { sid } = ev
|
||||
&& *sid == my_sid
|
||||
{
|
||||
cx.emit(workspace::shared_screen::Event::Close);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.observe_release(
|
||||
&room,
|
||||
|_: &mut SharedScreen, _: &mut Room, cx: &mut Context<SharedScreen>| {
|
||||
cx.emit(workspace::shared_screen::Event::Close);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx));
|
||||
cx.subscribe(
|
||||
&view,
|
||||
|_: &mut SharedScreen,
|
||||
_: Entity<RemoteVideoTrackView>,
|
||||
ev: &RemoteVideoTrackViewEvent,
|
||||
cx: &mut Context<SharedScreen>| match ev {
|
||||
RemoteVideoTrackViewEvent::Close => {
|
||||
cx.emit(workspace::shared_screen::Event::Close);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
pub(super) fn clone_remote_video_track_view(
|
||||
view: &AnyView,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyView {
|
||||
let view = view
|
||||
.clone()
|
||||
.downcast::<RemoteVideoTrackView>()
|
||||
.expect("SharedScreen view must be a RemoteVideoTrackView");
|
||||
let cloned = view.update(cx, |view, cx| view.clone(window, cx));
|
||||
AnyView::from(cloned)
|
||||
}
|
||||
|
||||
SharedScreen::new(
|
||||
peer_id,
|
||||
user,
|
||||
AnyView::from(view),
|
||||
clone_remote_video_track_view,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn peer_ids_with_video_tracks(&self, cx: &App) -> Vec<proto::PeerId> {
|
||||
let Some(room) = self.0.read(cx).room() else {
|
||||
return Vec::new();
|
||||
};
|
||||
room.read(cx)
|
||||
.remote_participants()
|
||||
.values()
|
||||
.filter(|p| p.has_video_tracks())
|
||||
.map(|p| p.peer_id)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OneAtATime {
|
||||
cancel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl OneAtATime {
|
||||
/// spawn a task in the given context.
|
||||
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
|
||||
/// otherwise you'll see the result of the task.
|
||||
fn spawn<F, Fut, R>(&mut self, cx: &mut App, f: F) -> Task<Result<Option<R>>>
|
||||
where
|
||||
F: 'static + FnOnce(AsyncApp) -> Fut,
|
||||
Fut: Future<Output = Result<R>>,
|
||||
R: 'static,
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.cancel.replace(tx);
|
||||
cx.spawn(async move |cx| {
|
||||
futures::select_biased! {
|
||||
_ = rx.fuse() => Ok(None),
|
||||
result = f(cx.clone()).fuse() => result.map(Some),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn running(&self) -> bool {
|
||||
self.cancel
|
||||
.as_ref()
|
||||
.is_some_and(|cancel| !cancel.is_canceled())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct IncomingCall {
|
||||
pub room_id: u64,
|
||||
pub calling_user: Arc<User>,
|
||||
pub participants: Vec<Arc<User>>,
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(Entity<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<Entity<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakEntity<Project>>,
|
||||
_join_debouncer: OneAtATime,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
watch::Sender<Option<IncomingCall>>,
|
||||
watch::Receiver<Option<IncomingCall>>,
|
||||
),
|
||||
client: Arc<Client>,
|
||||
user_store: Entity<UserStore>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActiveCall {}
|
||||
|
||||
impl ActiveCall {
|
||||
fn new(client: Arc<Client>, user_store: Entity<UserStore>, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
_join_debouncer: OneAtATime { cancel: None },
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.weak_entity(), Self::handle_incoming_call),
|
||||
client.add_message_handler(cx.weak_entity(), Self::handle_call_canceled),
|
||||
],
|
||||
client,
|
||||
user_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &App) -> Option<ChannelId> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
async fn handle_incoming_call(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::IncomingCall>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let call = IncomingCall {
|
||||
room_id: envelope.payload.room_id,
|
||||
participants: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_users(envelope.payload.participant_user_ids, cx)
|
||||
})
|
||||
.await?,
|
||||
calling_user: user_store
|
||||
.update(&mut cx, |user_store, cx| {
|
||||
user_store.get_user(envelope.payload.calling_user_id, cx)
|
||||
})
|
||||
.await?,
|
||||
initial_project: envelope.payload.initial_project,
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
*this.incoming_call.0.borrow_mut() = Some(call);
|
||||
});
|
||||
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_call_canceled(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::CallCanceled>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
let mut incoming_call = this.incoming_call.0.borrow_mut();
|
||||
if incoming_call
|
||||
.as_ref()
|
||||
.is_some_and(|call| call.room_id == envelope.payload.room_id)
|
||||
{
|
||||
incoming_call.take();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
Self::try_global(cx).unwrap()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
let any = cx.try_global::<GlobalAnyActiveCall>()?;
|
||||
any.0.entity().downcast::<Self>().ok()
|
||||
}
|
||||
|
||||
pub fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Entity<Project>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
cx.notify();
|
||||
|
||||
if self._join_debouncer.running() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let room = room.await.map_err(|err| anyhow!("{err:?}"))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(cx, |room, cx| room.share_project(initial_project, cx))
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(cx, move |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(async move |this, cx| {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| this.set_room(Some(room.clone()), cx))?
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(cx, |this, _| this.pending_room_creation = None)?;
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.background_spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{err:?}"))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.report_call_event("Participant Invited", cx)
|
||||
})?;
|
||||
} else {
|
||||
//TODO: report collaboration error
|
||||
log::error!("invite failed: {:?}", result);
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
cx.notify();
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let room_id = if let Some(room) = self.room() {
|
||||
room.read(cx).id()
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no active call")));
|
||||
};
|
||||
|
||||
let client = self.client.clone();
|
||||
cx.background_spawn(async move {
|
||||
client
|
||||
.request(proto::CancelCall {
|
||||
room_id,
|
||||
called_user_id,
|
||||
})
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
|
||||
self.incoming_call.1.clone()
|
||||
}
|
||||
|
||||
pub fn accept_incoming(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
if self.room.is_some() {
|
||||
return Task::ready(Err(anyhow!("cannot join while on another call")));
|
||||
}
|
||||
|
||||
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
|
||||
call
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("no incoming call")));
|
||||
};
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
|
||||
let room_id = call.room_id;
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self
|
||||
._join_debouncer
|
||||
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let room = join.await?;
|
||||
this.update(cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.report_call_event("Incoming Call Accepted", cx)
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self, _: &mut Context<Self>) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.context("no incoming call")?;
|
||||
telemetry::event!("Incoming Call Declined", room_id = call.room_id);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Option<Entity<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
|
||||
if self.pending_room_creation.is_some() {
|
||||
return Task::ready(Ok(None));
|
||||
}
|
||||
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let join = self._join_debouncer.spawn(cx, move |cx| async move {
|
||||
Room::join_channel(channel_id, client, user_store, cx).await
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let room = join.await?;
|
||||
this.update(cx, |this, cx| this.set_room(room.clone(), cx))?
|
||||
.await?;
|
||||
this.update(cx, |this, cx| this.report_call_event("Channel Joined", cx))?;
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("Call Ended", cx);
|
||||
|
||||
Audio::end_call(cx);
|
||||
|
||||
let channel_id = self.channel_id(cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
cx.emit(Event::RoomLeft { channel_id });
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("Project Shared", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let (room, _) = self.room.as_ref().context("no active call")?;
|
||||
self.report_call_event("Project Unshared", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&WeakEntity<Project>> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&Entity<Project>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_room(&mut self, room: Option<Entity<Room>>, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
cx.notify();
|
||||
if let Some(room) = room {
|
||||
if room.read(cx).status().is_offline() {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
} else {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&room, |this, room, cx| {
|
||||
if room.read(cx).status().is_offline() {
|
||||
this.set_room(None, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}),
|
||||
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
|
||||
];
|
||||
self.room = Some((room.clone(), subscriptions));
|
||||
let location = self
|
||||
.location
|
||||
.as_ref()
|
||||
.and_then(|location| location.upgrade());
|
||||
let channel_id = room.read(cx).channel_id();
|
||||
cx.emit(Event::RoomJoined { channel_id });
|
||||
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
|
||||
}
|
||||
} else {
|
||||
self.room = None;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn room(&self) -> Option<&Entity<Room>> {
|
||||
self.room.as_ref().map(|(room, _)| room)
|
||||
}
|
||||
|
||||
pub fn client(&self) -> Arc<Client> {
|
||||
self.client.clone()
|
||||
}
|
||||
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut App) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
telemetry::event!(
|
||||
operation,
|
||||
room_id = room.id(),
|
||||
channel_id = room.channel_id()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
use crate::OneAtATime;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_one_at_a_time(cx: &mut TestAppContext) {
|
||||
let mut one_at_a_time = OneAtATime { cancel: None };
|
||||
|
||||
assert_eq!(
|
||||
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let (a, b) = cx.update(|cx| {
|
||||
(
|
||||
one_at_a_time.spawn(cx, |_| async {
|
||||
panic!("");
|
||||
}),
|
||||
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(a.await.unwrap(), None::<u32>);
|
||||
assert_eq!(b.await.unwrap(), Some(3));
|
||||
|
||||
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
|
||||
drop(one_at_a_time);
|
||||
|
||||
assert_eq!(promise.await.unwrap(), None);
|
||||
}
|
||||
}
|
||||
51
crates/call/src/call_impl/participant.rs
Normal file
51
crates/call/src/call_impl/participant.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use client::{ParticipantIndex, User, proto};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakEntity;
|
||||
use livekit_client::AudioStream;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use livekit_client::TrackSid;
|
||||
pub use livekit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LocalParticipant {
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub active_project: Option<WeakEntity<Project>>,
|
||||
pub role: proto::ChannelRole,
|
||||
}
|
||||
|
||||
impl LocalParticipant {
|
||||
pub fn can_write(&self) -> bool {
|
||||
matches!(
|
||||
self.role,
|
||||
proto::ChannelRole::Admin | proto::ChannelRole::Member
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteParticipant {
|
||||
pub user: Arc<User>,
|
||||
pub peer_id: proto::PeerId,
|
||||
pub role: proto::ChannelRole,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub location: workspace::ParticipantLocation,
|
||||
pub participant_index: ParticipantIndex,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
|
||||
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
|
||||
}
|
||||
|
||||
impl RemoteParticipant {
|
||||
pub fn has_video_tracks(&self) -> bool {
|
||||
!self.video_tracks.is_empty()
|
||||
}
|
||||
|
||||
pub fn can_write(&self) -> bool {
|
||||
matches!(
|
||||
self.role,
|
||||
proto::ChannelRole::Admin | proto::ChannelRole::Member
|
||||
)
|
||||
}
|
||||
}
|
||||
1885
crates/call/src/call_impl/room.rs
Normal file
1885
crates/call/src/call_impl/room.rs
Normal file
File diff suppressed because it is too large
Load Diff
17
crates/call/src/call_settings.rs
Normal file
17
crates/call/src/call_settings.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use settings::{RegisterSetting, Settings};
|
||||
|
||||
#[derive(Debug, RegisterSetting)]
|
||||
pub struct CallSettings {
|
||||
pub mute_on_join: bool,
|
||||
pub share_on_join: bool,
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
fn from_settings(content: &settings::SettingsContent) -> Self {
|
||||
let call = content.calls.clone().unwrap();
|
||||
CallSettings {
|
||||
mute_on_join: call.mute_on_join.unwrap(),
|
||||
share_on_join: call.share_on_join.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user