From f3dc87fa648654c138178701ccfc1d82dea1d4a5 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:31:13 +0900 Subject: [PATCH] rework to having non-blocking screen cast prompts --- Cargo.lock | 217 +---- Cargo.toml | 1 + wayvr/Cargo.toml | 75 +- wayvr/src/assets/gui/screencast.xml | 18 + wayvr/src/assets/lang/de.json | 10 +- wayvr/src/assets/lang/en.json | 8 + wayvr/src/assets/lang/es.json | 10 +- wayvr/src/assets/lang/it.json | 10 +- wayvr/src/assets/lang/ja.json | 12 +- wayvr/src/assets/lang/pl.json | 10 +- wayvr/src/assets/lang/zh_CN.json | 10 +- wayvr/src/backend/openxr/overlay.rs | 8 +- wayvr/src/gui/panel/button.rs | 7 +- wayvr/src/gui/panel/mod.rs | 5 +- wayvr/src/overlays/screen/backend.rs | 9 +- wayvr/src/overlays/screen/mirror.rs | 196 ++-- wayvr/src/overlays/screen/mod.rs | 2 +- wayvr/src/overlays/screen/pw.rs | 505 +++++++--- wayvr/src/overlays/screen/wl.rs | 128 +-- wayvr/src/overlays/screen/x11.rs | 55 +- wayvr/src/state.rs | 58 ++ wgui/src/parser/mod.rs | 8 + wlx-capture/Cargo.toml | 64 +- .../src/{pipewire.rs => pipewire/capture.rs} | 96 -- wlx-capture/src/pipewire/dbus_screencast.rs | 50 + wlx-capture/src/pipewire/mod.rs | 905 ++++++++++++++++++ 26 files changed, 1735 insertions(+), 742 deletions(-) create mode 100644 wayvr/src/assets/gui/screencast.xml rename wlx-capture/src/{pipewire.rs => pipewire/capture.rs} (89%) create mode 100644 wlx-capture/src/pipewire/dbus_screencast.rs create mode 100644 wlx-capture/src/pipewire/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0f4a4ab4..cc2a291c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,36 +294,6 @@ dependencies = [ "libloading 0.8.9", ] -[[package]] -name = "ashpd" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a3c86f3fd70c0ffa500ed189abfa90b5a52398a45d5dc372fcc38ebeb7a645" -dependencies = [ - "async-fs", - "async-net", - "enumflags2", - "futures-channel", - "futures-util", - "rand 0.9.4", - "serde", - "serde_repr", - "url", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - [[package]] name = "async-channel" version = "2.5.0" @@ -431,17 +401,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "async-signal" version = "0.2.14" @@ -1501,6 +1460,8 @@ version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" dependencies = [ + "futures-channel", + "futures-util", "libc", "libdbus-sys", "windows-sys 0.61.2", @@ -1685,12 +1646,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - [[package]] name = "enum-primitive-derive" version = "0.2.2" @@ -1702,27 +1657,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "enumset" version = "1.1.13" @@ -3196,15 +3130,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miette" version = "5.10.0" @@ -3916,16 +3841,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "ouroboros" version = "0.18.5" @@ -5132,17 +5047,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "serde_spanned" version = "1.1.1" @@ -6091,17 +5995,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - [[package]] name = "uidev" version = "0.1.0" @@ -6177,7 +6070,6 @@ dependencies = [ "idna", "percent-encoding", "serde", - "serde_derive", ] [[package]] @@ -7213,13 +7105,14 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" name = "wlx-capture" version = "0.6.0" dependencies = [ - "ashpd", + "dbus", "drm-fourcc", "idmap", "libc", "log", "pipewire", "rxscreen", + "slotmap", "smithay-client-toolkit 0.20.0", "wayland-client", "wayland-protocols", @@ -7422,67 +7315,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.4", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.118", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" -dependencies = [ - "serde", - "winnow", - "zvariant", -] - [[package]] name = "zeno" version = "0.3.3" @@ -7592,44 +7424,3 @@ checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ "zune-core", ] - -[[package]] -name = "zvariant" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" -dependencies = [ - "endi", - "enumflags2", - "serde", - "url", - "winnow", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.118", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.118", - "winnow", -] diff --git a/Cargo.toml b/Cargo.toml index cd326be4..bfd05042 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ anyhow = "1.0.100" bytes = { version = "1.11.1" } clap = { version = "4.5.53", features = ["derive"] } +dbus = { version = "0.9.12" } glam = { version = "0.30.9", features = ["mint", "serde"] } idmap = "0.2.2" idmap-derive = "0.2.22" diff --git a/wayvr/Cargo.toml b/wayvr/Cargo.toml index d14b22c3..e734b8e1 100644 --- a/wayvr/Cargo.toml +++ b/wayvr/Cargo.toml @@ -28,6 +28,7 @@ wlx-common = { path = "../wlx-common" } anyhow.workspace = true clap.workspace = true +dbus.workspace = true glam = { workspace = true, features = ["mint", "serde"] } idmap = { workspace = true, features = ["serde"] } idmap-derive.workspace = true @@ -44,41 +45,58 @@ vulkano.workspace = true vulkano-shaders.workspace = true xdg.workspace = true -ash = "^0.38.0" # must match vulkano +ash = "^0.38.0" # must match vulkano bytes = { workspace = true } chrono = { version = "0.4.42", features = ["unstable-locales"] } chrono-tz = "0.10.4" config = "0.15.19" -dbus = { version = "0.9.9" } futures = "0.3.31" -image_dds = { version = "0.7.2", default-features = false, features = [ - "ddsfile", -] } +image_dds = { + version = "0.7.2", + default-features = false, + features = [ + "ddsfile", + ] +} input-linux = "0.7.1" interprocess = { version = "2.2.3" } json = { version = "0.12.4", optional = true } json5 = "1.3.0" libc = "0.2.178" -libmonado = { git = "https://github.com/wayvr-org/libmonado-rs.git", rev = "6f66b26930c24a8a2fc57ddcd85704784894c750", optional = true } +libmonado = { + git = "https://github.com/wayvr-org/libmonado-rs.git", + rev = "6f66b26930c24a8a2fc57ddcd85704784894c750", + optional = true +} log-panics = { version = "2.1.0", features = ["with-backtrace"] } mint = "0.5.9" openxr = { version = "0.21.0", features = ["linked", "mint"], optional = true } -ovr_overlay = { git = "https://github.com/galister/ovr_overlay_oyasumi", rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc", features = [ - "ovr_input", - "ovr_system", -], optional = true } +ovr_overlay = { + git = "https://github.com/galister/ovr_overlay_oyasumi", + rev = "e477bd2a9e04293ea68c1e7529ef2cb131f32acc", + features = [ + "ovr_input", + "ovr_system", + ], + optional = true +} prost = { version = "0.14.3", optional = true } pure-rust-locales = "0.8.2" rosc = { version = "0.11.4", optional = true } serde_yaml = "0.9.34" signal-hook = "0.3.18" smallvec = "1.15.1" -smithay = { version = "0.7.0", default-features = false, features = [ - "backend_vulkan", - "desktop", - "wayland_frontend", - "xwayland", -] } +smithay = { + version = "0.7.0", + default-features = false, + features = [ + "backend_vulkan", + "desktop", + "wayland_frontend", + "xwayland", + ] +} +smithay-clipboard = "0.7.3" smol = { workspace = true } sysinfo = { version = "0.37" } thiserror = "2.0" @@ -88,19 +106,23 @@ uuid = { workspace = true } wayland-client = { workspace = true } wayland-protocols = { workspace = true } wayland-protocols-misc = { workspace = true } -winit = { version = "0.30.12", optional = true } -xcb = { version = "1.6.0", features = [ - "as-raw-xcb-connection", -], optional = true } -xkbcommon = { version = "0.8.0" } # 0.9.0 breaks keymap import on some distros whisper-rs = { version = "0.16.0", features = ["vulkan"], optional = true } -smithay-clipboard = "0.7.3" +winit = { version = "0.30.12", optional = true } +xcb = { + version = "1.6.0", + features = [ + "as-raw-xcb-connection", + ], + optional = true +} +xkbcommon = { + version = "0.8.0" +} # 0.9.0 breaks keymap import on some distros calloop = { version = "0.14", optional = true } calloop-wayland-source = { version = "0.4", optional = true } [build-dependencies] - regex.workspace = true [features] @@ -113,6 +135,11 @@ osc = ["dep:rosc"] pipewire = ["wlx-capture/pipewire"] uidev = ["dep:winit"] wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"] -whisper = ["dep:pipewire", "dep:whisper-rs", "dep:calloop", "dep:calloop-wayland-source"] +whisper = [ + "dep:calloop", + "dep:calloop-wayland-source", + "dep:pipewire", + "dep:whisper-rs" +] x11 = ["dep:xcb", "wlx-capture/xshm", "xkbcommon/x11"] xcb = ["dep:xcb"] diff --git a/wayvr/src/assets/gui/screencast.xml b/wayvr/src/assets/gui/screencast.xml new file mode 100644 index 00000000..e3baf388 --- /dev/null +++ b/wayvr/src/assets/gui/screencast.xml @@ -0,0 +1,18 @@ + + + +
+
+
+
+
+
+
+
+
+
+ +
diff --git a/wayvr/src/assets/lang/de.json b/wayvr/src/assets/lang/de.json index ded01c0c..bed0a763 100644 --- a/wayvr/src/assets/lang/de.json +++ b/wayvr/src/assets/lang/de.json @@ -113,7 +113,8 @@ "ONE_CONTROLLER_ON_FLOOR": "Lege einen Controller auf den Boden!", "CANNOT_ADD_SET": "Satz kann nicht hinzugefügt werden!", "MAXIMUM_SETS_REACHED": "Maximale Anzahl an Sets erreicht.", - "FIXING_FLOOR_IN_X_SECS": "Boden wird in {SECONDS} Sekunde(n) repariert..." + "FIXING_FLOOR_IN_X_SECS": "Boden wird in {SECONDS} Sekunde(n) repariert...", + "TITLE_SCREENCAST_FAIL": "Bildschirmübertragungsquelle konnte nicht gestartet werden!" }, "DONE": "Erledigt", "WHISPER": { @@ -127,5 +128,12 @@ "INITIAL_TEXT": "Halten Sie die Mikrofontaste gedrückt, um mit der Transkription zu beginnen...", "INIT_ERROR": "Fehler beim Starten der Whisper-Engine", "ENTER_KEY": "Enter-Taste" + }, + "SCREENCAST": { + "PENDING": "Bitte warten...", + "QUEUED": "Warten auf die Bestätigung des Screen-Cast-Pop-ups eines anderen Overlays.", + "WAITING_FOR_USER_DESC": "Überprüfen Sie Ihren Desktop auf ein Pop-up zur Bildschirmübertragung.\nWählen Sie Folgendes aus:", + "WAITING_FOR_USER": "Überprüfen Sie Ihren Desktop auf ein Pop-up zur Bildschirmübertragung.", + "FAIL": "Bildschirmübertragungsquelle konnte nicht initialisiert werden." } } \ No newline at end of file diff --git a/wayvr/src/assets/lang/en.json b/wayvr/src/assets/lang/en.json index 69119250..e5214b69 100644 --- a/wayvr/src/assets/lang/en.json +++ b/wayvr/src/assets/lang/en.json @@ -89,7 +89,15 @@ "GRABBING_WATCH": "To swap hands, move the watch in front and grab it with the other hand.", "UNRESTRICTED_MOVEMENT": "Unrestricted movement" }, + "SCREENCAST": { + "PENDING": "Please wait...", + "QUEUED": "Waiting for a different overlay's screen cast pop-up to be approved.", + "WAITING_FOR_USER_DESC": "Check your dekstop for a screen cast pop-up.\nSelect the following:", + "WAITING_FOR_USER": "Check your desktop for a screen cast pop-up.", + "FAIL": "Could not initialize screen cast source." + }, "TOAST": { + "TITLE_SCREENCAST_FAIL": "Could not start screen cast source!", "CANNOT_ADD_SET": "Cannot add set!", "CANNOT_REMOVE_SET": "Cannot remove set!", "DEFAULT_TITLE": "Notification", diff --git a/wayvr/src/assets/lang/es.json b/wayvr/src/assets/lang/es.json index b12dcb57..9683d0a2 100644 --- a/wayvr/src/assets/lang/es.json +++ b/wayvr/src/assets/lang/es.json @@ -113,7 +113,8 @@ "ONE_CONTROLLER_ON_FLOOR": "¡Coloca un mando en el suelo!", "CANNOT_ADD_SET": "¡No se puede agregar el conjunto!", "MAXIMUM_SETS_REACHED": "Se ha alcanzado el número máximo de sets.", - "FIXING_FLOOR_IN_X_SECS": "Corrigiendo el suelo en {SECONDS} segundo(s)..." + "FIXING_FLOOR_IN_X_SECS": "Corrigiendo el suelo en {SECONDS} segundo(s)...", + "TITLE_SCREENCAST_FAIL": "¡No se pudo iniciar la fuente de transmisión de pantalla!" }, "DONE": "Hecho", "WHISPER": { @@ -127,5 +128,12 @@ "INITIAL_TEXT": "Mantén presionado el botón del micrófono para comenzar a transcribir...", "INIT_ERROR": "Error al iniciar el motor de Whisper", "ENTER_KEY": "Tecla Enter" + }, + "SCREENCAST": { + "PENDING": "Por favor, espere...", + "QUEUED": "Esperando a que se apruebe la ventana emergente de la transmisión de pantalla de otra superposición.", + "WAITING_FOR_USER_DESC": "Busca la ventana emergente de transmisión de pantalla en tu escritorio.\nSelecciona lo siguiente:", + "WAITING_FOR_USER": "Revisa tu escritorio para ver la ventana emergente de transmisión de pantalla.", + "FAIL": "No se pudo inicializar la fuente de transmisión de pantalla." } } \ No newline at end of file diff --git a/wayvr/src/assets/lang/it.json b/wayvr/src/assets/lang/it.json index 4321c9a3..d165fd08 100644 --- a/wayvr/src/assets/lang/it.json +++ b/wayvr/src/assets/lang/it.json @@ -99,7 +99,8 @@ "EMPTY_SET": "Set vuoto!", "LETS_ADD_OVERLAYS": "Aggiungiamo degli overlay dal Watch!", "ONE_CONTROLLER_ON_FLOOR": "Posiziona un controller a terra!", - "FIXING_FLOOR_IN_X_SECS": "Correzione del pavimento in {SECONDS} secondo/i..." + "FIXING_FLOOR_IN_X_SECS": "Correzione del pavimento in {SECONDS} secondo/i...", + "TITLE_SCREENCAST_FAIL": "Impossibile avviare la sorgente dello screen cast!" }, "WATCH": { "ADD_NEW_SET": "Aggiungi un nuovo set", @@ -125,5 +126,12 @@ "INITIAL_TEXT": "Tieni premuto il tasto del microfono per iniziare la trascrizione...", "INIT_ERROR": "Errore durante l'avvio del motore Whisper", "ENTER_KEY": "Tasto Invio" + }, + "SCREENCAST": { + "PENDING": "Attendi prego...", + "QUEUED": "In attesa dell'approvazione del pop-up di screen cast di un altro overlay.", + "WAITING_FOR_USER_DESC": "Controlla il desktop per un pop-up di condivisione schermo.\nSeleziona quanto segue:", + "WAITING_FOR_USER": "Controlla il desktop per un pop-up di condivisione schermo.", + "FAIL": "Impossibile inizializzare la sorgente di trasmissione dello schermo." } } \ No newline at end of file diff --git a/wayvr/src/assets/lang/ja.json b/wayvr/src/assets/lang/ja.json index 110f0caa..d719a5fd 100644 --- a/wayvr/src/assets/lang/ja.json +++ b/wayvr/src/assets/lang/ja.json @@ -111,7 +111,8 @@ "ONE_CONTROLLER_ON_FLOOR": "コントローラーを床に置いてください!", "CANNOT_ADD_SET": "セットを追加できません!", "MAXIMUM_SETS_REACHED": "最大セット数に達しました。", - "FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒で床を固定中..." + "FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒で床を固定中...", + "TITLE_SCREENCAST_FAIL": "スクリーンキャストのソースを開始できませんでした!" }, "DONE": "完了", "WHISPER": { @@ -125,5 +126,12 @@ "INITIAL_TEXT": "マイクボタンを押し続けて文字起こしを開始してください...", "INIT_ERROR": "Whisperエンジンの起動中にエラーが発生しました", "ENTER_KEY": "Enterキー" + }, + "SCREENCAST": { + "PENDING": "少々お待ちください...", + "QUEUED": "別のオーバーレイのスクリーンキャスト許可ポップアップの応答を待っています。", + "WAITING_FOR_USER_DESC": "デスクトップのスクリーンキャストのポップアップを確認してください。\n以下を選択してください:", + "WAITING_FOR_USER": "デスクトップにスクリーンキャストのポップアップが表示されていないか確認してください。", + "FAIL": "スクリーンキャストのソースを初期化できませんでした。" } -} +} \ No newline at end of file diff --git a/wayvr/src/assets/lang/pl.json b/wayvr/src/assets/lang/pl.json index 62cf8913..a1b7ce6d 100644 --- a/wayvr/src/assets/lang/pl.json +++ b/wayvr/src/assets/lang/pl.json @@ -111,7 +111,8 @@ "ONE_CONTROLLER_ON_FLOOR": "Umieść jeden kontroler na podłodze!", "CANNOT_ADD_SET": "Nie można dodać zestawu!", "MAXIMUM_SETS_REACHED": "Osiągnięto maksymalną liczbę zestawów.", - "FIXING_FLOOR_IN_X_SECS": "Naprawianie podłogi za {SECONDS} sekund(y)..." + "FIXING_FLOOR_IN_X_SECS": "Naprawianie podłogi za {SECONDS} sekund(y)...", + "TITLE_SCREENCAST_FAIL": "Nie udało się uruchomić źródła przesyłania obrazu z ekranu!" }, "DONE": "Gotowe", "WHISPER": { @@ -125,5 +126,12 @@ "INITIAL_TEXT": "Naciśnij i przytrzymaj przycisk mikrofonu, aby rozpocząć transkrypcję...", "INIT_ERROR": "Błąd podczas uruchamiania silnika Whisper", "ENTER_KEY": "Klawisz Enter" + }, + "SCREENCAST": { + "PENDING": "Proszę czekać...", + "QUEUED": "Oczekiwanie na zatwierdzenie okna wysyłania obrazu z innego nakładki.", + "WAITING_FOR_USER_DESC": "Sprawdź na pulpicie okno wyboru ekranu do transmitowania.\nWybierz następujące:", + "WAITING_FOR_USER": "Sprawdź na pulpicie powiadomienie o przesyłaniu ekranu.", + "FAIL": "Nie udało się zainicjować źródła przesyłania obrazu z ekranu." } } \ No newline at end of file diff --git a/wayvr/src/assets/lang/zh_CN.json b/wayvr/src/assets/lang/zh_CN.json index f70babe9..56dec529 100644 --- a/wayvr/src/assets/lang/zh_CN.json +++ b/wayvr/src/assets/lang/zh_CN.json @@ -99,7 +99,8 @@ "EMPTY_SET": "空集合!", "LETS_ADD_OVERLAYS": "让我们从手表添加一些覆盖层吧!", "ONE_CONTROLLER_ON_FLOOR": "请将一个控制器放在地面上!", - "FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒内固定地面..." + "FIXING_FLOOR_IN_X_SECS": "{SECONDS}秒内固定地面...", + "TITLE_SCREENCAST_FAIL": "无法启动屏幕投影源!" }, "WATCH": { "ADD_NEW_SET": "添加新集合", @@ -125,5 +126,12 @@ "INITIAL_TEXT": "按住麦克风按钮开始转录...", "INIT_ERROR": "启动 Whisper 引擎时出错", "ENTER_KEY": "回车键" + }, + "SCREENCAST": { + "PENDING": "请稍候...", + "QUEUED": "等待另一个覆盖层的屏幕投射确认弹窗。", + "WAITING_FOR_USER_DESC": "请检查桌面上的屏幕投射弹出窗口。\n选择以下选项:", + "WAITING_FOR_USER": "请检查桌面上的屏幕投射弹出窗口。", + "FAIL": "无法初始化屏幕镜像源。" } } \ No newline at end of file diff --git a/wayvr/src/backend/openxr/overlay.rs b/wayvr/src/backend/openxr/overlay.rs index b4404e86..8479569c 100644 --- a/wayvr/src/backend/openxr/overlay.rs +++ b/wayvr/src/backend/openxr/overlay.rs @@ -87,7 +87,13 @@ impl OverlayWindowData { smallvec![(swapchain.get_subimage(0), EyeVisibility::BOTH),] }; - let transform = state.transform * self.config.backend.frame_meta().unwrap().transform; // contract + let transform = state.transform + * self + .config + .backend + .frame_meta() + .map(|x| x.transform) + .unwrap_or_default(); // contract let aspect_ratio = swapchain.extent[0] as f32 / swapchain.extent[1] as f32; let (scale_x, scale_y) = if aspect_ratio < 1.0 { diff --git a/wayvr/src/gui/panel/button.rs b/wayvr/src/gui/panel/button.rs index 62384500..840a0c82 100644 --- a/wayvr/src/gui/panel/button.rs +++ b/wayvr/src/gui/panel/button.rs @@ -539,10 +539,9 @@ pub(super) fn setup_custom_button( OverlaySelector::Name(name.clone()), SpawnPos::Spread, Box::new(move |app| { - Some(crate::overlays::screen::mirror::new_mirror( - name, - &app.session, - )) + crate::overlays::screen::mirror::new_mirror(name, app) + .log_err("Could not create mirror") + .ok() }), ))); Ok(EventResult::Consumed) diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index 3ee58c82..bfd9e64e 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -14,7 +14,7 @@ use button::setup_custom_button; use glam::{Affine2, Vec2, vec2}; use idmap::IdMap; use label::setup_custom_label; -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; use wgui::{ assets::AssetPath, components::{ @@ -95,6 +95,7 @@ pub struct NewGuiPanelParams { pub resize_to_parent: bool, pub external_xml: bool, pub gui_scale: f32, + pub extra_vars: HashMap, Rc>, } impl Default for NewGuiPanelParams { @@ -105,6 +106,7 @@ impl Default for NewGuiPanelParams { resize_to_parent: false, external_xml: false, gui_scale: 1.0, + extra_vars: HashMap::default(), } } } @@ -134,6 +136,7 @@ impl GuiPanel { }, extra: wgui::parser::ParseDocumentExtra { on_custom_attribs: Some(on_custom_attrib_inner.clone()), + extra_vars: params.extra_vars, ..Default::default() }, }; diff --git a/wayvr/src/overlays/screen/backend.rs b/wayvr/src/overlays/screen/backend.rs index ec049f28..e40d974d 100644 --- a/wayvr/src/overlays/screen/backend.rs +++ b/wayvr/src/overlays/screen/backend.rs @@ -59,7 +59,6 @@ pub struct ScreenBackend { stereo_adjust_mouse: bool, pub(super) logical_pos: Vec2, pub(super) logical_size: Vec2, - pub(super) mouse_transform_original: Transform, mouse_transform_override: MouseTransform, just_resumed: bool, } @@ -89,13 +88,12 @@ impl ScreenBackend { stereo_adjust_mouse: false, logical_pos: Vec2::ZERO, logical_size: Vec2::ZERO, - mouse_transform_original: Transform::Undefined, mouse_transform_override: MouseTransform::Default, just_resumed: false, } } - pub(super) fn apply_mouse_transform_with_override(&mut self, override_transform: Transform) { + pub(super) fn apply_mouse_transform_with_override(&mut self, transform: Transform) { let mut size = self.logical_size; let pos = self.logical_pos; @@ -109,11 +107,6 @@ impl ScreenBackend { } } - let transform = match override_transform { - Transform::Undefined => self.mouse_transform_original, - other => other, - }; - self.mouse_transform = match transform { Transform::Normal | Transform::Undefined => { Affine2::from_cols(vec2(size.x, 0.), vec2(0., size.y), pos) diff --git a/wayvr/src/overlays/screen/mirror.rs b/wayvr/src/overlays/screen/mirror.rs index 059a4172..3e7801b1 100644 --- a/wayvr/src/overlays/screen/mirror.rs +++ b/wayvr/src/overlays/screen/mirror.rs @@ -1,164 +1,59 @@ -use std::{ - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - task::{Context, Poll}, +use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, }; -use futures::{Future, FutureExt}; -use glam::{Affine2, Affine3A, Quat, Vec3, vec3}; -use wlx_capture::{ - WlxCapture, - pipewire::{PipewireCapture, PipewireSelectScreenResult, pipewire_select_screen}, -}; +use glam::{Affine2, Affine3A, Quat, Vec2, Vec3, vec3}; +use wlx_capture::{WlxCapture, pipewire::ScreenCastParams}; use wlx_common::{ overlays::{BackendAttrib, BackendAttribValue}, windowing::OverlayWindowState, }; use crate::{ - backend::{ - input::{HoverResult, PointerHit}, - task::{OverlayTask, TaskType, ToggleMode}, - }, + backend::input::{HoverResult, PointerHit}, overlays::screen::{ backend::CaptureType, - capture::{MainThreadWlxCapture, new_wlx_capture}, + capture::{WlxCaptureIn, WlxCaptureOut}, + pw::ScreenCastBackend, }, - state::{AppSession, AppState}, + state::AppState, subsystem::hid::WheelDelta, windowing::{ - OverlaySelector, - backend::{ - FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, - ui_transform, - }, + backend::{FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender}, window::{OverlayCategory, OverlayWindowConfig}, }, }; use super::backend::ScreenBackend; -type PinnedSelectorFuture = core::pin::Pin< - Box>>, ->; - static MIRROR_COUNTER: AtomicUsize = AtomicUsize::new(1); -pub struct MirrorBackend { - name: Arc, - renderer: Option, - selector: Option, - last_extent: [u32; 2], - interaction_transform: Option, -} -impl MirrorBackend { - pub fn new(name: Arc) -> Self { - let selector = Box::pin(pipewire_select_screen(None, false, false, false, false)); - Self { - name, - renderer: None, - selector: Some(selector), - last_extent: [0; 2], - interaction_transform: None, - } - } -} +pub struct MirrorBackend(ScreenBackend); impl OverlayBackend for MirrorBackend { fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> { Ok(()) } fn should_render(&mut self, app: &mut AppState) -> anyhow::Result { - if let Some(mut selector) = self.selector.take() { - let maybe_pw_result = match selector - .poll_unpin(&mut Context::from_waker(futures::task::noop_waker_ref())) - { - Poll::Ready(result) => result, - Poll::Pending => { - self.selector = Some(selector); - return Ok(ShouldRender::Unable); - } - }; - - match maybe_pw_result { - Ok(pw_result) => { - log::debug!( - "{}: PipeWire result streams: {:?}", - self.name, - &pw_result.streams - ); - let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element - log::info!("{}: PipeWire node selected: {}", self.name, node_id); - let capture = new_wlx_capture!( - app.gfx_extras.queue_capture, - PipewireCapture::new(self.name.clone(), node_id) - ); - self.renderer = Some(ScreenBackend::new_raw( - self.name.clone(), - app.xr_backend, - CaptureType::PipeWire, - capture, - )); - app.tasks - .enqueue(TaskType::Overlay(OverlayTask::ToggleOverlay( - OverlaySelector::Name(self.name.clone()), - ToggleMode::EnsureOn, - ))); - } - Err(e) => { - log::warn!("Failed to create mirror due to PipeWire error: {e:?}"); - self.renderer = None; - // drop self - app.tasks - .enqueue(TaskType::Overlay(OverlayTask::Drop(OverlaySelector::Name( - self.name.clone(), - )))); - } - } - } - self.renderer - .as_mut() - .map_or(Ok(ShouldRender::Unable), |r| r.should_render(app)) + self.0.should_render(app) } fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> { - let Some(renderer) = self.renderer.as_mut() else { - anyhow::bail!("render failed after should_render passed"); - }; - - renderer.render(app, rdr)?; - if let Some(meta) = renderer.frame_meta() { - let extent = meta.extent; - if self.last_extent != extent { - self.last_extent = extent; - self.interaction_transform = Some(ui_transform(extent)); - } - } - + self.0.render(app, rdr)?; Ok(()) } fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> { - if let Some(renderer) = self.renderer.as_mut() { - renderer.pause(app)?; - } - Ok(()) + self.0.pause(app) } fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> { - if let Some(renderer) = self.renderer.as_mut() { - renderer.resume(app)?; - } - Ok(()) + self.0.resume(app) } fn frame_meta(&mut self) -> Option { - self.renderer.as_mut().and_then(ScreenBackend::frame_meta) + self.0.frame_meta() } fn notify(&mut self, app: &mut AppState, event_data: OverlayEventData) -> anyhow::Result<()> { - let Some(renderer) = self.renderer.as_mut() else { - return Ok(()); - }; - renderer.notify(app, event_data) + self.0.notify(app, event_data) } fn on_hover(&mut self, _: &mut AppState, _: &PointerHit) -> HoverResult { @@ -168,21 +63,13 @@ impl OverlayBackend for MirrorBackend { fn on_pointer(&mut self, _: &mut AppState, _: &PointerHit, _: bool) {} fn on_scroll(&mut self, _: &mut AppState, _: &PointerHit, _delta: WheelDelta) {} fn get_interaction_transform(&mut self) -> Option { - self.interaction_transform + self.0.get_interaction_transform() } fn get_attrib(&self, attrib: BackendAttrib) -> Option { - if let Some(renderer) = self.renderer.as_ref() { - renderer.get_attrib(attrib) - } else { - None - } + self.0.get_attrib(attrib) } fn set_attrib(&mut self, app: &mut AppState, value: BackendAttribValue) -> bool { - if let Some(renderer) = self.renderer.as_mut() { - renderer.set_attrib(app, value) - } else { - false - } + self.0.set_attrib(app, value) } } @@ -190,8 +77,41 @@ pub fn new_mirror_name() -> Arc { format!("M-{}", MIRROR_COUNTER.fetch_add(1, Ordering::Relaxed)).into() } -pub fn new_mirror(name: Arc, session: &AppSession) -> OverlayWindowConfig { - OverlayWindowConfig { +pub fn new_mirror(name: Arc, app: &mut AppState) -> anyhow::Result { + fn finalize_fn( + name: Arc, + _: Vec2, + _: Vec2, + capture: Box>, + app: &mut AppState, + ) -> Box { + let renderer = + ScreenBackend::new_raw(name.clone(), app.xr_backend, CaptureType::PipeWire, capture); + + let backend = MirrorBackend(renderer); + + Box::new(backend) + } + + let params = ScreenCastParams { + token: None, + embed_mouse: true, + screens_only: false, + persist: false, + allow_multiple: false, + }; + + let backend = ScreenCastBackend::new_raw( + name.clone(), + "".into(), + Vec2::ZERO, + Vec2::ZERO, + params, + app, + finalize_fn, + )?; + + Ok(OverlayWindowConfig { name: name.clone(), category: OverlayCategory::Mirror, show_on_spawn: true, @@ -200,12 +120,12 @@ pub fn new_mirror(name: Arc, session: &AppSession) -> OverlayWindowConfig { grabbable: true, curvature: Some(0.15), transform: Affine3A::from_scale_rotation_translation( - Vec3::ONE * session.config.default_overlay_scale, + Vec3::ONE * app.session.config.default_overlay_scale, Quat::IDENTITY, vec3(0.0, 0.2, -0.35), ), ..OverlayWindowState::default() }, - ..OverlayWindowConfig::from_backend(Box::new(MirrorBackend::new(name))) - } + ..OverlayWindowConfig::from_backend(Box::new(backend)) + }) } diff --git a/wayvr/src/overlays/screen/mod.rs b/wayvr/src/overlays/screen/mod.rs index e1f9d581..cbfe93eb 100644 --- a/wayvr/src/overlays/screen/mod.rs +++ b/wayvr/src/overlays/screen/mod.rs @@ -72,7 +72,7 @@ pub fn create_screens(app: &mut AppState) -> anyhow::Result<(ScreenCreateData, b { if let Some(mut wl) = wlx_capture::wayland::WlxClient::new() { log::info!("Wayland detected."); - return Ok((wl::create_screens_wayland(&mut wl, app), true)); + return Ok((wl::create_screens_wayland(&mut wl, app)?, true)); } log::info!("Wayland not detected, assuming X11."); } diff --git a/wayvr/src/overlays/screen/pw.rs b/wayvr/src/overlays/screen/pw.rs index 1ab14e57..782953d8 100644 --- a/wayvr/src/overlays/screen/pw.rs +++ b/wayvr/src/overlays/screen/pw.rs @@ -1,147 +1,426 @@ -use std::{error::Error, path::PathBuf, task, time::Instant}; +use std::{ + any::Any, + collections::HashMap, + rc::Rc, + sync::Arc, + time::{Duration, Instant}, +}; -use serde::{Deserialize, Serialize}; +use glam::Vec2; +use wgui::{ + event::CallbackDataCommon, i18n::Translation, log::LogErr, parser::Fetchable, + widget::label::WidgetLabel, +}; use wlx_capture::{ WlxCapture, - pipewire::{PipewireCapture, PipewireSelectScreenResult}, + pipewire::{ScreenCastParams, ScreenCastRequestId, ScreenCastResult, capture::PipewireCapture}, wayland::WlxOutput, }; -use wlx_common::{ - config::{PwTokenMap, def_pw_tokens}, - config_io, -}; +use wlx_common::{astr_containers::AStrMapExt, overlays::ToastTopic}; -use crate::{state::AppState, subsystem::dbus::DbusConnector}; +use crate::{ + backend::task::{OverlayTask, TaskType}, + gui::panel::{GuiPanel, NewGuiPanelParams}, + overlays::{ + screen::{ + backend::CaptureType, + capture::{WlxCaptureIn, WlxCaptureOut}, + }, + toast::Toast, + }, + state::{AppState, save_pw_token_config}, + subsystem::dbus::DbusConnector, + windowing::{ + OverlaySelector, + backend::{DummyBackend, OverlayBackend}, + }, +}; use super::{ backend::ScreenBackend, capture::{MainThreadWlxCapture, new_wlx_capture}, }; -#[cfg(feature = "wayland")] -impl ScreenBackend { - pub fn new_pw( +pub(super) type ScreenCastFinalizeFn = fn( + Arc, + Vec2, + Vec2, + Box>, + &mut AppState, +) -> Box; + +pub struct ScreenCastPanelState { + name: Arc, + description: Arc, + logical_pos: Vec2, + logical_size: Vec2, + params: Option, + finalize_fn: ScreenCastFinalizeFn, +} + +pub struct ScreenCastBackend { + panel: GuiPanel, +} + +impl ScreenCastBackend { + #[cfg(feature = "wayland")] + pub fn new_wl( output: &WlxOutput, - token: Option<&str>, + token: Option>, app: &mut AppState, - ) -> anyhow::Result<(Self, Option /* pipewire restore token */)> { - use crate::overlays::screen::backend::CaptureType; + ) -> anyhow::Result { + use glam::vec2; - let name = output.name.clone(); - let embed_mouse = !app.session.config.double_cursor_fix; + fn finalize_fn( + name: Arc, + logical_pos: Vec2, + logical_size: Vec2, + capture: Box>, + app: &mut AppState, + ) -> Box { + use wlx_capture::frame::Transform; - let select_screen_result = select_pw_screen( - &format!( - "Now select: {} {} {} @ {},{}", - &output.name, - &output.make, - &output.model, - &output.logical_pos.0, - &output.logical_pos.1 - ), + let mut backend = + ScreenBackend::new_raw(name, app.xr_backend, CaptureType::PipeWire, capture); + + backend.logical_pos = logical_pos; + backend.logical_size = logical_size; + backend.apply_mouse_transform_with_override(Transform::Undefined); + + Box::new(backend) + } + + let params = ScreenCastParams { token, - embed_mouse, - true, - true, - false, - )?; + embed_mouse: true, + screens_only: true, + persist: true, + allow_multiple: false, + }; - log::debug!( - "{}: PipeWire result streams: {:?}", - output.name, - &select_screen_result.streams - ); + Self::new_raw( + output.name.clone(), + format!( + "{} {} @ {},{}", + output.make, output.model, output.logical_pos.0, output.logical_pos.1 + ) + .into(), + vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32), + vec2(output.logical_size.0 as f32, output.logical_size.1 as f32), + params, + app, + finalize_fn, + ) + } - let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element + pub(super) fn new_raw( + name: Arc, + description: Arc, + logical_pos: Vec2, + logical_size: Vec2, + params: ScreenCastParams, + app: &mut AppState, + finalize_fn: ScreenCastFinalizeFn, + ) -> anyhow::Result { + if app.screencast_manager.is_none() { + anyhow::bail!("xdg-desktop-portal screencasts not supported"); + }; - let capture = new_wlx_capture!( - app.gfx_extras.queue_capture, - PipewireCapture::new(name, node_id) - ); - Ok(( - Self::new_raw( - output.name.clone(), - app.xr_backend, - CaptureType::PipeWire, - capture, - ), - select_screen_result.restore_token, - )) + let panel_params = NewGuiPanelParams { + extra_vars: HashMap::from([ + ("name".into(), name.as_ref().into()), + ("description".into(), description.as_ref().into()), + ]), + ..Default::default() + }; + + let state = ScreenCastPanelState { + name: name.clone(), + description: description.clone(), + logical_pos, + logical_size, + params: Some(params), + finalize_fn, + }; + + let mut panel = + GuiPanel::new_from_template(app, "gui/screencast.xml", state, panel_params)?; + + panel.update_layout(app)?; + + Ok(Self { panel }) } } -#[allow(clippy::fn_params_excessive_bools)] -pub(super) fn select_pw_screen( - instructions: &str, - token: Option<&str>, - embed_mouse: bool, - screens_only: bool, - persist: bool, - multiple: bool, -) -> Result { - use std::time::Duration; - use wlx_capture::pipewire::pipewire_select_screen; +impl OverlayBackend for ScreenCastBackend { + fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> { + if let Some(params) = self.panel.state.params.take() + && let Some(screencast_manager) = app.screencast_manager.as_mut() + { + let request_id = screencast_manager.request(params.clone())?; + check( + self.panel.state.name.clone(), + self.panel.state.description.clone(), + self.panel.state.logical_pos, + self.panel.state.logical_size, + 0, + None, + None, + request_id.clone(), + self.panel.state.finalize_fn, + app, + ); + } - let future = async move { - let print_at = Instant::now() + Duration::from_millis(250); - let mut notify = None; + self.panel.init(app) + } + fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> { + self.panel.pause(app) + } + fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> { + self.panel.resume(app) + } + fn should_render( + &mut self, + app: &mut AppState, + ) -> anyhow::Result { + self.panel.should_render(app) + } + fn render( + &mut self, + app: &mut AppState, + rdr: &mut crate::windowing::backend::RenderResources, + ) -> anyhow::Result<()> { + self.panel.render(app, rdr) + } + fn frame_meta(&mut self) -> Option { + self.panel.frame_meta() + } + fn notify( + &mut self, + app: &mut AppState, + event_data: crate::windowing::backend::OverlayEventData, + ) -> anyhow::Result<()> { + self.panel.notify(app, event_data) + } + fn on_hover( + &mut self, + app: &mut AppState, + hit: &crate::backend::input::PointerHit, + ) -> crate::backend::input::HoverResult { + self.panel.on_hover(app, hit) + } + fn on_left(&mut self, app: &mut AppState, pointer: usize) { + self.panel.on_left(app, pointer); + } + fn on_pointer( + &mut self, + app: &mut AppState, + hit: &crate::backend::input::PointerHit, + pressed: bool, + ) { + self.panel.on_pointer(app, hit, pressed); + } + fn on_scroll( + &mut self, + app: &mut AppState, + hit: &crate::backend::input::PointerHit, + delta: crate::subsystem::hid::WheelDelta, + ) { + self.panel.on_scroll(app, hit, delta); + } + fn get_interaction_transform(&mut self) -> Option { + self.panel.get_interaction_transform() + } + fn get_attrib( + &self, + _attrib: wlx_common::overlays::BackendAttrib, + ) -> Option { + None + } + fn set_attrib( + &mut self, + _app: &mut AppState, + _value: wlx_common::overlays::BackendAttribValue, + ) -> bool { + false + } +} - let f = pipewire_select_screen(token, embed_mouse, screens_only, persist, multiple); - futures::pin_mut!(f); +fn check( + name: Arc, + description: Arc, + logical_pos: Vec2, + logical_size: Vec2, + user_wait: u32, + mut notify_id: Option, + last_result: Option, + request_id: ScreenCastRequestId, + finalize_fn: ScreenCastFinalizeFn, + app: &mut AppState, +) { + const POLL_INTERVAL: Duration = Duration::from_millis(100); - loop { - match futures::poll!(&mut f) { - task::Poll::Ready(result) => return result, - task::Poll::Pending => { - if Instant::now() >= print_at { - log::info!("{instructions}"); - if let Ok(id) = - DbusConnector::notify_send(instructions, "", 1, 30000, 0, true) - { - notify = Some(id); - } - break; - } - futures::future::lazy(|_| { - std::thread::sleep(Duration::from_millis(10)); - }) - .await; + if let Some(screencast_manager) = app.screencast_manager.as_mut() { + let new_result = screencast_manager.check(&request_id); + match new_result { + ScreenCastResult::Ok(ref pw_result) => { + log::debug!( + "{}: PipeWire result streams: {:?}", + name, + &pw_result.streams + ); + let node_id = pw_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element + log::info!("{}: PipeWire node selected: {}", name, node_id); + + if let Some(id) = notify_id.take() { + let _ = DbusConnector::notify_close(id); } + + let pw_tokens_copy = app.session.pw_tokens.clone(); + if let Some(restore_token) = pw_result.restore_token.as_ref() + && app + .session + .pw_tokens + .arc_set(name.clone(), restore_token.clone()) + { + log::info!("Adding Pipewire token for {name}"); + } + if pw_tokens_copy != app.session.pw_tokens { + // Token list changed, re-create token config file + if let Err(err) = save_pw_token_config(app.session.pw_tokens.clone()) { + log::error!("Failed to save Pipewire token config: {err}"); + } + } + app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify( + OverlaySelector::Name(name.clone()), + Box::new(move |app, owc| { + let capture = new_wlx_capture!( + app.gfx_extras.queue_capture, + PipewireCapture::new(name.clone(), node_id) + ); + + owc.backend = finalize_fn(name, logical_pos, logical_size, capture, app); + + let _ = owc + .backend + .init(app) + .log_err("Failed to init ScreenBackend"); + }), + ))); + } + // TODO: display status to user + ScreenCastResult::Queued + | ScreenCastResult::Pending + | ScreenCastResult::WaitingForUser => { + let user_wait_add = if matches!(new_result, ScreenCastResult::WaitingForUser) { + if user_wait == 2 { + notify_id = DbusConnector::notify_send( + "Select screen cast for:", + format!("{name} {description}").as_str(), + 1, + 30000, + 0, + true, + ) + .ok(); + } + 1 + } else { + 0 + }; + + if last_result.is_none_or(|last_result| last_result != new_result) { + update_status_hack(&name, &description, &new_result, app); + } + + app.tasks.enqueue_at( + TaskType::Overlay(OverlayTask::Modify( + OverlaySelector::Name(name.clone()), + Box::new({ + move |app, _owc| { + check( + name.clone(), + description.clone(), + logical_pos, + logical_size, + user_wait + user_wait_add, + notify_id, + Some(new_result), + request_id.clone(), + finalize_fn, + app, + ); + } + }), + )), + Instant::now() + POLL_INTERVAL, + ); + } + ScreenCastResult::Failed(ref e) => { + if let Some(id) = notify_id.take() { + let _ = DbusConnector::notify_close(id); + } + + update_status_hack(&name, &description, &new_result, app); + + log::warn!("Failed to create mirror due to PipeWire error: {e:?}"); + Toast::new( + ToastTopic::Error, + "TOAST.TITLE_SCREENCAST_FAIL".into(), + format!("{e}"), + ) + .submit(app); } } + } +} - let result = f.await; - if let Some(id) = notify { - //safe unwrap; checked above - let _ = DbusConnector::notify_close(id); +fn update_status_hack( + name: &Arc, + description: &Arc, + result: &ScreenCastResult, + app: &mut AppState, +) { + let message = match result { + ScreenCastResult::Queued => "SCREENCAST.QUEUED", + ScreenCastResult::WaitingForUser => { + if description.is_empty() { + "SCREENCAST.WAITING_FOR_USER" + } else { + "SCREENCAST.WAITING_FOR_USER_DESC" + } } - result + ScreenCastResult::Failed(_) => "SCREENCAST.FAIL", + _ => "SCREENCAST.PENDING", // Ok is technically never visible }; - futures::executor::block_on(future) -} - -#[derive(Deserialize, Serialize, Default)] -pub struct TokenConf { - #[serde(default = "def_pw_tokens")] - pub pw_tokens: PwTokenMap, -} - -fn get_pw_token_path() -> PathBuf { - let mut path = config_io::ConfigRoot::Generic.get_conf_d_path(); - path.push("pw_tokens.yaml"); - path -} - -pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box> { - let conf = TokenConf { pw_tokens: tokens }; - let yaml = serde_yaml::to_string(&conf)?; - std::fs::write(get_pw_token_path(), yaml)?; - Ok(()) -} - -pub fn load_pw_token_config() -> Result> { - let yaml = std::fs::read_to_string(get_pw_token_path())?; - let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?; - Ok(conf.pw_tokens) + app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify( + OverlaySelector::Name(name.clone()), + Box::new({ + move |_app, owc| { + let backend = + std::mem::replace(&mut owc.backend, Box::new(DummyBackend {})) as Box; + let mut backend = backend + .downcast::() + .expect("Wrong type to unwrap"); + { + let Ok(mut label) = backend + .panel + .parser_state + .fetch_widget_as::(&backend.panel.layout.state, "status") + .log_err("element with id=\"status\" not found in screencast.xml!") + else { + return; + }; + let mut common = CallbackDataCommon { + state: &backend.panel.layout.state, + alterables: &mut backend.panel.layout.alterables, + }; + label.set_text(&mut common, Translation::from_translation_key(message)); + } + let _ = std::mem::replace(&mut owc.backend, backend); + } + }), + ))); } diff --git a/wayvr/src/overlays/screen/wl.rs b/wayvr/src/overlays/screen/wl.rs index 899fc4be..5c264aec 100644 --- a/wayvr/src/overlays/screen/wl.rs +++ b/wayvr/src/overlays/screen/wl.rs @@ -1,25 +1,23 @@ use glam::vec2; +use wgui::log::LogErr; use wlx_capture::{ WlxCapture, frame::Transform, wayland::{WlxClient, WlxOutput}, wlr_screencopy::WlrScreencopyCapture, }; -use wlx_common::{ - astr_containers::AStrMapExt, - config::{CaptureMethod, PwTokenMap}, -}; +use wlx_common::{astr_containers::AStrMapExt, config::CaptureMethod}; use crate::{ - overlays::screen::{backend::CaptureType, create_screen_from_backend}, + overlays::screen::{backend::CaptureType, create_screen_from_backend, pw::ScreenCastBackend}, state::{AppState, ScreenMeta}, + windowing::backend::OverlayBackend, }; use super::{ ScreenCreateData, backend::ScreenBackend, capture::{MainThreadWlxCapture, new_wlx_capture}, - pw::{load_pw_token_config, save_pw_token_config}, }; impl ScreenBackend { @@ -38,67 +36,50 @@ impl ScreenBackend { } } -#[allow(clippy::useless_let_if_seq)] pub fn create_screen_renderer_wl( output: &WlxOutput, has_wlr_screencopy: bool, - pw_token_store: &mut PwTokenMap, app: &mut AppState, -) -> Option { - let mut capture: Option = None; - +) -> anyhow::Result> { if matches!( app.session.config.capture_method, CaptureMethod::ScreenCopyCpu | CaptureMethod::ScreenCopyGpu | CaptureMethod::Auto ) && has_wlr_screencopy { - log::info!("{}: Using ScreenCopy capture", &output.name); - capture = ScreenBackend::new_wlr_screencopy(output, app); - } - - if capture.is_none() { - log::info!("{}: Using Pipewire capture", &output.name); - - let display_name = &*output.name; - - // Find existing token by display - let token = pw_token_store - .arc_get(display_name) - .map(std::string::String::as_str); - - if let Some(t) = token { - log::info!("Found existing Pipewire token for display {display_name}: {t}"); - } - - match ScreenBackend::new_pw(output, token, app) { - Ok((renderer, restore_token)) => { - capture = Some(renderer); - - if let Some(token) = restore_token - && pw_token_store.arc_set(display_name.into(), token.clone()) - { - log::info!("Adding Pipewire token {token}"); - } - } - Err(e) => { - log::warn!( - "{}: Failed to create Pipewire capture: {:?}", - &output.name, - e - ); - } + if let Some(mut backend) = ScreenBackend::new_wlr_screencopy(output, app) { + log::info!("{}: Using ScreenCopy capture", &output.name); + backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32); + backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32); + backend.apply_mouse_transform_with_override(Transform::Undefined); + return Ok(Box::new(backend)); } } - capture + + log::info!("{}: Using Pipewire capture", &output.name); + let display_name = &*output.name; + + // Find existing token by display + let token = app + .session + .pw_tokens + .arc_get(display_name) + .map(|x| x.to_string().into()); + + if token.is_some() { + log::info!("Found existing Pipewire token for display {display_name}"); + } + + Ok(Box::new( + ScreenCastBackend::new_wl(output, token, app) + .log_err("Failed to create screen with screen cast backend")?, + )) } -pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenCreateData { +pub fn create_screens_wayland( + wl: &mut WlxClient, + app: &mut AppState, +) -> anyhow::Result { let mut screens = vec![]; - - // Load existing Pipewire tokens from file - let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default(); - - let pw_tokens_copy = pw_tokens.clone(); let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some(); for (id, output) in &wl.outputs { @@ -114,35 +95,20 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC output.logical_pos, ); - if let Some(mut backend) = - create_screen_renderer_wl(output, has_wlr_screencopy, &mut pw_tokens, app) - { - backend.logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32); - backend.logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32); - backend.mouse_transform_original = output.transform; - backend.apply_mouse_transform_with_override(Transform::Undefined); + let backend = create_screen_renderer_wl(output, has_wlr_screencopy, app)?; + let window_config = create_screen_from_backend( + output.name.clone(), + output.transform, + &app.session, + backend, + ); - let window_config = create_screen_from_backend( - output.name.clone(), - output.transform, - &app.session, - Box::new(backend), - ); + let meta = ScreenMeta { + name: wl.outputs[id].name.clone(), + native_handle: *id, + }; - let meta = ScreenMeta { - name: wl.outputs[id].name.clone(), - native_handle: *id, - }; - - screens.push((meta, window_config)); - } - } - - if pw_tokens_copy != pw_tokens { - // Token list changed, re-create token config file - if let Err(err) = save_pw_token_config(pw_tokens) { - log::error!("Failed to save Pipewire token config: {err}"); - } + screens.push((meta, window_config)); } let extent = wl.get_desktop_extent(); @@ -155,5 +121,5 @@ pub fn create_screens_wayland(wl: &mut WlxClient, app: &mut AppState) -> ScreenC .inner .set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32)); - ScreenCreateData { screens } + Ok(ScreenCreateData { screens }) } diff --git a/wayvr/src/overlays/screen/x11.rs b/wayvr/src/overlays/screen/x11.rs index f1f40687..bbca9577 100644 --- a/wayvr/src/overlays/screen/x11.rs +++ b/wayvr/src/overlays/screen/x11.rs @@ -7,6 +7,8 @@ use wlx_capture::{ xshm::{XshmCapture, XshmScreen}, }; +#[cfg(feature = "pipewire")] +use crate::state::save_pw_token_config; use crate::{ overlays::screen::{backend::CaptureType, create_screen_from_backend}, state::{AppState, ScreenMeta}, @@ -19,7 +21,9 @@ use super::{ }; #[cfg(feature = "pipewire")] -use wlx_capture::pipewire::PipewireStream; +use wlx_capture::pipewire::{ + PipewireStream, ScreenCastParams, capture::PipewireCapture, screen_cast_select_blocking, +}; impl ScreenBackend { pub fn new_xshm(screen: Arc, app: &AppState) -> Self { @@ -39,42 +43,41 @@ impl ScreenBackend { #[cfg(feature = "pipewire")] pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result { use glam::vec2; - use wlx_capture::{pipewire::PipewireCapture, xshm::xshm_get_monitors}; - use wlx_common::{astr_containers::AStrMapExt, config::PwTokenMap}; + use wlx_capture::xshm::xshm_get_monitors; + use wlx_common::astr_containers::AStrMapExt; - use crate::{ - overlays::screen::{ - create_screen_from_backend, - pw::{load_pw_token_config, save_pw_token_config, select_pw_screen}, - }, - state::ScreenMeta, - }; + use crate::{overlays::screen::create_screen_from_backend, state::ScreenMeta}; use super::ScreenCreateData; - // Load existing Pipewire tokens from file - let mut pw_tokens: PwTokenMap = load_pw_token_config().unwrap_or_default(); - let pw_tokens_copy = pw_tokens.clone(); - let token = pw_tokens.arc_get("x11").map(std::string::String::as_str); - let embed_mouse = !app.session.config.double_cursor_fix; + let pw_tokens_copy = app.session.pw_tokens.clone(); - let select_screen_result = select_pw_screen( - "Select ALL screens on the screencast pop-up!", - token, - embed_mouse, - true, - true, - true, - )?; + let params = ScreenCastParams { + token: app + .session + .pw_tokens + .arc_get("x11") + .map(|x| x.to_string().into()), + embed_mouse: !app.session.config.double_cursor_fix, + allow_multiple: true, + persist: true, + screens_only: true, + }; + + // this one still blocks + let select_screen_result = screen_cast_select_blocking(params)?; if let Some(restore_token) = select_screen_result.restore_token - && pw_tokens.arc_set("x11".into(), restore_token.clone()) + && app + .session + .pw_tokens + .arc_set("x11".into(), restore_token.clone()) { log::info!("Adding Pipewire token {restore_token}"); } - if pw_tokens_copy != pw_tokens { + if pw_tokens_copy != app.session.pw_tokens { // Token list changed, re-create token config file - if let Err(err) = save_pw_token_config(pw_tokens) { + if let Err(err) = save_pw_token_config(app.session.pw_tokens.clone()) { log::error!("Failed to save Pipewire token config: {err}"); } } diff --git a/wayvr/src/state.rs b/wayvr/src/state.rs index c8aac088..8e4dc085 100644 --- a/wayvr/src/state.rs +++ b/wayvr/src/state.rs @@ -1,6 +1,9 @@ use glam::Affine3A; use idmap::IdMap; +#[cfg(feature = "pipewire")] +use serde::{Deserialize, Serialize}; use smallvec::{SmallVec, smallvec}; +use std::path::PathBuf; use std::rc::Rc; use std::sync::Arc; use wgui::log::LogErr; @@ -9,6 +12,9 @@ use wgui::{ drawing, font_config::WguiFontConfig, gfx::WGfx, globals::WguiGlobals, parser::parse_color_hex, renderer_vk::context::SharedContext as WSharedContext, }; +#[cfg(feature = "pipewire")] +use wlx_capture::pipewire::ScreenCastManager; +use wlx_common::config::PwTokenMap; use wlx_common::locale::WayVRLangProvider; use wlx_common::{ audio, @@ -80,6 +86,9 @@ pub struct AppState { #[cfg(feature = "openxr")] pub monado_state: Option, + #[cfg(feature = "pipewire")] + pub screencast_manager: Option, + pub delta_time: f32, } @@ -165,6 +174,14 @@ impl AppState { let lang_provider = WayVRLangProvider::from_config(&session.config); + #[cfg(feature = "pipewire")] + let screencast_manager = ScreenCastManager::new() + .log_err( + // would only fail if session D-bus is unreachable + "Could not initialize ScreenCastManager. PipeWire screen capture will not work. Check your D-bus setup.", + ) + .ok(); + let mut app_state = Self { session, tasks, @@ -202,6 +219,9 @@ impl AppState { #[cfg(feature = "openxr")] monado_state: None, + #[cfg(feature = "pipewire")] + screencast_manager, + delta_time: 1.0 / 120.0, }; @@ -254,6 +274,9 @@ pub struct AppSession { pub config: GeneralConfig, pub config_dirty: bool, + #[cfg(feature = "pipewire")] + pub pw_tokens: PwTokenMap, + pub no_autostart: bool, pub toast_topics: IdMap, @@ -275,11 +298,18 @@ impl AppSession { toast_topics.insert(*k, *v); }); + #[cfg(feature = "pipewire")] + let pw_tokens = load_pw_token_config() + .log_err("Could not load PipeWire tokens") + .unwrap_or_default(); + Self { config, toast_topics, no_autostart: false, config_dirty: false, + #[cfg(feature = "pipewire")] + pw_tokens, } } } @@ -289,3 +319,31 @@ pub struct ScreenMeta { #[allow(dead_code)] pub native_handle: u32, } + +#[cfg(feature = "pipewire")] +#[derive(Deserialize, Serialize, Default)] +struct TokenConf { + pub pw_tokens: PwTokenMap, +} + +#[cfg(feature = "pipewire")] +fn get_pw_token_path() -> PathBuf { + let mut path = config_io::ConfigRoot::Generic.get_conf_d_path(); + path.push("pw_tokens.yaml"); + path +} + +#[cfg(feature = "pipewire")] +pub fn save_pw_token_config(tokens: PwTokenMap) -> anyhow::Result<()> { + let conf = TokenConf { pw_tokens: tokens }; + let yaml = serde_yaml::to_string(&conf)?; + std::fs::write(get_pw_token_path(), yaml)?; + Ok(()) +} + +#[cfg(feature = "pipewire")] +pub fn load_pw_token_config() -> anyhow::Result { + let yaml = std::fs::read_to_string(get_pw_token_path())?; + let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?; + Ok(conf.pw_tokens) +} diff --git a/wgui/src/parser/mod.rs b/wgui/src/parser/mod.rs index 5ffc00f0..4cb8859b 100644 --- a/wgui/src/parser/mod.rs +++ b/wgui/src/parser/mod.rs @@ -523,6 +523,12 @@ impl ParserContext<'_> { } } + fn populate_extra_variables(&mut self, other: &HashMap, Rc>) { + for (k,v) in other.iter() { + self.data_local.var_map.insert(k.clone(), v.clone()); + } + } + fn populate_theme_variables(&mut self) { let theme = self.layout.state.theme.clone(); @@ -1275,6 +1281,7 @@ pub type OnCustomAttribsFunc = Rc; pub struct ParseDocumentExtra { pub on_custom_attribs: Option, // all attributes with '_' character prepended pub dev_mode: bool, + pub extra_vars: HashMap, Rc>, } // filled-in by you in `new_layout_from_assets` function @@ -1292,6 +1299,7 @@ pub fn parse_from_assets( let parser_data = ParserData::default(); let mut ctx = create_default_context(doc_params, layout, &parser_data); ctx.populate_theme_variables(); + ctx.populate_extra_variables(&doc_params.extra.extra_vars); let (file, node_layout) = get_doc_from_asset_path(&ctx, doc_params.path)?; parse_document_root(&file, &mut ctx, parent_id, node_layout)?; diff --git a/wlx-capture/Cargo.toml b/wlx-capture/Cargo.toml index b338f85d..da9a0411 100644 --- a/wlx-capture/Cargo.toml +++ b/wlx-capture/Cargo.toml @@ -5,44 +5,50 @@ edition = "2024" authors = ["galister"] description = "Screen capture library for Wayland and X11" repository = "https://github.com/galister/wlx-capture" -keywords = ["desktop", "capture", "pipewire", "wayland", "x11"] +keywords = ["capture", "desktop", "pipewire", "wayland", "x11"] categories = ["computer-vision", "multimedia::images"] documentation = "https://docs.rs/wlx-capture" license = "GPL-3.0-only" +[dependencies] +dbus = { workspace = true, features = ["futures"], optional = true } +idmap = { workspace = true } +log = { workspace = true } +pipewire = { workspace = true, optional = true } +slotmap.workspace = true +wayland-client = { workspace = true, optional = true } +wayland-protocols = { + workspace = true, + features = [ + "client", + "staging", + "unstable", + "wayland-client", + ], + optional = true +} + +drm-fourcc = "2.2.0" +libc = "0.2.178" +rxscreen = { + version = "0.1.7", + features = [ + "mouse", + "shm", + "xrandr", + ], + optional = true +} +smithay-client-toolkit = { version = "0.20.0", optional = true } + [features] -default = ["wayland", "pipewire"] +default = ["pipewire", "wayland"] egl = [] -wlr = ["wayland"] -pipewire = ["wayland", "dep:ashpd", "dep:pipewire"] +pipewire = ["dep:dbus", "dep:pipewire", "wayland"] wayland = [ "dep:smithay-client-toolkit", "dep:wayland-client", "dep:wayland-protocols", ] +wlr = ["wayland"] xshm = ["dep:rxscreen"] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -ashpd = { version = "0.12.0", default-features = false, features = [ - "async-std", -], optional = true } -drm-fourcc = "2.2.0" -idmap = { workspace = true } -libc = "0.2.178" -log = { workspace = true } -pipewire = { workspace = true, optional = true } -rxscreen = { version = "0.1.7", features = [ - "shm", - "xrandr", - "mouse", -], optional = true } -smithay-client-toolkit = { version = "0.20.0", optional = true } -wayland-client = { workspace = true, optional = true } -wayland-protocols = { version = "0.32.9", features = [ - "wayland-client", - "client", - "staging", - "unstable", -], optional = true } diff --git a/wlx-capture/src/pipewire.rs b/wlx-capture/src/pipewire/capture.rs similarity index 89% rename from wlx-capture/src/pipewire.rs rename to wlx-capture/src/pipewire/capture.rs index f3e5cdd5..cb588c81 100644 --- a/wlx-capture/src/pipewire.rs +++ b/wlx-capture/src/pipewire/capture.rs @@ -1,18 +1,9 @@ use std::any::Any; use std::collections::HashMap; use std::sync::Arc; -use std::sync::atomic::AtomicU32; -use std::sync::atomic::Ordering; use std::sync::mpsc; use std::thread::JoinHandle; -use ashpd::desktop::{ - PersistMode, - screencast::{CursorMode, Screencast, SourceType}, -}; - -pub use ashpd::Error as AshpdError; - use drm_fourcc::DrmFormat; use drm_fourcc::DrmFourcc; use drm_fourcc::DrmModifier; @@ -46,93 +37,6 @@ use crate::frame::Transform; use crate::frame::WlxFrame; use crate::frame::{DmabufFrame, FramePlane, MemFdFrame, MemPtrFrame}; -#[derive(Debug, Clone)] -pub struct PipewireStream { - pub node_id: u32, - pub position: Option<(i32, i32)>, - pub size: Option<(i32, i32)>, -} - -#[derive(Debug, Clone)] -pub struct PipewireSelectScreenResult { - pub streams: Vec, - pub restore_token: Option, -} - -pub async fn pipewire_select_screen( - token: Option<&str>, - embed_mouse: bool, - screens_only: bool, - persist: bool, - multiple: bool, -) -> Result { - static CURSOR_MODES: AtomicU32 = AtomicU32::new(0); - - let proxy = Screencast::new().await?; - let session = proxy.create_session().await?; - - let mut cursor_modes = CURSOR_MODES.load(Ordering::Relaxed); - if cursor_modes == 0 { - cursor_modes = proxy.get_property::("AvailableCursorModes").await?; - - log::debug!("Available cursor modes: {cursor_modes:#x}"); - - // properly will be same system-wide, so race condition not a concern - CURSOR_MODES.store(cursor_modes, Ordering::Relaxed); - } - - let cursor_mode = match embed_mouse { - true if cursor_modes & (CursorMode::Embedded as u32) != 0 => CursorMode::Embedded, - _ if cursor_modes & (CursorMode::Metadata as u32) != 0 => CursorMode::Metadata, - _ => CursorMode::Hidden, - }; - - log::debug!("Selected cursor mode: {cursor_mode:?}"); - - let source_type = if screens_only { - SourceType::Monitor.into() - } else { - SourceType::Monitor | SourceType::Window | SourceType::Virtual - }; - - let persist_mode = if persist { - PersistMode::ExplicitlyRevoked - } else { - PersistMode::DoNot - }; - - proxy - .select_sources( - &session, - cursor_mode, - source_type, - multiple, - token, - persist_mode, - ) - .await?; - - let response = proxy.start(&session, None).await?.response()?; - - let streams: Vec<_> = response - .streams() - .iter() - .map(|stream| PipewireStream { - node_id: stream.pipe_wire_node_id(), - position: stream.position(), - size: stream.size(), - }) - .collect(); - if !streams.is_empty() { - return Ok(PipewireSelectScreenResult { - streams, - restore_token: response.restore_token().map(String::from), - }); - } - - Err(ashpd::Error::NoResponse) -} - #[derive(Default)] struct StreamData { format: Option, diff --git a/wlx-capture/src/pipewire/dbus_screencast.rs b/wlx-capture/src/pipewire/dbus_screencast.rs new file mode 100644 index 00000000..a01dd516 --- /dev/null +++ b/wlx-capture/src/pipewire/dbus_screencast.rs @@ -0,0 +1,50 @@ +// This code was autogenerated with `dbus-codegen-rust -g -m None --client nonblock`, see https://github.com/diwic/dbus-rs +use dbus as dbus; +#[allow(unused_imports)] +use dbus::arg; +use dbus::nonblock; + +pub trait OrgFreedesktopPortalScreenCast { + fn create_session(&self, options: arg::PropMap) -> nonblock::MethodReply>; + fn select_sources(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply>; + fn start(&self, session_handle: dbus::Path, parent_window: &str, options: arg::PropMap) -> nonblock::MethodReply>; + fn open_pipe_wire_remote(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply; + fn available_source_types(&self) -> nonblock::MethodReply; + fn available_cursor_modes(&self) -> nonblock::MethodReply; + fn version(&self) -> nonblock::MethodReply; +} + +impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref> OrgFreedesktopPortalScreenCast for nonblock::Proxy<'a, C> { + + fn create_session(&self, options: arg::PropMap) -> nonblock::MethodReply> { + self.method_call("org.freedesktop.portal.ScreenCast", "CreateSession", (options, )) + .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, )) + } + + fn select_sources(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply> { + self.method_call("org.freedesktop.portal.ScreenCast", "SelectSources", (session_handle, options, )) + .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, )) + } + + fn start(&self, session_handle: dbus::Path, parent_window: &str, options: arg::PropMap) -> nonblock::MethodReply> { + self.method_call("org.freedesktop.portal.ScreenCast", "Start", (session_handle, parent_window, options, )) + .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, )) + } + + fn open_pipe_wire_remote(&self, session_handle: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply { + self.method_call("org.freedesktop.portal.ScreenCast", "OpenPipeWireRemote", (session_handle, options, )) + .and_then(|r: (arg::OwnedFd, )| Ok(r.0, )) + } + + fn available_source_types(&self) -> nonblock::MethodReply { + ::get(self, "org.freedesktop.portal.ScreenCast", "AvailableSourceTypes") + } + + fn available_cursor_modes(&self) -> nonblock::MethodReply { + ::get(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes") + } + + fn version(&self) -> nonblock::MethodReply { + ::get(self, "org.freedesktop.portal.ScreenCast", "version") + } +} diff --git a/wlx-capture/src/pipewire/mod.rs b/wlx-capture/src/pipewire/mod.rs new file mode 100644 index 00000000..19949f20 --- /dev/null +++ b/wlx-capture/src/pipewire/mod.rs @@ -0,0 +1,905 @@ +use std::{ + collections::{HashMap, VecDeque}, + error::Error as StdError, + fmt, + future::Future, + pin::Pin, + rc::Rc, + sync::{Arc, Mutex, MutexGuard}, + task::{Context, Poll, Wake, Waker}, + time::Duration, +}; + +use dbus::{ + arg::{self, RefArg}, + channel::{BusType, Channel}, + message::{MatchRule, Message}, + nonblock::{self, MsgMatch, Process, Proxy, SyncConnection}, +}; +use slotmap::SlotMap; + +use dbus_screencast::OrgFreedesktopPortalScreenCast; + +slotmap::new_key_type! { + pub struct ScreenCastRequestId; +} + +pub mod capture; +mod dbus_screencast; + +#[derive(Debug, Clone)] +pub struct PipewireStream { + pub node_id: u32, + pub position: Option<(i32, i32)>, + pub size: Option<(i32, i32)>, +} + +#[derive(Debug, Clone)] +pub struct ScreenCastResponse { + pub streams: Vec, + pub restore_token: Option, +} + +#[derive(Default, Debug, Clone)] +pub struct ScreenCastParams { + /// Optional restore token. + pub token: Option>, + + /// If true, use the first available: EMBEDDED, METADATA/FALLBACK, HIDDEN. + pub embed_mouse: bool, + + /// false: only request MONITOR + /// true: request MONITOR, WINDOW, VIRTUAL + pub screens_only: bool, + + /// true: EXPLICITLY_REVOKED + /// false: DO_NOT + pub persist: bool, + + /// Allow the user to select multiple sources. + pub allow_multiple: bool, +} + +#[derive(Debug, Clone)] +pub enum ScreenCastResult { + Ok(ScreenCastResponse), + Queued, + Pending, + WaitingForUser, + Failed(ScreenCastError), +} + +impl PartialEq for ScreenCastResult { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Ok(_), Self::Ok(_)) => true, + (Self::Queued, Self::Queued) => true, + (Self::Pending, Self::Pending) => true, + (Self::WaitingForUser, Self::WaitingForUser) => true, + (Self::Failed(_), Self::Failed(_)) => true, + _ => false, + } + } + fn ne(&self, other: &Self) -> bool { + !self.eq(other) + } +} + +#[derive(Debug, Clone)] +pub enum ScreenCastError { + Dbus(String), + DbusDisconnected, + InvalidObjectPath(String), + UnknownRequest, + UnsupportedSourceTypes { requested: u32, available: u32 }, + UnsupportedCursorMode { available: u32 }, + PortalResponse(u32), + MissingField(&'static str), + InvalidResponse(&'static str), +} + +impl fmt::Display for ScreenCastError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Dbus(e) => write!(f, "D-Bus error: {e}"), + Self::DbusDisconnected => write!(f, "D-Bus connection is disconnected"), + Self::InvalidObjectPath(e) => write!(f, "invalid D-Bus object path: {e}"), + Self::UnknownRequest => write!(f, "unknown screen select request"), + Self::UnsupportedSourceTypes { + requested, + available, + } => { + write!( + f, + "unsupported source types: requested={requested:#x}, available={available:#x}" + ) + } + Self::UnsupportedCursorMode { available } => { + write!(f, "unsupported cursor mode: available={available:#x}") + } + Self::PortalResponse(code) => write!(f, "portal returned response code {code}"), + Self::MissingField(name) => write!(f, "portal response missing field {name}"), + Self::InvalidResponse(what) => write!(f, "invalid portal response: {what}"), + } + } +} + +impl StdError for ScreenCastError {} + +impl From for ScreenCastError { + fn from(value: dbus::Error) -> Self { + Self::Dbus(value.to_string()) + } +} + +/// manages pipewire screen requests +pub struct ScreenCastManager { + conn: Arc, + timeout: Duration, + sender_component: String, + + source_types: OneShot, + cursor_modes: OneShot, + + requests: SlotMap, + queue: VecDeque, + active: Option, + + responses: Arc>>>, + cleanup: Vec>, + + next_token: u64, +} + +impl ScreenCastManager { + pub fn new() -> Result { + let channel = Channel::get_private(BusType::Session)?; + let conn = Arc::new(SyncConnection::from(channel)); + + let timeout = Duration::from_secs(120); + let proxy = portal_proxy(conn.clone(), timeout); + + let sender_component = sender_path_component(&format!("{}", conn.unique_name())); + + Ok(Self { + conn, + timeout, + sender_component, + + source_types: OneShot::Pending(proxy.available_source_types()), + cursor_modes: OneShot::Pending(proxy.available_cursor_modes()), + + requests: SlotMap::with_key(), + queue: VecDeque::new(), + active: None, + + responses: Arc::new(Mutex::new(HashMap::new())), + cleanup: Vec::new(), + + next_token: 0, + }) + } + + pub fn request( + &mut self, + params: ScreenCastParams, + ) -> Result { + let id = self.requests.insert(RequestEntry { + params, + state: RequestState::Queued, + }); + + self.queue.push_back(id); + Ok(id) + } + + pub fn check(&mut self, request_id: &ScreenCastRequestId) -> ScreenCastResult { + if !self.requests.contains_key(*request_id) { + return ScreenCastResult::Failed(ScreenCastError::UnknownRequest); + } + + if let Err(e) = self.pump_dbus() { + return ScreenCastResult::Failed(e); + } + + self.poll_cleanup(); + self.poll_globals(); + + self.ensure_active(); + + // drive several immediate transitions, but never spin forever + for _ in 0..12 { + if !self.drive_active_once() { + break; + } + + self.ensure_active(); + } + + let result = self.result_for(*request_id); + + if is_terminal(&result) { + let _ = self.requests.remove(*request_id); + + if self.active == Some(*request_id) { + self.active = None; + } + + self.queue.retain(|id| id != request_id); + } + + result + } + + fn pump_dbus(&self) -> Result<(), ScreenCastError> { + let channel: &Channel = self.conn.as_ref().as_ref(); + channel + .read_write(Some(Duration::ZERO)) + .map_err(|_| ScreenCastError::DbusDisconnected)?; + + self.conn.process_all(); + Ok(()) + } + + fn poll_cleanup(&mut self) { + let mut i = 0; + while i < self.cleanup.len() { + match poll_boxed(&mut self.cleanup[i]) { + Poll::Ready(_) => { + let _ = self.cleanup.swap_remove(i); + } + Poll::Pending => i += 1, + } + } + } + + fn poll_globals(&mut self) { + poll_one_shot(&mut self.source_types); + poll_one_shot(&mut self.cursor_modes); + } + + fn globals(&self) -> Result, ScreenCastError> { + match (&self.source_types, &self.cursor_modes) { + (OneShot::Ready(source_types), OneShot::Ready(cursor_modes)) => Ok(Some(Globals { + source_types: *source_types, + cursor_modes: *cursor_modes, + })), + (OneShot::Failed(e), _) | (_, OneShot::Failed(e)) => Err(e.clone()), + _ => Ok(None), + } + } + + fn ensure_active(&mut self) { + if self.active.is_some() { + return; + } + + while let Some(id) = self.queue.pop_front() { + let Some(entry) = self.requests.get_mut(id) else { + continue; + }; + + if matches!(entry.state, RequestState::Queued) { + entry.state = RequestState::Init; + self.active = Some(id); + return; + } + } + } + + fn drive_active_once(&mut self) -> bool { + let Some(id) = self.active else { + return false; + }; + + let Some(params) = self.requests.get(id).map(|r| r.params.clone()) else { + self.active = None; + return true; + }; + + let state = match self.requests.get_mut(id) { + Some(entry) => std::mem::replace(&mut entry.state, RequestState::Queued), + None => { + self.active = None; + return true; + } + }; + + let next_state = match self.advance_state(params, state) { + Ok(state) => state, + Err(error) => RequestState::Terminal(ScreenCastResult::Failed(error)), + }; + + let progressed = !matches!(next_state, RequestState::Queued); + + let terminal = matches!(next_state, RequestState::Terminal(_)); + + if let Some(entry) = self.requests.get_mut(id) { + entry.state = next_state; + } else { + self.active = None; + return true; + } + + if terminal { + self.active = None; + } + + progressed + } + + fn advance_state( + &mut self, + params: ScreenCastParams, + state: RequestState, + ) -> Result { + match state { + RequestState::Queued => Ok(RequestState::Queued), + + RequestState::Init => { + if self.globals()?.is_none() { + return Ok(RequestState::Init); + } + + let session_token = self.next_token("pw_session"); + self.begin_step(PortalStep::CreateSession { session_token }) + } + + RequestState::AddMatch { + step, + handle_token, + request_path, + mut future, + } => match poll_boxed(&mut future) { + Poll::Pending => Ok(RequestState::AddMatch { + step, + handle_token, + request_path, + future, + }), + Poll::Ready(Err(e)) => Err(e.into()), + Poll::Ready(Ok(match_handle)) => { + let reply = self.call_step(¶ms, &step, &handle_token)?; + Ok(RequestState::MethodCall { + step, + request_path, + match_handle, + reply, + }) + } + }, + + RequestState::MethodCall { + step, + request_path, + match_handle, + mut reply, + } => match poll_unpin(&mut reply) { + Poll::Pending => Ok(RequestState::MethodCall { + step, + request_path, + match_handle, + reply, + }), + Poll::Ready(Err(e)) => { + self.schedule_remove_match(match_handle); + Err(e.into()) + } + Poll::Ready(Ok(_handle)) => Ok(RequestState::WaitResponse { + step, + request_path, + match_handle, + }), + }, + + RequestState::WaitResponse { + step, + request_path, + match_handle, + } => { + let Some(response) = self.take_response(&request_path) else { + return Ok(RequestState::WaitResponse { + step, + request_path, + match_handle, + }); + }; + + self.schedule_remove_match(match_handle); + + let response = response?; + + if response.response != 0 { + return Ok(RequestState::Terminal(ScreenCastResult::Failed( + ScreenCastError::PortalResponse(response.response), + ))); + } + + match step { + PortalStep::CreateSession { .. } => { + let session_handle = required_string(&response.results, "session_handle")?; + let session_handle = dbus::Path::new(session_handle) + .map_err(ScreenCastError::InvalidObjectPath)?; + + self.begin_step(PortalStep::SelectSources { session_handle }) + } + + PortalStep::SelectSources { session_handle } => { + self.begin_step(PortalStep::Start { session_handle }) + } + + PortalStep::Start { .. } => { + let response = parse_start_response(&response.results)?; + Ok(RequestState::Terminal(ScreenCastResult::Ok(response))) + } + } + } + + RequestState::Terminal(result) => Ok(RequestState::Terminal(result)), + } + } + + fn begin_step(&mut self, step: PortalStep) -> Result { + let handle_token = self.next_token("pw_request"); + let request_path = self.request_path(&handle_token)?; + + let future = make_response_match( + self.conn.clone(), + self.responses.clone(), + request_path.clone(), + )?; + + Ok(RequestState::AddMatch { + step, + handle_token, + request_path, + future, + }) + } + + fn call_step( + &self, + params: &ScreenCastParams, + step: &PortalStep, + handle_token: &str, + ) -> Result>, ScreenCastError> { + let proxy = portal_proxy(self.conn.clone(), self.timeout); + + match step { + PortalStep::CreateSession { session_token } => { + let mut options = arg::PropMap::new(); + prop_insert(&mut options, "handle_token", handle_token.to_owned()); + prop_insert( + &mut options, + "session_handle_token", + session_token.to_owned(), + ); + + Ok(proxy.create_session(options)) + } + + PortalStep::SelectSources { session_handle } => { + let globals = self.globals()?.ok_or(ScreenCastError::InvalidResponse( + "global portal properties not ready", + ))?; + + let requested_types = if params.screens_only { + SOURCE_MONITOR | SOURCE_WINDOW | SOURCE_VIRTUAL + } else { + SOURCE_MONITOR + }; + + let source_types = requested_types & globals.source_types; + if source_types == 0 { + return Err(ScreenCastError::UnsupportedSourceTypes { + requested: requested_types, + available: globals.source_types, + }); + } + + let cursor_mode = choose_cursor_mode(globals.cursor_modes, params.embed_mouse) + .ok_or(ScreenCastError::UnsupportedCursorMode { + available: globals.cursor_modes, + })?; + + let mut options = arg::PropMap::new(); + prop_insert(&mut options, "handle_token", handle_token.to_owned()); + prop_insert(&mut options, "types", source_types); + prop_insert(&mut options, "multiple", params.allow_multiple); + prop_insert(&mut options, "cursor_mode", cursor_mode); + prop_insert( + &mut options, + "persist_mode", + if params.persist { 2u32 } else { 0u32 }, + ); + + if let Some(token) = ¶ms.token { + prop_insert(&mut options, "restore_token", token.to_string()); + } + + Ok(proxy.select_sources(session_handle.clone(), options)) + } + + PortalStep::Start { session_handle } => { + let mut options = arg::PropMap::new(); + prop_insert(&mut options, "handle_token", handle_token.to_owned()); + + Ok(proxy.start(session_handle.clone(), "", options)) + } + } + } + + fn request_path(&self, handle_token: &str) -> Result { + let path = format!( + "/org/freedesktop/portal/desktop/request/{}/{}", + self.sender_component, handle_token + ); + + let _ = dbus::Path::new(path.clone()).map_err(ScreenCastError::InvalidObjectPath)?; + + Ok(path) + } + + fn next_token(&mut self, prefix: &str) -> String { + self.next_token = self.next_token.saturating_add(1); + format!("{prefix}_{}", self.next_token) + } + + fn take_response(&self, request_path: &str) -> Option> { + lock_response_map(&self.responses).remove(request_path) + } + + fn schedule_remove_match(&mut self, match_handle: MsgMatch) { + let token = match_handle.token(); + let conn = self.conn.clone(); + + self.cleanup + .push(Box::pin(async move { conn.remove_match(token).await })); + } + + fn result_for(&self, id: ScreenCastRequestId) -> ScreenCastResult { + let Some(entry) = self.requests.get(id) else { + return ScreenCastResult::Failed(ScreenCastError::UnknownRequest); + }; + + match &entry.state { + RequestState::Queued => ScreenCastResult::Queued, + RequestState::Init + | RequestState::AddMatch { .. } + | RequestState::MethodCall { .. } => ScreenCastResult::Pending, + RequestState::WaitResponse { step, .. } => match step { + PortalStep::Start { .. } => ScreenCastResult::WaitingForUser, + _ => ScreenCastResult::Pending, + }, + RequestState::Terminal(result) => result.clone(), + } + } +} + +struct RequestEntry { + params: ScreenCastParams, + state: RequestState, +} + +enum RequestState { + Queued, + Init, + AddMatch { + step: PortalStep, + handle_token: String, + request_path: String, + future: DbusFuture, + }, + MethodCall { + step: PortalStep, + request_path: String, + match_handle: MsgMatch, + reply: nonblock::MethodReply>, + }, + WaitResponse { + step: PortalStep, + request_path: String, + match_handle: MsgMatch, + }, + Terminal(ScreenCastResult), +} + +#[derive(Clone)] +enum PortalStep { + CreateSession { session_token: String }, + SelectSources { session_handle: dbus::Path<'static> }, + Start { session_handle: dbus::Path<'static> }, +} + +#[derive(Debug, Clone, Copy)] +struct Globals { + source_types: u32, + cursor_modes: u32, +} + +enum OneShot { + Pending(nonblock::MethodReply), + Ready(T), + Failed(ScreenCastError), +} + +#[derive(Debug)] +struct PortalResponse { + response: u32, + results: arg::PropMap, +} + +type DbusFuture = Pin>>>; + +const PORTAL_DEST: &str = "org.freedesktop.portal.Desktop"; +const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop"; +const REQUEST_IFACE: &str = "org.freedesktop.portal.Request"; + +const SOURCE_MONITOR: u32 = 1; +const SOURCE_WINDOW: u32 = 2; +const SOURCE_VIRTUAL: u32 = 4; + +const CURSOR_HIDDEN: u32 = 1; +const CURSOR_EMBEDDED: u32 = 2; +const CURSOR_METADATA: u32 = 4; + +fn portal_proxy( + conn: Arc, + timeout: Duration, +) -> Proxy<'static, Arc> { + Proxy::new(PORTAL_DEST, PORTAL_PATH, timeout, conn) +} + +fn make_response_match( + conn: Arc, + responses: Arc>>>, + request_path: String, +) -> Result, ScreenCastError> { + let path = dbus::Path::new(request_path.clone()).map_err(ScreenCastError::InvalidObjectPath)?; + + let rule = MatchRule::new_signal(REQUEST_IFACE, "Response") + .with_sender(PORTAL_DEST) + .with_path(path); + + Ok(Box::pin(async move { + let match_handle = conn.add_match(rule).await?; + + let key = request_path.clone(); + let responses = responses.clone(); + + Ok(match_handle.msg_cb(move |msg: Message| { + let parsed = parse_portal_response_message(&msg); + lock_response_map(&responses).insert(key.clone(), parsed); + false + })) + })) +} + +fn parse_portal_response_message(msg: &Message) -> Result { + let (response, results): (u32, arg::PropMap) = msg + .read2() + .map_err(|_| ScreenCastError::InvalidResponse("Request::Response signal"))?; + + Ok(PortalResponse { response, results }) +} + +fn parse_start_response(results: &arg::PropMap) -> Result { + Ok(ScreenCastResponse { + streams: parse_streams(results)?, + restore_token: optional_string(results, "restore_token"), + }) +} + +fn parse_streams(results: &arg::PropMap) -> Result, ScreenCastError> { + let streams = required_arg(results, "streams")?; + let streams = unvariant(streams); + + let iter = streams + .as_iter() + .ok_or(ScreenCastError::InvalidResponse("streams array"))?; + + let mut out = Vec::new(); + + for stream in iter { + let mut fields = stream + .as_iter() + .ok_or(ScreenCastError::InvalidResponse("stream tuple"))?; + + let node_id = fields + .next() + .and_then(|v| v.as_u64()) + .and_then(|v| u32::try_from(v).ok()) + .ok_or(ScreenCastError::InvalidResponse("stream node id"))?; + + let props = fields + .next() + .ok_or(ScreenCastError::InvalidResponse("stream properties"))?; + + let position = dict_get(props, "position").and_then(tuple_i32); + let size = dict_get(props, "size").and_then(tuple_i32); + + out.push(PipewireStream { + node_id, + position, + size, + }); + } + + if out.is_empty() { + return Err(ScreenCastError::InvalidResponse("empty streams array")); + } + + Ok(out) +} + +fn required_string(map: &arg::PropMap, key: &'static str) -> Result { + optional_string(map, key).ok_or(ScreenCastError::MissingField(key)) +} + +fn optional_string(map: &arg::PropMap, key: &str) -> Option { + map.get(key) + .and_then(|v| unvariant(&*v.0).as_str()) + .map(str::to_owned) +} + +fn required_arg<'a>( + map: &'a arg::PropMap, + key: &'static str, +) -> Result<&'a dyn RefArg, ScreenCastError> { + map.get(key) + .map(|v| &*v.0 as &dyn RefArg) + .ok_or(ScreenCastError::MissingField(key)) +} + +fn dict_get<'a>(dict: &'a dyn RefArg, key: &str) -> Option<&'a dyn RefArg> { + let dict = unvariant(dict); + let mut iter = dict.as_iter()?; + + loop { + let k = iter.next()?; + let v = iter.next()?; + + if k.as_str() == Some(key) { + return Some(unvariant(v)); + } + } +} + +fn tuple_i32(value: &dyn RefArg) -> Option<(i32, i32)> { + let value = unvariant(value); + let mut iter = value.as_iter()?; + + let x = i32::try_from(iter.next()?.as_i64()?).ok()?; + let y = i32::try_from(iter.next()?.as_i64()?).ok()?; + + Some((x, y)) +} + +fn unvariant(value: &dyn RefArg) -> &dyn RefArg { + if value.arg_type() != arg::ArgType::Variant { + return value; + } + + value + .as_iter() + .and_then(|mut iter| iter.next()) + .unwrap_or(value) +} + +fn prop_insert(map: &mut arg::PropMap, key: &str, value: T) +where + T: RefArg + 'static, +{ + map.insert(key.to_owned(), arg::Variant(Box::new(value))); +} + +fn choose_cursor_mode(available: u32, embed_mouse: bool) -> Option { + let choices: &[u32] = if embed_mouse { + // The portal names this METADATA; this is the closest "fallback" mode. + &[CURSOR_EMBEDDED, CURSOR_METADATA, CURSOR_HIDDEN] + } else { + &[CURSOR_HIDDEN] + }; + + choices.iter().copied().find(|mode| available & *mode != 0) +} + +fn poll_one_shot(value: &mut OneShot) { + let old = std::mem::replace( + value, + OneShot::Failed(ScreenCastError::InvalidResponse( + "internal one-shot placeholder", + )), + ); + + *value = match old { + OneShot::Pending(mut future) => match poll_unpin(&mut future) { + Poll::Ready(Ok(value)) => OneShot::Ready(value), + Poll::Ready(Err(e)) => OneShot::Failed(e.into()), + Poll::Pending => OneShot::Pending(future), + }, + other => other, + }; +} + +fn poll_unpin(future: &mut F) -> Poll +where + F: Future + Unpin, +{ + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + Pin::new(future).poll(&mut cx) +} + +fn poll_boxed(future: &mut DbusFuture) -> Poll> { + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + future.as_mut().poll(&mut cx) +} + +fn noop_waker() -> Waker { + struct NoopWake; + + impl Wake for NoopWake { + fn wake(self: Arc) {} + fn wake_by_ref(self: &Arc) {} + } + + Waker::from(Arc::new(NoopWake)) +} + +fn sender_path_component(unique_name: &str) -> String { + let name = unique_name.strip_prefix(':').unwrap_or(unique_name); + + let mut out = String::with_capacity(name.len()); + for ch in name.chars() { + if ch.is_ascii_alphanumeric() || ch == '_' { + out.push(ch); + } else { + out.push('_'); + } + } + + if out.is_empty() { + "unknown".to_owned() + } else { + out + } +} + +fn lock_response_map( + responses: &Arc>>>, +) -> MutexGuard<'_, HashMap>> { + match responses.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +fn is_terminal(result: &ScreenCastResult) -> bool { + matches!( + result, + ScreenCastResult::Ok(_) | ScreenCastResult::Failed(_) + ) +} + +/// Helper function to make a single screen cast selection +pub fn screen_cast_select_blocking( + params: ScreenCastParams, +) -> Result { + let mut manager = ScreenCastManager::new()?; + let request_id = manager.request(params)?; + + loop { + match manager.check(&request_id) { + ScreenCastResult::Ok(response) => return Ok(response), + ScreenCastResult::Failed(error) => return Err(error), + + ScreenCastResult::Queued + | ScreenCastResult::Pending + | ScreenCastResult::WaitingForUser => { + std::thread::sleep(Duration::from_millis(10)); + } + } + } +}