From 476020ae8464605436a6f1728674dfa95ea017c1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 24 Oct 2022 10:04:08 +0200 Subject: [PATCH] Show shared screen as a pane item --- Cargo.lock | 16 +-- crates/call/Cargo.toml | 1 + crates/call/src/call.rs | 2 +- crates/call/src/participant.rs | 14 +- crates/call/src/room.rs | 65 ++++----- crates/collab/src/integration_tests.rs | 26 +--- crates/language/Cargo.toml | 2 +- crates/workspace/src/pane_group.rs | 35 +---- crates/workspace/src/shared_screen.rs | 175 +++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 76 ++++++++--- 10 files changed, 286 insertions(+), 126 deletions(-) create mode 100644 crates/workspace/src/shared_screen.rs diff --git a/Cargo.lock b/Cargo.lock index 20f32b79b2..65b562c8ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,17 +170,6 @@ dependencies = [ "rust-embed", ] -[[package]] -name = "async-broadcast" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" -dependencies = [ - "easy-parallel", - "event-listener", - "futures-core", -] - [[package]] name = "async-broadcast" version = "0.4.1" @@ -727,6 +716,7 @@ name = "call" version = "0.1.0" dependencies = [ "anyhow", + "async-broadcast", "client", "collections", "futures 0.3.24", @@ -2983,7 +2973,7 @@ name = "language" version = "0.1.0" dependencies = [ "anyhow", - "async-broadcast 0.3.4", + "async-broadcast", "async-trait", "client", "clock", @@ -3156,7 +3146,7 @@ name = "live_kit_client" version = "0.1.0" dependencies = [ "anyhow", - "async-broadcast 0.4.1", + "async-broadcast", "async-trait", "block", "byteorder", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 7556be6f77..a7a3331d20 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -27,6 +27,7 @@ project = { path = "../project" } util = { path = "../util" } anyhow = "1.0.38" +async-broadcast = "0.4" futures = "0.3" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 6b06d04375..2fb56d83a5 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,4 +1,4 @@ -mod participant; +pub mod participant; pub mod room; use anyhow::{anyhow, Result}; diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index c045bd77dc..dfa456f734 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use client::{proto, User}; use collections::HashMap; -use gpui::{Task, WeakModelHandle}; -use live_kit_client::Frame; +use gpui::WeakModelHandle; +pub use live_kit_client::Frame; use project::Project; use std::sync::Arc; @@ -41,18 +41,16 @@ pub struct RemoteParticipant { pub user: Arc, pub projects: Vec, pub location: ParticipantLocation, - pub tracks: HashMap, + pub tracks: HashMap>, } #[derive(Clone)] pub struct RemoteVideoTrack { - pub(crate) frame: Option, - pub(crate) _live_kit_track: Arc, - pub(crate) _maintain_frame: Arc>, + pub(crate) live_kit_track: Arc, } impl RemoteVideoTrack { - pub fn frame(&self) -> Option<&Frame> { - self.frame.as_ref() + pub fn frames(&self) -> async_broadcast::Receiver { + self.live_kit_track.frames() } } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index ff63e09605..f19389c1ca 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -7,7 +7,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use collections::{BTreeMap, HashSet}; use futures::StreamExt; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; -use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate}; +use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate, Sid}; use postage::stream::Stream; use project::Project; use std::{mem, os::unix::prelude::OsStrExt, sync::Arc}; @@ -15,9 +15,16 @@ use util::{post_inc, ResultExt}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { - Frame { + ParticipantLocationChanged { participant_id: PeerId, - track_id: live_kit_client::Sid, + }, + RemoteVideoTrackShared { + participant_id: PeerId, + track_id: Sid, + }, + RemoteVideoTrackUnshared { + peer_id: PeerId, + track_id: Sid, }, RemoteProjectShared { owner: Arc, @@ -356,7 +363,12 @@ impl Room { if let Some(remote_participant) = this.remote_participants.get_mut(&peer_id) { remote_participant.projects = participant.projects; - remote_participant.location = location; + if location != remote_participant.location { + remote_participant.location = location; + cx.emit(Event::ParticipantLocationChanged { + participant_id: peer_id, + }); + } } else { this.remote_participants.insert( peer_id, @@ -430,44 +442,16 @@ impl Room { .remote_participants .get_mut(&peer_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - let mut frames = track.frames(); participant.tracks.insert( track_id.clone(), - RemoteVideoTrack { - frame: None, - _live_kit_track: track, - _maintain_frame: Arc::new(cx.spawn_weak(|this, mut cx| async move { - while let Some(frame) = frames.next().await { - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - break; - }; - - let done = this.update(&mut cx, |this, cx| { - if let Some(track) = - this.remote_participants.get_mut(&peer_id).and_then( - |participant| participant.tracks.get_mut(&track_id), - ) - { - track.frame = Some(frame); - cx.emit(Event::Frame { - participant_id: peer_id, - track_id: track_id.clone(), - }); - false - } else { - true - } - }); - - if done { - break; - } - } - })), - }, + Arc::new(RemoteVideoTrack { + live_kit_track: track, + }), ); + cx.emit(Event::RemoteVideoTrackShared { + participant_id: peer_id, + track_id, + }); } RemoteVideoTrackUpdate::Unsubscribed { publisher_id, @@ -479,6 +463,7 @@ impl Room { .get_mut(&peer_id) .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; participant.tracks.remove(&track_id); + cx.emit(Event::RemoteVideoTrackUnshared { peer_id, track_id }); } } @@ -750,7 +735,7 @@ struct LiveKitRoom { _maintain_tracks: Task<()>, } -pub enum ScreenTrack { +enum ScreenTrack { None, Pending { publish_id: usize }, Published(LocalTrackPublication), diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index f239a0ce58..b414839110 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -5,10 +5,7 @@ use crate::{ }; use ::rpc::Peer; use anyhow::anyhow; -use call::{ - room::{self, Event}, - ActiveCall, ParticipantLocation, Room, -}; +use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{ self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection, Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT, @@ -33,7 +30,7 @@ use language::{ range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope, }; -use live_kit_client::{Frame, MacOSDisplay}; +use live_kit_client::MacOSDisplay; use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; use project::{ @@ -202,36 +199,25 @@ async fn test_basic_calls( .await .unwrap(); - let frame = Frame { - width: 800, - height: 600, - label: "a".into(), - }; - display.send_frame(frame.clone()); deterministic.run_until_parked(); assert_eq!(events_b.borrow().len(), 1); let event = events_b.borrow().first().unwrap().clone(); - if let Event::Frame { + if let call::room::Event::RemoteVideoTrackShared { participant_id, track_id, } = event { assert_eq!(participant_id, client_a.peer_id().unwrap()); room_b.read_with(cx_b, |room, _| { - assert_eq!( - room.remote_participants()[&client_a.peer_id().unwrap()].tracks[&track_id].frame(), - Some(&frame) - ); + assert!(room.remote_participants()[&client_a.peer_id().unwrap()] + .tracks + .contains_key(&track_id)); }); } else { panic!("unexpected event") } - display.send_frame(frame.clone()); - deterministic.run_until_parked(); - assert_eq!(events_b.borrow().len(), 2); - // User A leaves the room. active_call_a.update(cx_a, |call, cx| { call.hang_up(cx).unwrap(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 96feadbfbc..9a11b80511 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -36,7 +36,7 @@ text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } anyhow = "1.0.38" -async-broadcast = "0.3.4" +async-broadcast = "0.4" async-trait = "0.1" futures = "0.3" lazy_static = "1.4" diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c778115d91..6c379ffd2a 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -2,9 +2,7 @@ use crate::{FollowerStatesByLeader, JoinProject, Pane, Workspace}; use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle, + elements::*, Axis, Border, CursorStyle, ModelHandle, MouseButton, RenderContext, ViewHandle, }; use project::Project; use serde::Deserialize; @@ -144,30 +142,6 @@ impl Member { Border::default() }; - let content = if leader.as_ref().map_or(false, |(_, leader)| { - leader.location == ParticipantLocation::External && !leader.tracks.is_empty() - }) { - let (_, leader) = leader.unwrap(); - let track = leader.tracks.values().next().unwrap(); - let frame = track.frame().cloned(); - Canvas::new(move |bounds, _, cx| { - if let Some(frame) = frame.clone() { - let size = constrain_size_preserving_aspect_ratio( - bounds.size(), - vec2f(frame.width() as f32, frame.height() as f32), - ); - let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; - cx.scene.push_surface(gpui::mac::Surface { - bounds: RectF::new(origin, size), - image_buffer: frame.image(), - }); - } - }) - .boxed() - } else { - ChildView::new(pane, cx).boxed() - }; - let prompt = if let Some((_, leader)) = leader { match leader.location { ParticipantLocation::SharedProject { @@ -251,7 +225,12 @@ impl Member { }; Stack::new() - .with_child(Container::new(content).with_border(border).boxed()) + .with_child( + ChildView::new(pane, cx) + .contained() + .with_border(border) + .boxed(), + ) .with_children(prompt) .boxed() } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs new file mode 100644 index 0000000000..e241542f18 --- /dev/null +++ b/crates/workspace/src/shared_screen.rs @@ -0,0 +1,175 @@ +use crate::{Item, ItemNavHistory}; +use anyhow::{anyhow, Result}; +use call::participant::{Frame, RemoteVideoTrack}; +use client::{PeerId, User}; +use futures::StreamExt; +use gpui::{ + elements::*, + geometry::{rect::RectF, vector::vec2f}, + Entity, ModelHandle, RenderContext, Task, View, ViewContext, +}; +use smallvec::SmallVec; +use std::{ + path::PathBuf, + sync::{Arc, Weak}, +}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task<()>, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + }) + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close)); + }), + } + } +} + +impl Entity for SharedScreen { + type Event = Event; +} + +impl View for SharedScreen { + fn ui_name() -> &'static str { + "SharedScreen" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + let frame = self.frame.clone(); + Canvas::new(move |bounds, _, cx| { + if let Some(frame) = frame.clone() { + let size = constrain_size_preserving_aspect_ratio( + bounds.size(), + vec2f(frame.width() as f32, frame.height() as f32), + ); + let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; + cx.scene.push_surface(gpui::mac::Surface { + bounds: RectF::new(origin, size), + image_buffer: frame.image(), + }); + } + }) + .boxed() + } +} + +impl Item for SharedScreen { + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_ref() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &gpui::AppContext, + ) -> gpui::ElementBox { + Flex::row() + .with_child( + Svg::new("icons/disable_screen_sharing_12.svg") + .with_color(style.label.text.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_margin_right(style.spacing) + .boxed(), + ) + .with_child( + Label::new( + format!("{}'s screen", self.user.github_login), + style.label.clone(), + ) + .aligned() + .boxed(), + ) + .boxed() + } + + fn project_path(&self, _: &gpui::AppContext) -> Option { + Default::default() + } + + fn project_entry_ids(&self, _: &gpui::AppContext) -> SmallVec<[project::ProjectEntryId; 3]> { + Default::default() + } + + fn is_singleton(&self, _: &gpui::AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option { + let track = self.track.upgrade()?; + Some(Self::new(&track, self.peer_id, self.user.clone(), cx)) + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + false + } + + fn save( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + Task::ready(Err(anyhow!("Item::save called on SharedScreen"))) + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + Task::ready(Err(anyhow!("Item::save_as called on SharedScreen"))) + } + + fn reload( + &mut self, + _: ModelHandle, + _: &mut ViewContext, + ) -> Task> { + Task::ready(Err(anyhow!("Item::reload called on SharedScreen"))) + } + + fn to_item_events(event: &Self::Event) -> Vec { + match event { + Event::Close => vec![crate::ItemEvent::CloseItem], + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7f84e58c03..3faec363ab 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,6 +6,7 @@ pub mod dock; pub mod pane; pub mod pane_group; pub mod searchable; +mod shared_screen; pub mod sidebar; mod status_bar; mod toolbar; @@ -36,6 +37,7 @@ use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, Work use searchable::SearchableItemHandle; use serde::Deserialize; use settings::{Autosave, DockAnchor, Settings}; +use shared_screen::SharedScreen; use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem}; use smallvec::SmallVec; use status_bar::StatusBar; @@ -1097,14 +1099,7 @@ impl Workspace { if cx.has_global::>() { let call = cx.global::>().clone(); let mut subscriptions = Vec::new(); - subscriptions.push(cx.observe(&call, |_, _, cx| cx.notify())); - subscriptions.push(cx.subscribe(&call, |this, _, event, cx| { - if let call::room::Event::Frame { participant_id, .. } = event { - if this.follower_states_by_leader.contains_key(&participant_id) { - cx.notify(); - } - } - })); + subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); active_call = Some((call, subscriptions)); } @@ -2517,13 +2512,43 @@ impl Workspace { } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + cx.notify(); + + let call = self.active_call()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participants().get(&leader_id)?; + let mut items_to_add = Vec::new(); - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if let Some(FollowerItem::Loaded(item)) = state - .active_view_id - .and_then(|id| state.items_by_leader_view_id.get(&id)) - { - items_to_add.push((pane.clone(), item.boxed_clone())); + match participant.location { + call::ParticipantLocation::SharedProject { project_id } => { + if Some(project_id) == self.project.read(cx).remote_id() { + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some(FollowerItem::Loaded(item)) = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)) + { + items_to_add.push((pane.clone(), item.boxed_clone())); + } + } + } + } + call::ParticipantLocation::UnsharedProject => {} + call::ParticipantLocation::External => { + let track = participant.tracks.values().next()?.clone(); + let user = participant.user.clone(); + + 'outer: for (pane, _) in self.follower_states_by_leader.get(&leader_id)? { + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == leader_id { + items_to_add.push((pane.clone(), Box::new(item))); + continue 'outer; + } + } + + let shared_screen = + cx.add_view(|cx| SharedScreen::new(&track, leader_id, user.clone(), cx)); + items_to_add.push((pane.clone(), Box::new(shared_screen))); + } } } @@ -2532,8 +2557,8 @@ impl Workspace { if pane == self.active_pane { pane.update(cx, |pane, cx| pane.focus_active_item(cx)); } - cx.notify(); } + None } @@ -2561,6 +2586,27 @@ impl Workspace { fn active_call(&self) -> Option<&ModelHandle> { self.active_call.as_ref().map(|(call, _)| call) } + + fn on_active_call_event( + &mut self, + _: ModelHandle, + event: &call::room::Event, + cx: &mut ViewContext, + ) { + match event { + call::room::Event::ParticipantLocationChanged { + participant_id: peer_id, + } + | call::room::Event::RemoteVideoTrackShared { + participant_id: peer_id, + .. + } + | call::room::Event::RemoteVideoTrackUnshared { peer_id, .. } => { + self.leader_updated(*peer_id, cx); + } + _ => {} + } + } } impl Entity for Workspace {