Show shared screen as a pane item

This commit is contained in:
Antonio Scandurra 2022-10-24 10:04:08 +02:00
parent 7e411ae098
commit 476020ae84
10 changed files with 286 additions and 126 deletions

16
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -1,4 +1,4 @@
mod participant;
pub mod participant;
pub mod room;
use anyhow::{anyhow, Result};

View File

@ -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<User>,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub tracks: HashMap<live_kit_client::Sid, RemoteVideoTrack>,
pub tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
}
#[derive(Clone)]
pub struct RemoteVideoTrack {
pub(crate) frame: Option<Frame>,
pub(crate) _live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
pub(crate) _maintain_frame: Arc<Task<()>>,
pub(crate) live_kit_track: Arc<live_kit_client::RemoteVideoTrack>,
}
impl RemoteVideoTrack {
pub fn frame(&self) -> Option<&Frame> {
self.frame.as_ref()
pub fn frames(&self) -> async_broadcast::Receiver<Frame> {
self.live_kit_track.frames()
}
}

View File

@ -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<User>,
@ -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),

View File

@ -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();

View File

@ -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"

View File

@ -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()
}

View File

@ -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<RemoteVideoTrack>,
frame: Option<Frame>,
pub peer_id: PeerId,
user: Arc<User>,
nav_history: Option<ItemNavHistory>,
_maintain_frame: Task<()>,
}
impl SharedScreen {
pub fn new(
track: &Arc<RemoteVideoTrack>,
peer_id: PeerId,
user: Arc<User>,
cx: &mut ViewContext<Self>,
) -> 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<Self>) -> 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<Self>) {
if let Some(nav_history) = self.nav_history.as_ref() {
nav_history.push::<()>(None, cx);
}
}
fn tab_content(
&self,
_: Option<usize>,
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<project::ProjectPath> {
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>) {
self.nav_history = Some(history);
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
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<project::Project>,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Item::save called on SharedScreen")))
}
fn save_as(
&mut self,
_: ModelHandle<project::Project>,
_: PathBuf,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Item::save_as called on SharedScreen")))
}
fn reload(
&mut self,
_: ModelHandle<project::Project>,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Item::reload called on SharedScreen")))
}
fn to_item_events(event: &Self::Event) -> Vec<crate::ItemEvent> {
match event {
Event::Close => vec![crate::ItemEvent::CloseItem],
}
}
}

View File

@ -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::<ModelHandle<ActiveCall>>() {
let call = cx.global::<ModelHandle<ActiveCall>>().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<Self>) -> 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::<SharedScreen>() {
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<ActiveCall>> {
self.active_call.as_ref().map(|(call, _)| call)
}
fn on_active_call_event(
&mut self,
_: ModelHandle<ActiveCall>,
event: &call::room::Event,
cx: &mut ViewContext<Self>,
) {
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 {