mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
Add screen-sharing support on Wayland/Linux (#51957)
Release Notes: - Added screen-sharing support on Wayland/Linux. --------- Co-authored-by: Neel Chotai <neel@zed.dev>
This commit is contained in:
parent
55a59ca17d
commit
01fe4f694e
12 changed files with 606 additions and 37 deletions
|
|
@ -19,7 +19,8 @@ test-support = [
|
|||
"gpui/test-support",
|
||||
"livekit_client/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
"util/test-support",
|
||||
"workspace/test-support"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -51,5 +52,6 @@ gpui = { workspace = true, features = ["test-support"] }
|
|||
language = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
livekit_client = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -1529,6 +1529,84 @@ impl Room {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn share_screen_wayland(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
log::info!("will screenshare on wayland");
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
if self.is_sharing_screen() {
|
||||
return Task::ready(Err(anyhow!("screen was already shared")));
|
||||
}
|
||||
|
||||
let (participant, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
let publish_id = post_inc(&mut live_kit.next_publish_id);
|
||||
live_kit.screen_track = LocalTrack::Pending { publish_id };
|
||||
cx.notify();
|
||||
(live_kit.room.local_participant(), publish_id)
|
||||
} else {
|
||||
return Task::ready(Err(anyhow!("live-kit was not initialized")));
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let publication = participant.publish_screenshare_track_wayland(cx).await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let live_kit = this
|
||||
.live_kit
|
||||
.as_mut()
|
||||
.context("live-kit was not initialized")?;
|
||||
|
||||
let canceled = if let LocalTrack::Pending {
|
||||
publish_id: cur_publish_id,
|
||||
} = &live_kit.screen_track
|
||||
{
|
||||
*cur_publish_id != publish_id
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
match publication {
|
||||
Ok((publication, stream, failure_rx)) => {
|
||||
if canceled {
|
||||
cx.spawn(async move |_, cx| {
|
||||
participant.unpublish_track(publication.sid(), cx).await
|
||||
})
|
||||
.detach()
|
||||
} else {
|
||||
cx.spawn(async move |this, cx| {
|
||||
if failure_rx.await.is_ok() {
|
||||
log::warn!("Wayland capture died, auto-unsharing screen");
|
||||
let _ =
|
||||
this.update(cx, |this, cx| this.unshare_screen(false, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
live_kit.screen_track = LocalTrack::Published {
|
||||
track_publication: publication,
|
||||
_stream: stream,
|
||||
};
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Audio::play_sound(Sound::StartScreenshare, cx);
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
if canceled {
|
||||
Ok(())
|
||||
} else {
|
||||
live_kit.screen_track = LocalTrack::None;
|
||||
cx.notify();
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_mute(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(live_kit) = self.live_kit.as_mut() {
|
||||
// When unmuting, undeafen if the user was deafened before.
|
||||
|
|
|
|||
|
|
@ -6596,6 +6596,151 @@ async fn test_join_call_after_screen_was_shared(
|
|||
});
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_share_screen_wayland(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// User A calls user B.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User B accepts.
|
||||
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
|
||||
executor.run_until_parked();
|
||||
incoming_call_b.next().await.unwrap().unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
let room_b = active_call_b.read_with(cx_b, |call, _| call.room().unwrap().clone());
|
||||
executor.run_until_parked();
|
||||
|
||||
// User A shares their screen via the Wayland path.
|
||||
let events_b = active_call_events(cx_b);
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, cx| room.share_screen_wayland(cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// Room A is sharing and has a nonzero synthetic screen ID.
|
||||
room_a.read_with(cx_a, |room, _| {
|
||||
assert!(room.is_sharing_screen());
|
||||
let screen_id = room.shared_screen_id();
|
||||
assert!(screen_id.is_some(), "shared_screen_id should be Some");
|
||||
assert_ne!(screen_id.unwrap(), 0, "synthetic ID must be nonzero");
|
||||
});
|
||||
|
||||
// User B observes the remote screen sharing track.
|
||||
assert_eq!(events_b.borrow().len(), 1);
|
||||
if let call::room::Event::RemoteVideoTracksChanged { participant_id } =
|
||||
events_b.borrow().first().unwrap()
|
||||
{
|
||||
assert_eq!(*participant_id, client_a.peer_id().unwrap());
|
||||
room_b.read_with(cx_b, |room, _| {
|
||||
assert_eq!(
|
||||
room.remote_participants()[&client_a.user_id().unwrap()]
|
||||
.video_tracks
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
});
|
||||
} else {
|
||||
panic!("expected RemoteVideoTracksChanged event");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_unshare_screen_wayland(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// User A calls user B.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User B accepts.
|
||||
let mut incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
|
||||
executor.run_until_parked();
|
||||
incoming_call_b.next().await.unwrap().unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
executor.run_until_parked();
|
||||
|
||||
// User A shares their screen via the Wayland path.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room()
|
||||
.unwrap()
|
||||
.update(cx, |room, cx| room.share_screen_wayland(cx))
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
room_a.read_with(cx_a, |room, _| {
|
||||
assert!(room.is_sharing_screen());
|
||||
});
|
||||
|
||||
// User A stops sharing.
|
||||
room_a
|
||||
.update(cx_a, |room, cx| room.unshare_screen(true, cx))
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
// Room A is no longer sharing, screen ID is gone.
|
||||
room_a.read_with(cx_a, |room, _| {
|
||||
assert!(!room.is_sharing_screen());
|
||||
assert!(room.shared_screen_id().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx.executor().clone()).await;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ pub fn init(cx: &mut App) {
|
|||
});
|
||||
});
|
||||
});
|
||||
// TODO(jk): Is this action ever triggered?
|
||||
workspace.register_action(|_, _: &ScreenShare, window, cx| {
|
||||
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||
if let Some(room) = room {
|
||||
|
|
@ -179,19 +180,32 @@ pub fn init(cx: &mut App) {
|
|||
if room.is_sharing_screen() {
|
||||
room.unshare_screen(true, cx).ok();
|
||||
} else {
|
||||
let sources = cx.screen_capture_sources();
|
||||
#[cfg(target_os = "linux")]
|
||||
let is_wayland = gpui::guess_compositor() == "Wayland";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let is_wayland = false;
|
||||
|
||||
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(())
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_wayland {
|
||||
room.share_screen_wayland(cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
if !is_wayland {
|
||||
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);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}
|
|||
#[cfg(all(target_os = "macos", any(test, feature = "test-support")))]
|
||||
pub use visual_test::VisualTestPlatform;
|
||||
|
||||
// TODO(jk): return an enum instead of a string
|
||||
/// Return which compositor we're guessing we'll use.
|
||||
/// Does not attempt to connect to the given compositor.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ pub(crate) trait LinuxClient {
|
|||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
|
|
|
|||
|
|
@ -702,11 +702,6 @@ impl LinuxClient for WaylandClient {
|
|||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
fn screen_capture_sources(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use playback::capture_local_video_track;
|
|||
use settings::Settings;
|
||||
use std::sync::{Arc, atomic::AtomicU64};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
mod playback;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -231,6 +233,33 @@ impl LocalParticipant {
|
|||
.map(LocalTrackPublication)
|
||||
.context("unpublishing a track")
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn publish_screenshare_track_wayland(
|
||||
&self,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(
|
||||
LocalTrackPublication,
|
||||
Box<dyn ScreenCaptureStream>,
|
||||
futures::channel::oneshot::Receiver<()>,
|
||||
)> {
|
||||
let (track, stop_flag, feed_task, failure_rx) =
|
||||
linux::start_wayland_desktop_capture(cx).await?;
|
||||
let options = livekit::options::TrackPublishOptions {
|
||||
source: livekit::track::TrackSource::Screenshare,
|
||||
video_codec: livekit::options::VideoCodec::VP8,
|
||||
..Default::default()
|
||||
};
|
||||
let publication = self
|
||||
.publish_track(livekit::track::LocalTrack::Video(track.0), options, cx)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
publication,
|
||||
Box::new(linux::WaylandScreenCaptureStream::new(stop_flag, feed_task)),
|
||||
failure_rx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalTrackPublication {
|
||||
|
|
|
|||
206
crates/livekit_client/src/livekit_client/linux.rs
Normal file
206
crates/livekit_client/src/livekit_client/linux.rs
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
use anyhow::Result;
|
||||
use futures::StreamExt as _;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AsyncApp, ScreenCaptureStream};
|
||||
use livekit::track;
|
||||
use livekit::webrtc::{
|
||||
video_frame::{VideoFrame, VideoRotation},
|
||||
video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource},
|
||||
};
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
static NEXT_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
pub struct WaylandScreenCaptureStream {
|
||||
id: u64,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
_feed_task: gpui::Task<()>,
|
||||
}
|
||||
|
||||
impl WaylandScreenCaptureStream {
|
||||
pub fn new(stop_flag: Arc<AtomicBool>, feed_task: gpui::Task<()>) -> Self {
|
||||
Self {
|
||||
id: NEXT_WAYLAND_SHARE_ID.fetch_add(1, Ordering::Relaxed),
|
||||
stop_flag,
|
||||
_feed_task: feed_task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScreenCaptureStream for WaylandScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<gpui::SourceMetadata> {
|
||||
Ok(gpui::SourceMetadata {
|
||||
id: self.id,
|
||||
label: None,
|
||||
is_main: None,
|
||||
resolution: gpui::size(gpui::DevicePixels(1), gpui::DevicePixels(1)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WaylandScreenCaptureStream {
|
||||
fn drop(&mut self) {
|
||||
self.stop_flag.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
struct CapturedFrame {
|
||||
width: u32,
|
||||
height: u32,
|
||||
stride: u32,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
fn desktop_frame_to_nv12(frame: &CapturedFrame) -> livekit::webrtc::prelude::NV12Buffer {
|
||||
use libwebrtc::native::yuv_helper::argb_to_nv12;
|
||||
use livekit::webrtc::prelude::NV12Buffer;
|
||||
|
||||
let mut buffer = NV12Buffer::new(frame.width, frame.height);
|
||||
let (stride_y, stride_uv) = buffer.strides();
|
||||
let (data_y, data_uv) = buffer.data_mut();
|
||||
argb_to_nv12(
|
||||
&frame.data,
|
||||
frame.stride,
|
||||
data_y,
|
||||
stride_y,
|
||||
data_uv,
|
||||
stride_uv,
|
||||
frame.width as i32,
|
||||
frame.height as i32,
|
||||
);
|
||||
buffer
|
||||
}
|
||||
|
||||
pub(crate) async fn start_wayland_desktop_capture(
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(
|
||||
crate::LocalVideoTrack,
|
||||
Arc<AtomicBool>,
|
||||
gpui::Task<()>,
|
||||
oneshot::Receiver<()>,
|
||||
)> {
|
||||
use futures::channel::mpsc;
|
||||
use gpui::FutureExt as _;
|
||||
use libwebrtc::desktop_capturer::{
|
||||
CaptureError, DesktopCaptureSourceType, DesktopCapturer, DesktopCapturerOptions,
|
||||
};
|
||||
|
||||
let (frame_tx, mut frame_rx) = mpsc::channel::<CapturedFrame>(2);
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stop = stop_flag.clone();
|
||||
|
||||
let permanent_error = Arc::new(AtomicBool::new(false));
|
||||
let permanent_error_cb = permanent_error.clone();
|
||||
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
let capture_executor = executor.clone();
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut options = DesktopCapturerOptions::new(DesktopCaptureSourceType::Generic);
|
||||
options.set_include_cursor(true);
|
||||
|
||||
let Some(mut capturer) = DesktopCapturer::new(options) else {
|
||||
log::error!(
|
||||
"Failed to create Wayland desktop capturer. Is xdg-desktop-portal running?"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let frame_tx_cb = parking_lot::Mutex::new(frame_tx.clone());
|
||||
capturer.start_capture(None, move |result| match result {
|
||||
Ok(frame) => {
|
||||
let captured = CapturedFrame {
|
||||
width: frame.width() as u32,
|
||||
height: frame.height() as u32,
|
||||
stride: frame.stride(),
|
||||
data: frame.data().to_vec(),
|
||||
};
|
||||
frame_tx_cb.lock().try_send(captured).ok();
|
||||
}
|
||||
Err(CaptureError::Temporary) => {
|
||||
// Expected before the portal picker completes
|
||||
}
|
||||
Err(CaptureError::Permanent) => {
|
||||
permanent_error_cb.store(true, Ordering::Relaxed);
|
||||
log::error!("Wayland desktop capture encountered a permanent error");
|
||||
}
|
||||
});
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
capturer.capture_frame();
|
||||
if permanent_error.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
capture_executor.timer(Duration::from_millis(33)).await;
|
||||
}
|
||||
|
||||
drop(frame_tx);
|
||||
})
|
||||
.detach();
|
||||
let first_frame = frame_rx
|
||||
.next()
|
||||
.with_timeout(Duration::from_secs(15), &executor)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
anyhow::anyhow!(
|
||||
"Screen sharing timed out waiting for the first frame. \
|
||||
Check that xdg-desktop-portal and PipeWire are running, \
|
||||
and that your portal backend matches your compositor."
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Screen sharing was canceled or the portal denied permission. \
|
||||
You can try again from the screen share button."
|
||||
)
|
||||
})?;
|
||||
|
||||
let width = first_frame.width;
|
||||
let height = first_frame.height;
|
||||
let video_source = gpui_tokio::Tokio::spawn(cx, async move {
|
||||
NativeVideoSource::new(VideoResolution { width, height }, true)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let nv12 = desktop_frame_to_nv12(&first_frame);
|
||||
video_source.capture_frame(&VideoFrame {
|
||||
rotation: VideoRotation::VideoRotation0,
|
||||
timestamp_us: 0,
|
||||
buffer: nv12,
|
||||
});
|
||||
|
||||
let track = super::LocalVideoTrack(track::LocalVideoTrack::create_video_track(
|
||||
"screen share",
|
||||
RtcVideoSource::Native(video_source.clone()),
|
||||
));
|
||||
|
||||
let (failure_tx, failure_rx) = oneshot::channel::<()>();
|
||||
let feed_stop = stop_flag.clone();
|
||||
let feed_task = cx.background_executor().spawn(async move {
|
||||
while let Some(frame) = frame_rx.next().await {
|
||||
if feed_stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let nv12 = desktop_frame_to_nv12(&frame);
|
||||
video_source.capture_frame(&VideoFrame {
|
||||
rotation: VideoRotation::VideoRotation0,
|
||||
timestamp_us: 0,
|
||||
buffer: nv12,
|
||||
});
|
||||
}
|
||||
if !feed_stop.load(Ordering::Relaxed) {
|
||||
log::error!("Wayland screen capture ended unexpectedly");
|
||||
let _ = failure_tx.send(());
|
||||
}
|
||||
});
|
||||
|
||||
Ok((track, stop_flag, feed_task, failure_rx))
|
||||
}
|
||||
|
|
@ -99,6 +99,31 @@ impl LocalParticipant {
|
|||
Box::new(TestScreenCaptureStream {}),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn publish_screenshare_track_wayland(
|
||||
&self,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<(
|
||||
LocalTrackPublication,
|
||||
Box<dyn ScreenCaptureStream>,
|
||||
futures::channel::oneshot::Receiver<()>,
|
||||
)> {
|
||||
let (_failure_tx, failure_rx) = futures::channel::oneshot::channel();
|
||||
let this = self.clone();
|
||||
let server = this.room.test_server();
|
||||
let sid = server
|
||||
.publish_video_track(this.room.token(), LocalVideoTrack {})
|
||||
.await?;
|
||||
Ok((
|
||||
LocalTrackPublication {
|
||||
room: self.room.downgrade(),
|
||||
sid,
|
||||
},
|
||||
Box::new(TestWaylandScreenCaptureStream::new()),
|
||||
failure_rx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteParticipant {
|
||||
|
|
@ -166,3 +191,32 @@ impl ScreenCaptureStream for TestScreenCaptureStream {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
static NEXT_TEST_WAYLAND_SHARE_ID: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
struct TestWaylandScreenCaptureStream {
|
||||
id: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl TestWaylandScreenCaptureStream {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
id: NEXT_TEST_WAYLAND_SHARE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl ScreenCaptureStream for TestWaylandScreenCaptureStream {
|
||||
fn metadata(&self) -> Result<SourceMetadata> {
|
||||
Ok(SourceMetadata {
|
||||
id: self.id,
|
||||
is_main: None,
|
||||
label: None,
|
||||
resolution: size(DevicePixels(1), DevicePixels(1)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -548,6 +548,11 @@ impl TitleBar {
|
|||
);
|
||||
|
||||
if can_use_microphone && screen_sharing_supported {
|
||||
#[cfg(target_os = "linux")]
|
||||
let is_wayland = gpui::guess_compositor() == "Wayland";
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let is_wayland = false;
|
||||
|
||||
let trigger = IconButton::new("screen-share", IconName::Screen)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
|
|
@ -564,28 +569,56 @@ impl TitleBar {
|
|||
.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 {
|
||||
Ok(None)
|
||||
};
|
||||
cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?;
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if is_wayland
|
||||
&& let Some(room) = ActiveCall::global(cx).read(cx).room().cloned()
|
||||
{
|
||||
let task = room.update(cx, |room, cx| {
|
||||
if should_share {
|
||||
room.share_screen_wayland(cx)
|
||||
} else {
|
||||
room.unshare_screen(true, cx)
|
||||
.map(|()| Task::ready(Ok(())))
|
||||
.unwrap_or_else(|e| Task::ready(Err(e)))
|
||||
}
|
||||
});
|
||||
task.detach_and_prompt_err(
|
||||
"Sharing Screen Failed",
|
||||
window,
|
||||
cx,
|
||||
|e, _, _| Some(format!("{e:?}")),
|
||||
);
|
||||
}
|
||||
}
|
||||
if !is_wayland {
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let screen = if should_share {
|
||||
cx.update(|_, cx| pick_default_screen(cx))?.await
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?;
|
||||
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach();
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
children.push(
|
||||
SplitButton::new(
|
||||
trigger.render(window, cx),
|
||||
self.render_screen_list().into_any_element(),
|
||||
)
|
||||
.style(SplitButtonStyle::Transparent)
|
||||
.into_any_element(),
|
||||
);
|
||||
if is_wayland {
|
||||
children.push(trigger.into_any_element());
|
||||
} else {
|
||||
children.push(
|
||||
SplitButton::new(
|
||||
trigger.render(window, cx),
|
||||
self.render_screen_list().into_any_element(),
|
||||
)
|
||||
.style(SplitButtonStyle::Transparent)
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
children.push(div().pr_2().into_any_element());
|
||||
|
|
|
|||
12
script/linux
12
script/linux
|
|
@ -50,6 +50,8 @@ if [[ -n $apt ]]; then
|
|||
musl-tools
|
||||
musl-dev
|
||||
build-essential
|
||||
pipewire
|
||||
xdg-desktop-portal
|
||||
)
|
||||
if (grep -qP 'PRETTY_NAME="(Debian|Raspbian).+13' /etc/os-release); then
|
||||
# libstdc++-14-dev is in build-essential
|
||||
|
|
@ -110,6 +112,8 @@ if [[ -n $dnf ]] || [[ -n $yum ]]; then
|
|||
libzstd-devel
|
||||
vulkan-loader
|
||||
sqlite-devel
|
||||
pipewire
|
||||
xdg-desktop-portal
|
||||
jq
|
||||
git
|
||||
tar
|
||||
|
|
@ -185,6 +189,8 @@ if [[ -n $zyp ]]; then
|
|||
tar
|
||||
wayland-devel
|
||||
xcb-util-devel
|
||||
pipewire
|
||||
xdg-desktop-portal
|
||||
)
|
||||
$maysudo "$zyp" install -y "${deps[@]}"
|
||||
finalize
|
||||
|
|
@ -213,6 +219,8 @@ if [[ -n $pacman ]]; then
|
|||
pkgconf
|
||||
mold
|
||||
sqlite
|
||||
pipewire
|
||||
xdg-desktop-portal
|
||||
jq
|
||||
git
|
||||
)
|
||||
|
|
@ -244,6 +252,8 @@ if [[ -n $xbps ]]; then
|
|||
vulkan-loader
|
||||
mold
|
||||
sqlite-devel
|
||||
pipewire
|
||||
xdg-desktop-portal
|
||||
)
|
||||
$maysudo "$xbps" -Syu "${deps[@]}"
|
||||
finalize
|
||||
|
|
@ -269,6 +279,8 @@ if [[ -n $emerge ]]; then
|
|||
x11-libs/libxkbcommon
|
||||
sys-devel/mold
|
||||
dev-db/sqlite
|
||||
media-video/pipewire
|
||||
sys-apps/xdg-desktop-portal
|
||||
)
|
||||
$maysudo "$emerge" -u "${deps[@]}"
|
||||
finalize
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue