diff --git a/.config/hakari.toml b/.config/hakari.toml index 5168887581..2050065cc2 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -24,7 +24,7 @@ workspace-members = [ third-party = [ { name = "reqwest", version = "0.11.27" }, # build of remote_server should not include scap / its x11 dependency - { name = "scap", git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318" }, + { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index a5ea621cd1..bc69de7a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14185,7 +14185,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.8" -source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318" +source = "git+https://github.com/zed-industries/scap?rev=808aa5c45b41e8f44729d02e38fd00a2fe2722e7#808aa5c45b41e8f44729d02e38fd00a2fe2722e7" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -16484,6 +16484,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "title_bar" version = "0.1.0" dependencies = [ + "anyhow", "auto_update", "call", "chrono", @@ -18729,8 +18730,7 @@ dependencies = [ [[package]] name = "windows-capture" version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16" +source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" dependencies = [ "clap", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index aa9af9a423..0169d32eb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -553,8 +553,7 @@ rustc-demangle = "0.1.23" rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" -# When updating scap rev, also update it in .config/hakari.toml -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false } +scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false } schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } @@ -708,6 +707,7 @@ features = [ [patch.crates-io] notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } +windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } # Makes the workspace hack crate refer to the local one, but only when you're building locally workspace-hack = { path = "tooling/workspace-hack" } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 7aac72ed46..afeee4c924 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -11,15 +11,18 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; +use gpui::{ + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource, + ScreenCaptureStream, Task, WeakEntity, +}; use gpui_tokio::Tokio; use language::LanguageRegistry; use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent}; -use livekit_client::{self as livekit, TrackSid}; +use livekit_client::{self as livekit, AudioStream, TrackSid}; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use settings::Settings as _; -use std::{any::Any, future::Future, mem, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration}; use util::{ResultExt, TryFutureExt, post_inc}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -1251,12 +1254,21 @@ impl Room { }) } - pub fn is_screen_sharing(&self) -> bool { + pub fn is_sharing_screen(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { !matches!(live_kit.screen_track, LocalTrack::None) }) } + pub fn shared_screen_id(&self) -> Option { + self.live_kit.as_ref().and_then(|lk| match lk.screen_track { + LocalTrack::Published { ref _stream, .. } => { + _stream.metadata().ok().map(|meta| meta.id) + } + _ => None, + }) + } + pub fn is_sharing_mic(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { !matches!(live_kit.microphone_track, LocalTrack::None) @@ -1369,11 +1381,15 @@ impl Room { }) } - pub fn share_screen(&mut self, cx: &mut Context) -> Task> { + pub fn share_screen( + &mut self, + source: Rc, + cx: &mut Context, + ) -> Task> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } - if self.is_screen_sharing() { + if self.is_sharing_screen() { return Task::ready(Err(anyhow!("screen was already shared"))); } @@ -1386,20 +1402,8 @@ impl Room { return Task::ready(Err(anyhow!("live-kit was not initialized"))); }; - let sources = cx.screen_capture_sources(); - cx.spawn(async move |this, cx| { - let sources = sources - .await - .map_err(|error| error.into()) - .and_then(|sources| sources); - let source = - sources.and_then(|sources| sources.into_iter().next().context("no display found")); - - let publication = match source { - Ok(source) => participant.publish_screenshare_track(&*source, cx).await, - Err(error) => Err(error), - }; + let publication = participant.publish_screenshare_track(&*source, cx).await; this.update(cx, |this, cx| { let live_kit = this @@ -1426,7 +1430,7 @@ impl Room { } else { live_kit.screen_track = LocalTrack::Published { track_publication: publication, - _stream: Box::new(stream), + _stream: stream, }; cx.notify(); } @@ -1492,7 +1496,7 @@ impl Room { } } - pub fn unshare_screen(&mut self, cx: &mut Context) -> Result<()> { + pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context) -> Result<()> { anyhow::ensure!(!self.status.is_offline(), "room is offline"); let live_kit = self @@ -1516,7 +1520,10 @@ impl Room { cx.notify(); } - Audio::play_sound(Sound::StopScreenshare, cx); + if play_sound { + Audio::play_sound(Sound::StopScreenshare, cx); + } + Ok(()) } } @@ -1624,8 +1631,8 @@ fn spawn_room_connection( struct LiveKitRoom { room: Rc, - screen_track: LocalTrack, - microphone_track: LocalTrack, + screen_track: LocalTrack, + microphone_track: LocalTrack, /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. muted_by_user: bool, deafened: bool, @@ -1663,18 +1670,18 @@ impl LiveKitRoom { } } -enum LocalTrack { +enum LocalTrack { None, Pending { publish_id: usize, }, Published { track_publication: LocalTrackPublication, - _stream: Box, + _stream: Box, }, } -impl Default for LocalTrack { +impl Default for LocalTrack { fn default() -> Self { Self::None } diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1a4c3a70a4..d9fd8ffeb2 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -439,7 +439,7 @@ async fn test_basic_following( editor_a1.item_id() ); - #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] { use crate::rpc::RECONNECT_TIMEOUT; use gpui::TestScreenCaptureSource; @@ -456,11 +456,19 @@ async fn test_basic_following( .await .unwrap(); cx_b.set_screen_capture_sources(vec![display]); + let source = cx_b + .read(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_b .update(cx_b, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(source, cx)) }) .await .unwrap(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d1099a327a..9795c27574 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -277,11 +277,19 @@ async fn test_basic_calls( let events_b = active_call_events(cx_b); let events_c = active_call_events(cx_c); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); @@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared( // User A shares their screen let display = gpui::TestScreenCaptureSource::new(); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); + active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ec23e2c3f5..4d5973481e 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -144,10 +144,22 @@ pub fn init(cx: &mut App) { if let Some(room) = room { window.defer(cx, move |_window, cx| { room.update(cx, |room, cx| { - if room.is_screen_sharing() { - room.unshare_screen(cx).ok(); + if room.is_sharing_screen() { + room.unshare_screen(true, cx).ok(); } else { - room.share_screen(cx).detach_and_log_err(cx); + let sources = cx.screen_capture_sources(); + + cx.spawn(async move |room, cx| { + let sources = sources.await??; + let first = sources.into_iter().next(); + if let Some(first) = first { + room.update(cx, |room, cx| room.share_screen(first, cx))? + .await + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); }; }); }); @@ -528,10 +540,10 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, - is_last: projects.peek().is_none() && !room.is_screen_sharing(), + is_last: projects.peek().is_none() && !room.is_sharing_screen(), }); } - if room.is_screen_sharing() { + if room.is_sharing_screen() { self.entries.push(ListEntry::ParticipantScreen { peer_id: None, is_last: true, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2771de9aac..759d33563e 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -696,7 +696,7 @@ impl App { /// Returns a list of available screen capture sources. pub fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { self.platform.screen_capture_sources() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8918fdd28b..6f227f1d07 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -85,7 +85,7 @@ pub(crate) use test::*; pub(crate) use windows::*; #[cfg(any(test, feature = "test-support"))] -pub use test::{TestDispatcher, TestScreenCaptureSource}; +pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; /// Returns a background executor for the current platform. pub fn background_executor() -> BackgroundExecutor { @@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static { false } #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver>>>; + fn screen_capture_sources(&self) + -> oneshot::Receiver>>>; #[cfg(not(feature = "screen-capture"))] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); sources_tx .send(Err(anyhow::anyhow!( @@ -293,10 +292,23 @@ pub trait PlatformDisplay: Send + Sync + Debug { } } +/// Metadata for a given [ScreenCaptureSource] +#[derive(Clone)] +pub struct SourceMetadata { + /// Opaque identifier of this screen. + pub id: u64, + /// Human-readable label for this source. + pub label: Option, + /// Whether this source is the main display. + pub is_main: Option, + /// Video resolution of this source. + pub resolution: Size, +} + /// A source of on-screen video content that can be captured. pub trait ScreenCaptureSource { - /// Returns the video resolution of this source. - fn resolution(&self) -> Result>; + /// Returns metadata for this source. + fn metadata(&self) -> Result; /// Start capture video from this source, invoking the given callback /// with each frame. @@ -308,7 +320,10 @@ pub trait ScreenCaptureSource { } /// A video stream captured from a screen. -pub trait ScreenCaptureStream {} +pub trait ScreenCaptureStream { + /// Returns metadata for this source. + fn metadata(&self) -> Result; +} /// A frame of video captured from a screen. pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index 663a740389..da54db3710 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { let (mut tx, rx) = futures::channel::oneshot::channel(); tx.send(Err(anyhow::anyhow!( diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index c4b90ccf08..a52841e1af 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -56,7 +56,7 @@ pub trait LinuxClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>>; + ) -> oneshot::Receiver>>>; fn open_window( &self, @@ -245,7 +245,7 @@ impl Platform for P { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { self.screen_capture_sources() } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 57d1dcec04..72e4477ecf 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may // be tricky. diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 0606f619c6..d1cb7d00cc 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver>>> + ) -> futures::channel::oneshot::Receiver>>> { crate::platform::scap_screen_capture::scap_screen_sources( &self.0.borrow().common.foreground_executor, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d9bb665469..1d2146cf73 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -583,7 +583,7 @@ impl Platform for MacPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { super::screen_capture::get_sources() } diff --git a/crates/gpui/src/platform/mac/screen_capture.rs b/crates/gpui/src/platform/mac/screen_capture.rs index af5e02fc06..4d4ffa6896 100644 --- a/crates/gpui/src/platform/mac/screen_capture.rs +++ b/crates/gpui/src/platform/mac/screen_capture.rs @@ -1,5 +1,5 @@ use crate::{ - DevicePixels, ForegroundExecutor, Size, + DevicePixels, ForegroundExecutor, SharedString, SourceMetadata, platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, size, }; @@ -7,8 +7,9 @@ use anyhow::{Result, anyhow}; use block::ConcreteBlock; use cocoa::{ base::{YES, id, nil}, - foundation::NSArray, + foundation::{NSArray, NSString}, }; +use collections::HashMap; use core_foundation::base::TCFType; use core_graphics::display::{ CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, @@ -32,11 +33,13 @@ use super::NSStringExt; #[derive(Clone)] pub struct MacScreenCaptureSource { sc_display: id, + meta: Option, } pub struct MacScreenCaptureStream { sc_stream: id, sc_stream_output: id, + meta: SourceMetadata, } static mut DELEGATE_CLASS: *const Class = ptr::null(); @@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback"; const SCStreamOutputTypeScreen: NSInteger = 0; impl ScreenCaptureSource for MacScreenCaptureSource { - fn resolution(&self) -> Result> { - unsafe { + fn metadata(&self) -> Result { + let (display_id, size) = unsafe { let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; let display_mode_ref = CGDisplayCopyDisplayMode(display_id); let width = CGDisplayModeGetPixelWidth(display_mode_ref); let height = CGDisplayModeGetPixelHeight(display_mode_ref); CGDisplayModeRelease(display_mode_ref); - Ok(size( - DevicePixels(width as i32), - DevicePixels(height as i32), - )) - } + ( + display_id, + size(DevicePixels(width as i32), DevicePixels(height as i32)), + ) + }; + let (label, is_main) = self + .meta + .clone() + .map(|meta| (meta.label, meta.is_main)) + .unzip(); + + Ok(SourceMetadata { + id: display_id as u64, + label, + is_main, + resolution: size, + }) } fn stream( @@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource { Box::into_raw(Box::new(frame_callback)) as *mut c_void, ); - let resolution = self.resolution().unwrap(); - let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64]; - let _: id = msg_send![configuration, setHeight: resolution.height.0 as i64]; + let meta = self.metadata().unwrap(); + let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; + let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; let (mut tx, rx) = oneshot::channel(); @@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource { move |error: id| { let result = if error == nil { let stream = MacScreenCaptureStream { + meta: meta.clone(), sc_stream: stream, sc_stream_output: output, }; @@ -138,7 +154,11 @@ impl Drop for MacScreenCaptureSource { } } -impl ScreenCaptureStream for MacScreenCaptureStream {} +impl ScreenCaptureStream for MacScreenCaptureStream { + fn metadata(&self) -> Result { + Ok(self.meta.clone()) + } +} impl Drop for MacScreenCaptureStream { fn drop(&mut self) { @@ -164,24 +184,74 @@ impl Drop for MacScreenCaptureStream { } } -pub(crate) fn get_sources() -> oneshot::Receiver>>> { +#[derive(Clone)] +struct ScreenMeta { + label: SharedString, + // Is this the screen with menu bar? + is_main: bool, +} + +unsafe fn screen_id_to_human_label() -> HashMap { + let screens: id = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut map = HashMap::default(); + let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") }; + for i in 0..count { + let screen: id = msg_send![screens, objectAtIndex: i]; + let device_desc: id = msg_send![screen, deviceDescription]; + if device_desc == nil { + continue; + } + + let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; + if nsnumber == nil { + continue; + } + + let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; + + let name: id = msg_send![screen, localizedName]; + if name != nil { + let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let rust_str = unsafe { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + }; + map.insert( + screen_id, + ScreenMeta { + label: rust_str.into(), + is_main: i == 0, + }, + ); + } + } + map +} + +pub(crate) fn get_sources() -> oneshot::Receiver>>> { unsafe { let (mut tx, rx) = oneshot::channel(); let tx = Rc::new(RefCell::new(Some(tx))); - + let screen_id_to_label = screen_id_to_human_label(); let block = ConcreteBlock::new(move |shareable_content: id, error: id| { let Some(mut tx) = tx.borrow_mut().take() else { return; }; + let result = if error == nil { let displays: id = msg_send![shareable_content, displays]; let mut result = Vec::new(); for i in 0..displays.count() { let display = displays.objectAtIndex(i); + let id: CGDirectDisplayID = msg_send![display, displayID]; + let meta = screen_id_to_label.get(&id).cloned(); let source = MacScreenCaptureSource { sc_display: msg_send![display, retain], + meta, }; - result.push(Box::new(source) as Box); + result.push(Rc::new(source) as Rc); } Ok(result) } else { diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index c5e2267a37..32041b655f 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -1,10 +1,12 @@ //! Screen capture for Linux and Windows use crate::{ DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, size, + Size, SourceMetadata, size, }; use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; +use scap::Target; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{self, AtomicBool}; @@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool}; #[allow(dead_code)] pub(crate) fn scap_screen_sources( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); get_screen_targets(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) @@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources( #[allow(dead_code)] pub(crate) fn start_scap_default_target_source( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (sources_tx, sources_rx) = oneshot::channel(); start_default_target_screen_capture(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) } struct ScapCaptureSource { - target: scap::Target, + target: scap::Display, size: Size, } @@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender> } }; let sources = targets - .iter() + .into_iter() .filter_map(|target| match target { scap::Target::Display(display) => { let size = Size { @@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender> height: DevicePixels(display.height as i32), }; Some(ScapCaptureSource { - target: target.clone(), + target: display, size, }) } @@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender> } impl ScreenCaptureSource for ScapCaptureSource { - fn resolution(&self) -> Result> { - Ok(self.size) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.target.title.clone().into()), + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource { let target = self.target.clone(); // Due to use of blocking APIs, a dedicated thread is used. - std::thread::spawn(move || match new_scap_capturer(Some(target)) { - Ok(mut capturer) => { - capturer.start_capture(); - run_capture(capturer, frame_callback, stream_tx); - } - Err(e) => { - stream_tx.send(Err(e)).ok(); + std::thread::spawn(move || { + match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { + Ok(mut capturer) => { + capturer.start_capture(); + run_capture(capturer, target.clone(), frame_callback, stream_tx); + } + Err(e) => { + stream_tx.send(Err(e)).ok(); + } } }); @@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource { // Callback for frames. Box, )>, + target: scap::Display, size: Size, } @@ -123,33 +133,48 @@ fn start_default_target_screen_capture( .get_next_frame() .context("Failed to get first frame of screenshare to get the size.")?; let size = frame_size(&first_frame); - Ok((capturer, size)) + let target = capturer + .target() + .context("Unable to determine the target display.")?; + let target = target.clone(); + Ok((capturer, size, target)) }); match start_result { - Err(e) => { - sources_tx.send(Err(e)).ok(); - } - Ok((capturer, size)) => { + Ok((capturer, size, Target::Display(display))) => { let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); sources_tx .send(Ok(vec![ScapDefaultTargetCaptureSource { stream_call_tx, size, + target: display.clone(), }])) .ok(); let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { return; }; - run_capture(capturer, frame_callback, stream_tx); + run_capture(capturer, display, frame_callback, stream_tx); + } + Err(e) => { + sources_tx.send(Err(e)).ok(); + } + _ => { + sources_tx + .send(Err(anyhow!("The screen capture source is not a display"))) + .ok(); } } }); } impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { - fn resolution(&self) -> Result> { - Ok(self.size) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: None, + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option) -> Result, stream_tx: oneshot::Sender>, ) { let cancel_stream = Arc::new(AtomicBool::new(false)); + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; let stream_send_result = stream_tx.send(Ok(ScapStream { cancel_stream: cancel_stream.clone(), + display, + size, })); if let Err(_) = stream_send_result { return; @@ -213,9 +245,20 @@ fn run_capture( struct ScapStream { cancel_stream: Arc, + display: scap::Display, + size: Size, } -impl ScreenCaptureStream for ScapStream {} +impl ScreenCaptureStream for ScapStream { + fn metadata(&self) -> Result { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.display.title.clone().into()), + is_main: None, + id: self.display.id as u64, + }) + } +} impl Drop for ScapStream { fn drop(&mut self) { @@ -237,12 +280,12 @@ fn frame_size(frame: &scap::frame::Frame) -> Size { } /// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their -/// results into `Box`. They need to `Send` their capture source, and so -/// the capture source structs are used as `Box` is not `Send`. +/// results into `Rc`. They need to `Send` their capture source, and so +/// the capture source structs are used as `Rc` is not `Send`. fn to_dyn_screen_capture_sources( sources_rx: oneshot::Receiver>>, foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver>>> { +) -> oneshot::Receiver>>> { let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); foreground_executor .spawn(async move { @@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources( Ok(Ok(results)) => dyn_sources_tx .send(Ok(results .into_iter() - .map(|source| Box::new(source) as Box) + .map(|source| Rc::new(source) as Rc) .collect::>())) .ok(), Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e4173b7c6b..9227df5b63 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -8,4 +8,4 @@ pub(crate) use display::*; pub(crate) use platform::*; pub(crate) use window::*; -pub use platform::TestScreenCaptureSource; +pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index bef05399e5..a26b65576c 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -2,7 +2,7 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -44,11 +44,17 @@ pub(crate) struct TestPlatform { /// A fake screen capture source, used for testing. pub struct TestScreenCaptureSource {} +/// A fake screen capture stream, used for testing. pub struct TestScreenCaptureStream {} impl ScreenCaptureSource for TestScreenCaptureSource { - fn resolution(&self) -> Result> { - Ok(size(DevicePixels(1), DevicePixels(1))) + fn metadata(&self) -> Result { + Ok(SourceMetadata { + id: 0, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) } fn stream( @@ -64,7 +70,11 @@ impl ScreenCaptureSource for TestScreenCaptureSource { } } -impl ScreenCaptureStream for TestScreenCaptureStream {} +impl ScreenCaptureStream for TestScreenCaptureStream { + fn metadata(&self) -> Result { + TestScreenCaptureSource {}.metadata() + } +} struct TestPrompt { msg: String, @@ -271,13 +281,13 @@ impl Platform for TestPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { let (mut tx, rx) = oneshot::channel(); tx.send(Ok(self .screen_capture_sources .borrow() .iter() - .map(|source| Box::new(source.clone()) as Box) + .map(|source| Rc::new(source.clone()) as Rc) .collect())) .ok(); rx diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index f69a802da0..401ecdeffe 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -440,7 +440,7 @@ impl Platform for WindowsPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver>>> { + ) -> oneshot::Receiver>>> { crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor) } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index 7e36314c12..c62b8853b4 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track( capture_source: &dyn ScreenCaptureSource, cx: &mut gpui::AsyncApp, ) -> Result<(crate::LocalVideoTrack, Box)> { - let resolution = capture_source.resolution()?; + let metadata = capture_source.metadata()?; let track_source = gpui_tokio::Tokio::spawn(cx, async move { NativeVideoSource::new(VideoResolution { - width: resolution.width.0 as u32, - height: resolution.height.0 as u32, + width: metadata.resolution.width.0 as u32, + height: metadata.resolution.height.0 as u32, }) })? .await?; diff --git a/crates/livekit_client/src/mock_client/participant.rs b/crates/livekit_client/src/mock_client/participant.rs index 1f4168b8e0..991d10bd50 100644 --- a/crates/livekit_client/src/mock_client/participant.rs +++ b/crates/livekit_client/src/mock_client/participant.rs @@ -5,7 +5,7 @@ use crate::{ }; use anyhow::Result; use collections::HashMap; -use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream}; +use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream}; #[derive(Clone, Debug)] pub struct LocalParticipant { @@ -119,7 +119,3 @@ impl RemoteParticipant { self.identity.clone() } } - -struct TestScreenCaptureStream; - -impl gpui::ScreenCaptureStream for TestScreenCaptureStream {} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 3c39e6b946..8e95c6f79f 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -27,6 +27,7 @@ test-support = [ ] [dependencies] +anyhow.workspace = true auto_update.workspace = true call.workspace = true chrono.workspace = true diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b2a37a4f1c..1eebc0de0c 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -1,12 +1,20 @@ +use std::rc::Rc; use std::sync::Arc; use call::{ActiveCall, ParticipantLocation, Room}; use client::{User, proto::PeerId}; -use gpui::{AnyElement, Hsla, IntoElement, MouseButton, Path, Styled, canvas, point}; +use gpui::{ + AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity, + canvas, point, +}; use gpui::{App, Task, Window, actions}; use rpc::proto::{self}; use theme::ActiveTheme; -use ui::{Avatar, AvatarAudioStatusIndicator, Facepile, TintColor, Tooltip, prelude::*}; +use ui::{ + Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Facepile, PopoverMenu, + SplitButton, TintColor, Tooltip, prelude::*, +}; +use util::maybe; use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; @@ -23,24 +31,49 @@ actions!( ] ); -fn toggle_screen_sharing(_: &ToggleScreenSharing, window: &mut Window, cx: &mut App) { +fn toggle_screen_sharing( + screen: Option>, + window: &mut Window, + cx: &mut App, +) { let call = ActiveCall::global(cx).read(cx); if let Some(room) = call.room().cloned() { let toggle_screen_sharing = room.update(cx, |room, cx| { - if room.is_screen_sharing() { + let clicked_on_currently_shared_screen = + room.shared_screen_id().is_some_and(|screen_id| { + Some(screen_id) + == screen + .as_deref() + .and_then(|s| s.metadata().ok().map(|meta| meta.id)) + }); + let should_unshare_current_screen = room.is_sharing_screen(); + let unshared_current_screen = should_unshare_current_screen.then(|| { telemetry::event!( "Screen Share Disabled", room_id = room.id(), channel_id = room.channel_id(), ); - Task::ready(room.unshare_screen(cx)) + room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) + }); + if let Some(screen) = screen { + if !should_unshare_current_screen { + telemetry::event!( + "Screen Share Enabled", + room_id = room.id(), + channel_id = room.channel_id(), + ); + } + cx.spawn(async move |room, cx| { + unshared_current_screen.transpose()?; + if !clicked_on_currently_shared_screen { + room.update(cx, |room, cx| room.share_screen(screen, cx))? + .await + } else { + Ok(()) + } + }) } else { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.share_screen(cx) + Task::ready(Ok(())) } }); toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); @@ -303,7 +336,7 @@ impl TitleBar { let is_muted = room.is_muted(); let muted_by_user = room.muted_by_user(); let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_screen_sharing(); + let is_screen_sharing = room.is_sharing_screen(); let can_use_microphone = room.can_use_microphone(); let can_share_projects = room.can_share_projects(); let screen_sharing_supported = cx.is_screen_capture_supported(); @@ -428,21 +461,43 @@ impl TitleBar { ); if can_use_microphone && screen_sharing_supported { + let trigger = IconButton::new("screen-share", ui::IconName::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .toggle_state(is_screen_sharing) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .tooltip(Tooltip::text(if is_screen_sharing { + "Stop Sharing Screen" + } else { + "Share Screen" + })) + .on_click(move |_, window, cx| { + let should_share = ActiveCall::global(cx) + .read(cx) + .room() + .is_some_and(|room| !room.read(cx).is_sharing_screen()); + + window + .spawn(cx, async move |cx| { + let screen = if should_share { + cx.update(|_, cx| pick_default_screen(cx))?.await + } else { + None + }; + + cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; + + Result::<_, anyhow::Error>::Ok(()) + }) + .detach(); + }); + children.push( - IconButton::new("screen-share", ui::IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - toggle_screen_sharing(&Default::default(), window, cx) - }) - .into_any_element(), + SplitButton::new( + trigger.render(window, cx), + self.render_screen_list().into_any_element(), + ) + .into_any_element(), ); } @@ -450,4 +505,89 @@ impl TitleBar { children } + + fn render_screen_list(&self) -> impl IntoElement { + PopoverMenu::new("screen-share-screen-list") + .with_handle(self.screen_share_popover_handle.clone()) + .trigger( + ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::None) + .child( + div() + .px_1() + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + ) + .toggle_state(self.screen_share_popover_handle.is_deployed()), + ) + .menu(|window, cx| { + let screens = cx.screen_capture_sources(); + Some(ContextMenu::build(window, cx, |context_menu, _, cx| { + cx.spawn(async move |this: WeakEntity, cx| { + let screens = screens.await??; + this.update(cx, |this, cx| { + let active_screenshare_id = ActiveCall::global(cx) + .read(cx) + .room() + .and_then(|room| room.read(cx).shared_screen_id()); + for screen in screens { + let Ok(meta) = screen.metadata() else { + continue; + }; + + let label = meta + .label + .clone() + .unwrap_or_else(|| SharedString::from("Unknown screen")); + let resolution = SharedString::from(format!( + "{} × {}", + meta.resolution.width.0, meta.resolution.height.0 + )); + this.push_item(ContextMenuItem::CustomEntry { + entry_render: Box::new(move |_, _| { + h_flex() + .gap_2() + .child(Icon::new(IconName::Screen).when( + active_screenshare_id == Some(meta.id), + |this| this.color(Color::Accent), + )) + .child(Label::new(label.clone())) + .child( + Label::new(resolution.clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .into_any() + }), + selectable: true, + documentation_aside: None, + handler: Rc::new(move |_, window, cx| { + toggle_screen_sharing(Some(screen.clone()), window, cx); + }), + }); + } + }) + }) + .detach_and_log_err(cx); + context_menu + })) + }) + } +} + +/// Picks the screen to share when clicking on the main screen sharing button. +fn pick_default_screen(cx: &App) -> Task>> { + let source = cx.screen_capture_sources(); + cx.spawn(async move |_| { + let available_sources = maybe!(async move { source.await? }).await.ok()?; + available_sources + .iter() + .find(|it| { + it.as_ref() + .metadata() + .is_ok_and(|meta| meta.is_main.unwrap_or_default()) + }) + .or_else(|| available_sources.iter().next()) + .cloned() + }) } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c4fdb16f4f..17c4c85b6d 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -36,7 +36,7 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, - IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*, + IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*, }; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt}; @@ -131,6 +131,7 @@ pub struct TitleBar { application_menu: Option>, _subscriptions: Vec, banner: Entity, + screen_share_popover_handle: PopoverMenuHandle, } impl Render for TitleBar { @@ -295,6 +296,7 @@ impl TitleBar { client, _subscriptions: subscriptions, banner, + screen_share_popover_handle: Default::default(), } } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 050db6addd..e5d13e09cd 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -178,7 +178,8 @@ impl VisibleOnHover for IconButton { } impl RenderOnce for IconButton { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + #[allow(refining_impl_trait)] + fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike { let is_disabled = self.base.disabled; let is_selected = self.base.selected; let selected_style = self.base.selected_style; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 467dd226fb..77468fd295 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -139,6 +139,8 @@ impl ContextMenuEntry { } } +impl FluentBuilder for ContextMenuEntry {} + impl From for ContextMenuItem { fn from(entry: ContextMenuEntry) -> Self { ContextMenuItem::Entry(entry) @@ -353,6 +355,10 @@ impl ContextMenu { self } + pub fn push_item(&mut self, item: impl Into) { + self.items.push(item.into()); + } + pub fn entry( mut self, label: impl Into,