Detailed changes
@@ -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]
@@ -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",
@@ -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" }
@@ -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<u64> {
+ 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<Self>) -> Task<Result<()>> {
+ pub fn share_screen(
+ &mut self,
+ source: Rc<dyn ScreenCaptureSource>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
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<Self>) -> Result<()> {
+ pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context<Self>) -> 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<livekit::Room>,
- screen_track: LocalTrack,
- microphone_track: LocalTrack,
+ screen_track: LocalTrack<dyn ScreenCaptureStream>,
+ microphone_track: LocalTrack<AudioStream>,
/// 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<Stream: ?Sized> {
None,
Pending {
publish_id: usize,
},
Published {
track_publication: LocalTrackPublication,
- _stream: Box<dyn Any>,
+ _stream: Box<Stream>,
},
}
-impl Default for LocalTrack {
+impl<T: ?Sized> Default for LocalTrack<T> {
fn default() -> Self {
Self::None
}
@@ -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();
@@ -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();
@@ -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,
@@ -696,7 +696,7 @@ impl App {
/// Returns a list of available screen capture sources.
pub fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
self.platform.screen_capture_sources()
}
@@ -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<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
+ fn screen_capture_sources(&self)
+ -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
#[cfg(not(feature = "screen-capture"))]
fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
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<SharedString>,
+ /// Whether this source is the main display.
+ pub is_main: Option<bool>,
+ /// Video resolution of this source.
+ pub resolution: Size<DevicePixels>,
+}
+
/// 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<Size<DevicePixels>>;
+ /// Returns metadata for this source.
+ fn metadata(&self) -> Result<SourceMetadata>;
/// 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<SourceMetadata>;
+}
/// A frame of video captured from a screen.
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
@@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
+ ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
let (mut tx, rx) = futures::channel::oneshot::channel();
tx.send(Err(anyhow::anyhow!(
@@ -56,7 +56,7 @@ pub trait LinuxClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
fn open_window(
&self,
@@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
self.screen_capture_sources()
}
@@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
+ ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
// be tricky.
@@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>
+ ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>
{
crate::platform::scap_screen_capture::scap_screen_sources(
&self.0.borrow().common.foreground_executor,
@@ -583,7 +583,7 @@ impl Platform for MacPlatform {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
super::screen_capture::get_sources()
}
@@ -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<ScreenMeta>,
}
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<Size<DevicePixels>> {
- unsafe {
+ fn metadata(&self) -> Result<SourceMetadata> {
+ 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<SourceMetadata> {
+ 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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+#[derive(Clone)]
+struct ScreenMeta {
+ label: SharedString,
+ // Is this the screen with menu bar?
+ is_main: bool,
+}
+
+unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> {
+ 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<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
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<dyn ScreenCaptureSource>);
+ result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>);
}
Ok(result)
} else {
@@ -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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
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<DevicePixels>,
}
@@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
}
};
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<Result<Vec<ScapCaptureSource>>
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<Result<Vec<ScapCaptureSource>>
}
impl ScreenCaptureSource for ScapCaptureSource {
- fn resolution(&self) -> Result<Size<DevicePixels>> {
- Ok(self.size)
+ fn metadata(&self) -> Result<SourceMetadata> {
+ 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<dyn Fn(ScreenCaptureFrame) + Send>,
)>,
+ target: scap::Display,
size: Size<DevicePixels>,
}
@@ -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<Size<DevicePixels>> {
- Ok(self.size)
+ fn metadata(&self) -> Result<SourceMetadata> {
+ 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<scap::Target>) -> Result<scap::capturer::Cap
fn run_capture(
mut capturer: scap::capturer::Capturer,
+ display: scap::Display,
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
stream_tx: oneshot::Sender<Result<ScapStream>>,
) {
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<AtomicBool>,
+ display: scap::Display,
+ size: Size<DevicePixels>,
}
-impl ScreenCaptureStream for ScapStream {}
+impl ScreenCaptureStream for ScapStream {
+ fn metadata(&self) -> Result<SourceMetadata> {
+ 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<DevicePixels> {
}
/// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their
-/// results into `Box<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
-/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`.
+/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
+/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
foreground_executor: &ForegroundExecutor,
-) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
foreground_executor
.spawn(async move {
@@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
Ok(Ok(results)) => dyn_sources_tx
.send(Ok(results
.into_iter()
- .map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>)
+ .map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
.collect::<Vec<_>>()))
.ok(),
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(),
@@ -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};
@@ -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<Size<DevicePixels>> {
- Ok(size(DevicePixels(1), DevicePixels(1)))
+ fn metadata(&self) -> Result<SourceMetadata> {
+ 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<SourceMetadata> {
+ 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<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
let (mut tx, rx) = oneshot::channel();
tx.send(Ok(self
.screen_capture_sources
.borrow()
.iter()
- .map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
+ .map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
.collect()))
.ok();
rx
@@ -440,7 +440,7 @@ impl Platform for WindowsPlatform {
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
- ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
+ ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor)
}
@@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track(
capture_source: &dyn ScreenCaptureSource,
cx: &mut gpui::AsyncApp,
) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
- 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?;
@@ -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 {}
@@ -27,6 +27,7 @@ test-support = [
]
[dependencies]
+anyhow.workspace = true
auto_update.workspace = true
call.workspace = true
chrono.workspace = true
@@ -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<Rc<dyn ScreenCaptureSource>>,
+ 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<ContextMenu>, 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<Option<Rc<dyn ScreenCaptureSource>>> {
+ 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()
+ })
}
@@ -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<Entity<ApplicationMenu>>,
_subscriptions: Vec<Subscription>,
banner: Entity<OnboardingBanner>,
+ screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
}
impl Render for TitleBar {
@@ -295,6 +296,7 @@ impl TitleBar {
client,
_subscriptions: subscriptions,
banner,
+ screen_share_popover_handle: Default::default(),
}
}
@@ -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;
@@ -139,6 +139,8 @@ impl ContextMenuEntry {
}
}
+impl FluentBuilder for ContextMenuEntry {}
+
impl From<ContextMenuEntry> 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<ContextMenuItem>) {
+ self.items.push(item.into());
+ }
+
pub fn entry(
mut self,
label: impl Into<SharedString>,