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

![image](https://github.com/user-attachments/assets/1afb664f-3cdb-4e0a-bb29-9d7093d87fa5)

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:
Piotr Osiewicz 2025-07-21 13:44:51 +02:00 committed by GitHub
parent 57ab09c2da
commit 88af35fe47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 473 additions and 145 deletions

View file

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