Detailed changes
@@ -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();
-
- 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")]
+ let is_wayland = gpui::guess_compositor() == "Wayland";
+ #[cfg(not(target_os = "linux"))]
+ let is_wayland = false;
+
+ #[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 {
@@ -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());
@@ -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