collab: Add screen selector (#31506)
Instead of selecting a screen to share arbitrarily, we'll now allow user to select the screen to share. Note that sharing multiple screens at the time is still not supported (though prolly not too far-fetched). Related to #4666  Release Notes: - Added screen selector dropdown to screen share button --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
57ab09c2da
commit
88af35fe47
26 changed files with 473 additions and 145 deletions
|
@ -24,7 +24,7 @@ workspace-members = [
|
||||||
third-party = [
|
third-party = [
|
||||||
{ name = "reqwest", version = "0.11.27" },
|
{ name = "reqwest", version = "0.11.27" },
|
||||||
# build of remote_server should not include scap / its x11 dependency
|
# 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]
|
[final-excludes]
|
||||||
|
|
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -14185,7 +14185,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scap"
|
name = "scap"
|
||||||
version = "0.0.8"
|
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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cocoa 0.25.0",
|
"cocoa 0.25.0",
|
||||||
|
@ -16484,6 +16484,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
name = "title_bar"
|
name = "title_bar"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
"call",
|
"call",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -18729,8 +18730,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-capture"
|
name = "windows-capture"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9"
|
||||||
checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
|
|
|
@ -553,8 +553,7 @@ rustc-demangle = "0.1.23"
|
||||||
rustc-hash = "2.1.0"
|
rustc-hash = "2.1.0"
|
||||||
rustls = { version = "0.23.26" }
|
rustls = { version = "0.23.26" }
|
||||||
rustls-platform-verifier = "0.5.0"
|
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 = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||||
scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false }
|
|
||||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||||
semver = "1.0"
|
semver = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
@ -708,6 +707,7 @@ features = [
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||||
notify-types = { 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
|
# Makes the workspace hack crate refer to the local one, but only when you're building locally
|
||||||
workspace-hack = { path = "tooling/workspace-hack" }
|
workspace-hack = { path = "tooling/workspace-hack" }
|
||||||
|
|
|
@ -11,15 +11,18 @@ use client::{
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::{FutureExt, StreamExt};
|
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 gpui_tokio::Tokio;
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent};
|
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 postage::{sink::Sink, stream::Stream, watch};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use settings::Settings as _;
|
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};
|
use util::{ResultExt, TryFutureExt, post_inc};
|
||||||
|
|
||||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
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| {
|
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||||
!matches!(live_kit.screen_track, LocalTrack::None)
|
!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 {
|
pub fn is_sharing_mic(&self) -> bool {
|
||||||
self.live_kit.as_ref().map_or(false, |live_kit| {
|
self.live_kit.as_ref().map_or(false, |live_kit| {
|
||||||
!matches!(live_kit.microphone_track, LocalTrack::None)
|
!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() {
|
if self.status.is_offline() {
|
||||||
return Task::ready(Err(anyhow!("room 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")));
|
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")));
|
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||||
};
|
};
|
||||||
|
|
||||||
let sources = cx.screen_capture_sources();
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let sources = sources
|
let publication = participant.publish_screenshare_track(&*source, cx).await;
|
||||||
.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),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let live_kit = this
|
let live_kit = this
|
||||||
|
@ -1426,7 +1430,7 @@ impl Room {
|
||||||
} else {
|
} else {
|
||||||
live_kit.screen_track = LocalTrack::Published {
|
live_kit.screen_track = LocalTrack::Published {
|
||||||
track_publication: publication,
|
track_publication: publication,
|
||||||
_stream: Box::new(stream),
|
_stream: stream,
|
||||||
};
|
};
|
||||||
cx.notify();
|
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");
|
anyhow::ensure!(!self.status.is_offline(), "room is offline");
|
||||||
|
|
||||||
let live_kit = self
|
let live_kit = self
|
||||||
|
@ -1516,7 +1520,10 @@ impl Room {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
Audio::play_sound(Sound::StopScreenshare, cx);
|
if play_sound {
|
||||||
|
Audio::play_sound(Sound::StopScreenshare, cx);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1624,8 +1631,8 @@ fn spawn_room_connection(
|
||||||
|
|
||||||
struct LiveKitRoom {
|
struct LiveKitRoom {
|
||||||
room: Rc<livekit::Room>,
|
room: Rc<livekit::Room>,
|
||||||
screen_track: LocalTrack,
|
screen_track: LocalTrack<dyn ScreenCaptureStream>,
|
||||||
microphone_track: LocalTrack,
|
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.
|
/// 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,
|
muted_by_user: bool,
|
||||||
deafened: bool,
|
deafened: bool,
|
||||||
|
@ -1663,18 +1670,18 @@ impl LiveKitRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LocalTrack {
|
enum LocalTrack<Stream: ?Sized> {
|
||||||
None,
|
None,
|
||||||
Pending {
|
Pending {
|
||||||
publish_id: usize,
|
publish_id: usize,
|
||||||
},
|
},
|
||||||
Published {
|
Published {
|
||||||
track_publication: LocalTrackPublication,
|
track_publication: LocalTrackPublication,
|
||||||
_stream: Box<dyn Any>,
|
_stream: Box<Stream>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LocalTrack {
|
impl<T: ?Sized> Default for LocalTrack<T> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::None
|
Self::None
|
||||||
}
|
}
|
||||||
|
|
|
@ -439,7 +439,7 @@ async fn test_basic_following(
|
||||||
editor_a1.item_id()
|
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 crate::rpc::RECONNECT_TIMEOUT;
|
||||||
use gpui::TestScreenCaptureSource;
|
use gpui::TestScreenCaptureSource;
|
||||||
|
@ -456,11 +456,19 @@ async fn test_basic_following(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
cx_b.set_screen_capture_sources(vec![display]);
|
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
|
active_call_b
|
||||||
.update(cx_b, |call, cx| {
|
.update(cx_b, |call, cx| {
|
||||||
call.room()
|
call.room()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.update(cx, |room, cx| room.share_screen(cx))
|
.update(cx, |room, cx| room.share_screen(source, cx))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -277,11 +277,19 @@ async fn test_basic_calls(
|
||||||
let events_b = active_call_events(cx_b);
|
let events_b = active_call_events(cx_b);
|
||||||
let events_c = active_call_events(cx_c);
|
let events_c = active_call_events(cx_c);
|
||||||
cx_a.set_screen_capture_sources(vec![display]);
|
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
|
active_call_a
|
||||||
.update(cx_a, |call, cx| {
|
.update(cx_a, |call, cx| {
|
||||||
call.room()
|
call.room()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.update(cx, |room, cx| room.share_screen(cx))
|
.update(cx, |room, cx| room.share_screen(screen_a, cx))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared(
|
||||||
// User A shares their screen
|
// User A shares their screen
|
||||||
let display = gpui::TestScreenCaptureSource::new();
|
let display = gpui::TestScreenCaptureSource::new();
|
||||||
cx_a.set_screen_capture_sources(vec![display]);
|
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
|
active_call_a
|
||||||
.update(cx_a, |call, cx| {
|
.update(cx_a, |call, cx| {
|
||||||
call.room()
|
call.room()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.update(cx, |room, cx| room.share_screen(cx))
|
.update(cx, |room, cx| room.share_screen(screen_a, cx))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -144,10 +144,22 @@ pub fn init(cx: &mut App) {
|
||||||
if let Some(room) = room {
|
if let Some(room) = room {
|
||||||
window.defer(cx, move |_window, cx| {
|
window.defer(cx, move |_window, cx| {
|
||||||
room.update(cx, |room, cx| {
|
room.update(cx, |room, cx| {
|
||||||
if room.is_screen_sharing() {
|
if room.is_sharing_screen() {
|
||||||
room.unshare_screen(cx).ok();
|
room.unshare_screen(true, cx).ok();
|
||||||
} else {
|
} 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,
|
project_id: project.id,
|
||||||
worktree_root_names: project.worktree_root_names.clone(),
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
host_user_id: user_id,
|
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 {
|
self.entries.push(ListEntry::ParticipantScreen {
|
||||||
peer_id: None,
|
peer_id: None,
|
||||||
is_last: true,
|
is_last: true,
|
||||||
|
|
|
@ -696,7 +696,7 @@ impl App {
|
||||||
/// Returns a list of available screen capture sources.
|
/// Returns a list of available screen capture sources.
|
||||||
pub fn screen_capture_sources(
|
pub fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||||
self.platform.screen_capture_sources()
|
self.platform.screen_capture_sources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ pub(crate) use test::*;
|
||||||
pub(crate) use windows::*;
|
pub(crate) use windows::*;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[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.
|
/// Returns a background executor for the current platform.
|
||||||
pub fn background_executor() -> BackgroundExecutor {
|
pub fn background_executor() -> BackgroundExecutor {
|
||||||
|
@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(&self)
|
||||||
&self,
|
-> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>;
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>;
|
|
||||||
#[cfg(not(feature = "screen-capture"))]
|
#[cfg(not(feature = "screen-capture"))]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||||
let (sources_tx, sources_rx) = oneshot::channel();
|
let (sources_tx, sources_rx) = oneshot::channel();
|
||||||
sources_tx
|
sources_tx
|
||||||
.send(Err(anyhow::anyhow!(
|
.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.
|
/// A source of on-screen video content that can be captured.
|
||||||
pub trait ScreenCaptureSource {
|
pub trait ScreenCaptureSource {
|
||||||
/// Returns the video resolution of this source.
|
/// Returns metadata for this source.
|
||||||
fn resolution(&self) -> Result<Size<DevicePixels>>;
|
fn metadata(&self) -> Result<SourceMetadata>;
|
||||||
|
|
||||||
/// Start capture video from this source, invoking the given callback
|
/// Start capture video from this source, invoking the given callback
|
||||||
/// with each frame.
|
/// with each frame.
|
||||||
|
@ -308,7 +320,10 @@ pub trait ScreenCaptureSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A video stream captured from a screen.
|
/// 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.
|
/// A frame of video captured from a screen.
|
||||||
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
|
||||||
|
|
|
@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&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();
|
let (mut tx, rx) = futures::channel::oneshot::channel();
|
||||||
tx.send(Err(anyhow::anyhow!(
|
tx.send(Err(anyhow::anyhow!(
|
||||||
|
|
|
@ -56,7 +56,7 @@ pub trait LinuxClient {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>;
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>;
|
||||||
|
|
||||||
fn open_window(
|
fn open_window(
|
||||||
&self,
|
&self,
|
||||||
|
@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||||
self.screen_capture_sources()
|
self.screen_capture_sources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&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
|
// TODO: Get screen capture working on wayland. Be sure to try window resizing as that may
|
||||||
// be tricky.
|
// be tricky.
|
||||||
|
|
|
@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&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(
|
crate::platform::scap_screen_capture::scap_screen_sources(
|
||||||
&self.0.borrow().common.foreground_executor,
|
&self.0.borrow().common.foreground_executor,
|
||||||
|
|
|
@ -583,7 +583,7 @@ impl Platform for MacPlatform {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> {
|
||||||
super::screen_capture::get_sources()
|
super::screen_capture::get_sources()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
DevicePixels, ForegroundExecutor, Size,
|
DevicePixels, ForegroundExecutor, SharedString, SourceMetadata,
|
||||||
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
|
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
|
||||||
size,
|
size,
|
||||||
};
|
};
|
||||||
|
@ -7,8 +7,9 @@ use anyhow::{Result, anyhow};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
use cocoa::{
|
use cocoa::{
|
||||||
base::{YES, id, nil},
|
base::{YES, id, nil},
|
||||||
foundation::NSArray,
|
foundation::{NSArray, NSString},
|
||||||
};
|
};
|
||||||
|
use collections::HashMap;
|
||||||
use core_foundation::base::TCFType;
|
use core_foundation::base::TCFType;
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
|
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
|
||||||
|
@ -32,11 +33,13 @@ use super::NSStringExt;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MacScreenCaptureSource {
|
pub struct MacScreenCaptureSource {
|
||||||
sc_display: id,
|
sc_display: id,
|
||||||
|
meta: Option<ScreenMeta>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MacScreenCaptureStream {
|
pub struct MacScreenCaptureStream {
|
||||||
sc_stream: id,
|
sc_stream: id,
|
||||||
sc_stream_output: id,
|
sc_stream_output: id,
|
||||||
|
meta: SourceMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
static mut DELEGATE_CLASS: *const Class = ptr::null();
|
static mut DELEGATE_CLASS: *const Class = ptr::null();
|
||||||
|
@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
|
||||||
const SCStreamOutputTypeScreen: NSInteger = 0;
|
const SCStreamOutputTypeScreen: NSInteger = 0;
|
||||||
|
|
||||||
impl ScreenCaptureSource for MacScreenCaptureSource {
|
impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
fn metadata(&self) -> Result<SourceMetadata> {
|
||||||
unsafe {
|
let (display_id, size) = unsafe {
|
||||||
let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID];
|
let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID];
|
||||||
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
|
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
|
||||||
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
|
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
|
||||||
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
|
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
|
||||||
CGDisplayModeRelease(display_mode_ref);
|
CGDisplayModeRelease(display_mode_ref);
|
||||||
|
|
||||||
Ok(size(
|
(
|
||||||
DevicePixels(width as i32),
|
display_id,
|
||||||
DevicePixels(height as i32),
|
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(
|
fn stream(
|
||||||
|
@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||||
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
|
Box::into_raw(Box::new(frame_callback)) as *mut c_void,
|
||||||
);
|
);
|
||||||
|
|
||||||
let resolution = self.resolution().unwrap();
|
let meta = self.metadata().unwrap();
|
||||||
let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64];
|
let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64];
|
||||||
let _: id = msg_send![configuration, setHeight: resolution.height.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 stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate];
|
||||||
|
|
||||||
let (mut tx, rx) = oneshot::channel();
|
let (mut tx, rx) = oneshot::channel();
|
||||||
|
@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
|
||||||
move |error: id| {
|
move |error: id| {
|
||||||
let result = if error == nil {
|
let result = if error == nil {
|
||||||
let stream = MacScreenCaptureStream {
|
let stream = MacScreenCaptureStream {
|
||||||
|
meta: meta.clone(),
|
||||||
sc_stream: stream,
|
sc_stream: stream,
|
||||||
sc_stream_output: output,
|
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 {
|
impl Drop for MacScreenCaptureStream {
|
||||||
fn drop(&mut self) {
|
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 {
|
unsafe {
|
||||||
let (mut tx, rx) = oneshot::channel();
|
let (mut tx, rx) = oneshot::channel();
|
||||||
let tx = Rc::new(RefCell::new(Some(tx)));
|
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 block = ConcreteBlock::new(move |shareable_content: id, error: id| {
|
||||||
let Some(mut tx) = tx.borrow_mut().take() else {
|
let Some(mut tx) = tx.borrow_mut().take() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = if error == nil {
|
let result = if error == nil {
|
||||||
let displays: id = msg_send![shareable_content, displays];
|
let displays: id = msg_send![shareable_content, displays];
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
for i in 0..displays.count() {
|
for i in 0..displays.count() {
|
||||||
let display = displays.objectAtIndex(i);
|
let display = displays.objectAtIndex(i);
|
||||||
|
let id: CGDirectDisplayID = msg_send![display, displayID];
|
||||||
|
let meta = screen_id_to_label.get(&id).cloned();
|
||||||
let source = MacScreenCaptureSource {
|
let source = MacScreenCaptureSource {
|
||||||
sc_display: msg_send![display, retain],
|
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)
|
Ok(result)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
//! Screen capture for Linux and Windows
|
//! Screen capture for Linux and Windows
|
||||||
use crate::{
|
use crate::{
|
||||||
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||||
Size, size,
|
Size, SourceMetadata, size,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
|
use scap::Target;
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{self, AtomicBool};
|
use std::sync::atomic::{self, AtomicBool};
|
||||||
|
|
||||||
|
@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool};
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn scap_screen_sources(
|
pub(crate) fn scap_screen_sources(
|
||||||
foreground_executor: &ForegroundExecutor,
|
foreground_executor: &ForegroundExecutor,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||||
let (sources_tx, sources_rx) = oneshot::channel();
|
let (sources_tx, sources_rx) = oneshot::channel();
|
||||||
get_screen_targets(sources_tx);
|
get_screen_targets(sources_tx);
|
||||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||||
|
@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources(
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn start_scap_default_target_source(
|
pub(crate) fn start_scap_default_target_source(
|
||||||
foreground_executor: &ForegroundExecutor,
|
foreground_executor: &ForegroundExecutor,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||||
let (sources_tx, sources_rx) = oneshot::channel();
|
let (sources_tx, sources_rx) = oneshot::channel();
|
||||||
start_default_target_screen_capture(sources_tx);
|
start_default_target_screen_capture(sources_tx);
|
||||||
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
to_dyn_screen_capture_sources(sources_rx, foreground_executor)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ScapCaptureSource {
|
struct ScapCaptureSource {
|
||||||
target: scap::Target,
|
target: scap::Display,
|
||||||
size: Size<DevicePixels>,
|
size: Size<DevicePixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let sources = targets
|
let sources = targets
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter_map(|target| match target {
|
.filter_map(|target| match target {
|
||||||
scap::Target::Display(display) => {
|
scap::Target::Display(display) => {
|
||||||
let size = Size {
|
let size = Size {
|
||||||
|
@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||||
height: DevicePixels(display.height as i32),
|
height: DevicePixels(display.height as i32),
|
||||||
};
|
};
|
||||||
Some(ScapCaptureSource {
|
Some(ScapCaptureSource {
|
||||||
target: target.clone(),
|
target: display,
|
||||||
size,
|
size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScreenCaptureSource for ScapCaptureSource {
|
impl ScreenCaptureSource for ScapCaptureSource {
|
||||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
fn metadata(&self) -> Result<SourceMetadata> {
|
||||||
Ok(self.size)
|
Ok(SourceMetadata {
|
||||||
|
resolution: self.size,
|
||||||
|
label: Some(self.target.title.clone().into()),
|
||||||
|
is_main: None,
|
||||||
|
id: self.target.id as u64,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream(
|
fn stream(
|
||||||
|
@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource {
|
||||||
let target = self.target.clone();
|
let target = self.target.clone();
|
||||||
|
|
||||||
// Due to use of blocking APIs, a dedicated thread is used.
|
// Due to use of blocking APIs, a dedicated thread is used.
|
||||||
std::thread::spawn(move || match new_scap_capturer(Some(target)) {
|
std::thread::spawn(move || {
|
||||||
Ok(mut capturer) => {
|
match new_scap_capturer(Some(scap::Target::Display(target.clone()))) {
|
||||||
capturer.start_capture();
|
Ok(mut capturer) => {
|
||||||
run_capture(capturer, frame_callback, stream_tx);
|
capturer.start_capture();
|
||||||
}
|
run_capture(capturer, target.clone(), frame_callback, stream_tx);
|
||||||
Err(e) => {
|
}
|
||||||
stream_tx.send(Err(e)).ok();
|
Err(e) => {
|
||||||
|
stream_tx.send(Err(e)).ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource {
|
||||||
// Callback for frames.
|
// Callback for frames.
|
||||||
Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||||
)>,
|
)>,
|
||||||
|
target: scap::Display,
|
||||||
size: Size<DevicePixels>,
|
size: Size<DevicePixels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,33 +133,48 @@ fn start_default_target_screen_capture(
|
||||||
.get_next_frame()
|
.get_next_frame()
|
||||||
.context("Failed to get first frame of screenshare to get the size.")?;
|
.context("Failed to get first frame of screenshare to get the size.")?;
|
||||||
let size = frame_size(&first_frame);
|
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 {
|
match start_result {
|
||||||
Err(e) => {
|
Ok((capturer, size, Target::Display(display))) => {
|
||||||
sources_tx.send(Err(e)).ok();
|
|
||||||
}
|
|
||||||
Ok((capturer, size)) => {
|
|
||||||
let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1);
|
let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1);
|
||||||
sources_tx
|
sources_tx
|
||||||
.send(Ok(vec![ScapDefaultTargetCaptureSource {
|
.send(Ok(vec![ScapDefaultTargetCaptureSource {
|
||||||
stream_call_tx,
|
stream_call_tx,
|
||||||
size,
|
size,
|
||||||
|
target: display.clone(),
|
||||||
}]))
|
}]))
|
||||||
.ok();
|
.ok();
|
||||||
let Ok((stream_tx, frame_callback)) = stream_rx.recv() else {
|
let Ok((stream_tx, frame_callback)) = stream_rx.recv() else {
|
||||||
return;
|
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 {
|
impl ScreenCaptureSource for ScapDefaultTargetCaptureSource {
|
||||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
fn metadata(&self) -> Result<SourceMetadata> {
|
||||||
Ok(self.size)
|
Ok(SourceMetadata {
|
||||||
|
resolution: self.size,
|
||||||
|
label: None,
|
||||||
|
is_main: None,
|
||||||
|
id: self.target.id as u64,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream(
|
fn stream(
|
||||||
|
@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option<scap::Target>) -> Result<scap::capturer::Cap
|
||||||
|
|
||||||
fn run_capture(
|
fn run_capture(
|
||||||
mut capturer: scap::capturer::Capturer,
|
mut capturer: scap::capturer::Capturer,
|
||||||
|
display: scap::Display,
|
||||||
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>,
|
||||||
stream_tx: oneshot::Sender<Result<ScapStream>>,
|
stream_tx: oneshot::Sender<Result<ScapStream>>,
|
||||||
) {
|
) {
|
||||||
let cancel_stream = Arc::new(AtomicBool::new(false));
|
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 {
|
let stream_send_result = stream_tx.send(Ok(ScapStream {
|
||||||
cancel_stream: cancel_stream.clone(),
|
cancel_stream: cancel_stream.clone(),
|
||||||
|
display,
|
||||||
|
size,
|
||||||
}));
|
}));
|
||||||
if let Err(_) = stream_send_result {
|
if let Err(_) = stream_send_result {
|
||||||
return;
|
return;
|
||||||
|
@ -213,9 +245,20 @@ fn run_capture(
|
||||||
|
|
||||||
struct ScapStream {
|
struct ScapStream {
|
||||||
cancel_stream: Arc<AtomicBool>,
|
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 {
|
impl Drop for ScapStream {
|
||||||
fn drop(&mut self) {
|
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
|
/// 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
|
/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so
|
||||||
/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`.
|
/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`.
|
||||||
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||||
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
|
sources_rx: oneshot::Receiver<Result<Vec<T>>>,
|
||||||
foreground_executor: &ForegroundExecutor,
|
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();
|
let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel();
|
||||||
foreground_executor
|
foreground_executor
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
|
@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>(
|
||||||
Ok(Ok(results)) => dyn_sources_tx
|
Ok(Ok(results)) => dyn_sources_tx
|
||||||
.send(Ok(results
|
.send(Ok(results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>)
|
.map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>)
|
||||||
.collect::<Vec<_>>()))
|
.collect::<Vec<_>>()))
|
||||||
.ok(),
|
.ok(),
|
||||||
Ok(Err(err)) => dyn_sources_tx.send(Err(err)).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 platform::*;
|
||||||
pub(crate) use window::*;
|
pub(crate) use window::*;
|
||||||
|
|
||||||
pub use platform::TestScreenCaptureSource;
|
pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream};
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
||||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||||
Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -44,11 +44,17 @@ pub(crate) struct TestPlatform {
|
||||||
/// A fake screen capture source, used for testing.
|
/// A fake screen capture source, used for testing.
|
||||||
pub struct TestScreenCaptureSource {}
|
pub struct TestScreenCaptureSource {}
|
||||||
|
|
||||||
|
/// A fake screen capture stream, used for testing.
|
||||||
pub struct TestScreenCaptureStream {}
|
pub struct TestScreenCaptureStream {}
|
||||||
|
|
||||||
impl ScreenCaptureSource for TestScreenCaptureSource {
|
impl ScreenCaptureSource for TestScreenCaptureSource {
|
||||||
fn resolution(&self) -> Result<Size<DevicePixels>> {
|
fn metadata(&self) -> Result<SourceMetadata> {
|
||||||
Ok(size(DevicePixels(1), DevicePixels(1)))
|
Ok(SourceMetadata {
|
||||||
|
id: 0,
|
||||||
|
is_main: None,
|
||||||
|
label: None,
|
||||||
|
resolution: size(DevicePixels(1), DevicePixels(1)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stream(
|
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 {
|
struct TestPrompt {
|
||||||
msg: String,
|
msg: String,
|
||||||
|
@ -271,13 +281,13 @@ impl Platform for TestPlatform {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&self,
|
||||||
) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> {
|
) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> {
|
||||||
let (mut tx, rx) = oneshot::channel();
|
let (mut tx, rx) = oneshot::channel();
|
||||||
tx.send(Ok(self
|
tx.send(Ok(self
|
||||||
.screen_capture_sources
|
.screen_capture_sources
|
||||||
.borrow()
|
.borrow()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>)
|
.map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>)
|
||||||
.collect()))
|
.collect()))
|
||||||
.ok();
|
.ok();
|
||||||
rx
|
rx
|
||||||
|
|
|
@ -440,7 +440,7 @@ impl Platform for WindowsPlatform {
|
||||||
#[cfg(feature = "screen-capture")]
|
#[cfg(feature = "screen-capture")]
|
||||||
fn screen_capture_sources(
|
fn screen_capture_sources(
|
||||||
&self,
|
&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)
|
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,
|
capture_source: &dyn ScreenCaptureSource,
|
||||||
cx: &mut gpui::AsyncApp,
|
cx: &mut gpui::AsyncApp,
|
||||||
) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
|
) -> 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 {
|
let track_source = gpui_tokio::Tokio::spawn(cx, async move {
|
||||||
NativeVideoSource::new(VideoResolution {
|
NativeVideoSource::new(VideoResolution {
|
||||||
width: resolution.width.0 as u32,
|
width: metadata.resolution.width.0 as u32,
|
||||||
height: resolution.height.0 as u32,
|
height: metadata.resolution.height.0 as u32,
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream};
|
use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct LocalParticipant {
|
pub struct LocalParticipant {
|
||||||
|
@ -119,7 +119,3 @@ impl RemoteParticipant {
|
||||||
self.identity.clone()
|
self.identity.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestScreenCaptureStream;
|
|
||||||
|
|
||||||
impl gpui::ScreenCaptureStream for TestScreenCaptureStream {}
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ test-support = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
call.workspace = true
|
call.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use call::{ActiveCall, ParticipantLocation, Room};
|
use call::{ActiveCall, ParticipantLocation, Room};
|
||||||
use client::{User, proto::PeerId};
|
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 gpui::{App, Task, Window, actions};
|
||||||
use rpc::proto::{self};
|
use rpc::proto::{self};
|
||||||
use theme::ActiveTheme;
|
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 workspace::notifications::DetachAndPromptErr;
|
||||||
|
|
||||||
use crate::TitleBar;
|
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);
|
let call = ActiveCall::global(cx).read(cx);
|
||||||
if let Some(room) = call.room().cloned() {
|
if let Some(room) = call.room().cloned() {
|
||||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
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!(
|
telemetry::event!(
|
||||||
"Screen Share Disabled",
|
"Screen Share Disabled",
|
||||||
room_id = room.id(),
|
room_id = room.id(),
|
||||||
channel_id = room.channel_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 {
|
} else {
|
||||||
telemetry::event!(
|
Task::ready(Ok(()))
|
||||||
"Screen Share Enabled",
|
|
||||||
room_id = room.id(),
|
|
||||||
channel_id = room.channel_id(),
|
|
||||||
);
|
|
||||||
room.share_screen(cx)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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)));
|
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 is_muted = room.is_muted();
|
||||||
let muted_by_user = room.muted_by_user();
|
let muted_by_user = room.muted_by_user();
|
||||||
let is_deafened = room.is_deafened().unwrap_or(false);
|
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_use_microphone = room.can_use_microphone();
|
||||||
let can_share_projects = room.can_share_projects();
|
let can_share_projects = room.can_share_projects();
|
||||||
let screen_sharing_supported = cx.is_screen_capture_supported();
|
let screen_sharing_supported = cx.is_screen_capture_supported();
|
||||||
|
@ -428,21 +461,43 @@ impl TitleBar {
|
||||||
);
|
);
|
||||||
|
|
||||||
if can_use_microphone && screen_sharing_supported {
|
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(
|
children.push(
|
||||||
IconButton::new("screen-share", ui::IconName::Screen)
|
SplitButton::new(
|
||||||
.style(ButtonStyle::Subtle)
|
trigger.render(window, cx),
|
||||||
.icon_size(IconSize::Small)
|
self.render_screen_list().into_any_element(),
|
||||||
.toggle_state(is_screen_sharing)
|
)
|
||||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
.into_any_element(),
|
||||||
.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(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -450,4 +505,89 @@ impl TitleBar {
|
||||||
|
|
||||||
children
|
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 title_bar_settings::TitleBarSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
|
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 util::ResultExt;
|
||||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||||
|
@ -131,6 +131,7 @@ pub struct TitleBar {
|
||||||
application_menu: Option<Entity<ApplicationMenu>>,
|
application_menu: Option<Entity<ApplicationMenu>>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
banner: Entity<OnboardingBanner>,
|
banner: Entity<OnboardingBanner>,
|
||||||
|
screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for TitleBar {
|
impl Render for TitleBar {
|
||||||
|
@ -295,6 +296,7 @@ impl TitleBar {
|
||||||
client,
|
client,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
banner,
|
banner,
|
||||||
|
screen_share_popover_handle: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -178,7 +178,8 @@ impl VisibleOnHover for IconButton {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce 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_disabled = self.base.disabled;
|
||||||
let is_selected = self.base.selected;
|
let is_selected = self.base.selected;
|
||||||
let selected_style = self.base.selected_style;
|
let selected_style = self.base.selected_style;
|
||||||
|
|
|
@ -139,6 +139,8 @@ impl ContextMenuEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl FluentBuilder for ContextMenuEntry {}
|
||||||
|
|
||||||
impl From<ContextMenuEntry> for ContextMenuItem {
|
impl From<ContextMenuEntry> for ContextMenuItem {
|
||||||
fn from(entry: ContextMenuEntry) -> Self {
|
fn from(entry: ContextMenuEntry) -> Self {
|
||||||
ContextMenuItem::Entry(entry)
|
ContextMenuItem::Entry(entry)
|
||||||
|
@ -353,6 +355,10 @@ impl ContextMenu {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push_item(&mut self, item: impl Into<ContextMenuItem>) {
|
||||||
|
self.items.push(item.into());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entry(
|
pub fn entry(
|
||||||
mut self,
|
mut self,
|
||||||
label: impl Into<SharedString>,
|
label: impl Into<SharedString>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue