diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 30bcf7c..3f98793 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -71,6 +71,102 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 0.38.44", + "tracing", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -82,6 +178,30 @@ dependencies = [ "syn", ] +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.87" @@ -93,6 +213,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -141,6 +267,19 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bytes" version = "1.10.1" @@ -438,6 +577,7 @@ dependencies = [ "godot", "godot_tokio", "pipewire", + "smol", ] [[package]] @@ -447,6 +587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92b254068dbdb5a5a9d702039f5b741ac9a454617c28f69c5cc6c31792c7d11d" dependencies = [ "godot", + "smol", ] [[package]] @@ -596,6 +737,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -816,6 +963,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.2" @@ -973,6 +1126,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pipewire" version = "0.8.0" @@ -1007,6 +1171,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1114,6 +1293,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.2" @@ -1123,7 +1315,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.2", "windows-sys 0.59.0", ] @@ -1197,6 +1389,23 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + [[package]] name = "socket2" version = "0.5.8" @@ -1270,7 +1479,7 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix", + "rustix 1.0.2", "windows-sys 0.59.0", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 17c2e80..faa87fd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -5,10 +5,12 @@ edition = "2021" [dependencies] ashpd = "0.11.0" -gdext_coroutines = "0.7.1" +gdext_coroutines = {version="0.7.1", features=["async"]} +# godot = {version="0.2.4", features=["api-4-2", "experimental-godot-api", "experimental-threads"]} godot = "0.2.4" godot_tokio = "0.3.0" pipewire = "0.8.0" +smol = "2.0.2" [lib] crate-type = ["cdylib"] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 18ee31d..c93a3e5 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -2,14 +2,14 @@ #![feature(coroutine_trait)] #![feature(stmt_expr_attributes)] #![feature(unboxed_closures)] -#![cfg_attr(feature = "async", feature(async_fn_traits))] +//#![cfg_attr(feature = "async", feature(async_fn_traits))] use godot::{classes::Engine, prelude::*}; use godot_tokio::AsyncRuntime; +mod pipewire; mod player; mod portals; -//mod screencapture; struct Extension; diff --git a/rust/src/pipewire/mod.rs b/rust/src/pipewire/mod.rs new file mode 100644 index 0000000..2a59726 --- /dev/null +++ b/rust/src/pipewire/mod.rs @@ -0,0 +1,28 @@ +use std::thread; + +use godot::prelude::*; + +#[derive(GodotClass)] +#[class(base=Node)] +/// Node for interacting with Pipewire +/// Intended to not be spawned manually, as the plugin will create a +struct Pipewire { + base: Base, + /// Handle for the thread running the pipewire main loop + thread_handle: Option, +} + +#[godot_api] +impl INode for Pipewire { + fn init(base: Base) -> Self { + Self { + base, + thread_handle: None, + } + } +} + +#[godot_api] +impl Pipewire { + pub const SINGLETON: &'static str = "Pipewire"; +} diff --git a/rust/src/portals/mod.rs b/rust/src/portals/mod.rs index d4cfca8..d714be2 100644 --- a/rust/src/portals/mod.rs +++ b/rust/src/portals/mod.rs @@ -3,14 +3,14 @@ use godot::prelude::*; mod screencast; #[derive(GodotClass)] -#[class(base=Object)] +#[class(base=Node)] struct XdgPortals { - base: Base, + base: Base, } #[godot_api] -impl IObject for XdgPortals { - fn init(base: Base) -> Self { +impl INode for XdgPortals { + fn init(base: Base) -> Self { Self { base } } } diff --git a/rust/src/portals/screencast.rs b/rust/src/portals/screencast.rs index 9d57859..9aa6a6e 100644 --- a/rust/src/portals/screencast.rs +++ b/rust/src/portals/screencast.rs @@ -2,83 +2,148 @@ use ashpd::desktop::{ screencast::{CursorMode, Screencast, SourceType}, PersistMode, }; +use gdext_coroutines::prelude::*; use godot::prelude::*; use godot_tokio::AsyncRuntime; -//use std::thread; +use smol::{channel, lock::Mutex}; +use std::sync::Arc; #[derive(GodotClass)] -#[class(base=RefCounted)] -struct ScreencastResult { - base: Base, +#[class(base = Node)] +/// Runner node for requesting a pipewire screencast +struct ScreencastRunner { + base: Base, + runner: Arc>, + receiver: channel::Receiver>, + sender: channel::Sender>, + last_result: Option>, } -#[godot_api] -impl IRefCounted for ScreencastResult { - fn init(base: Base) -> Self { - let mut result = Self { base }; - //thread::spawn(|| result.capture_runner()); +struct Runner; - result +#[godot_api] +impl INode for ScreencastRunner { + fn init(base: Base) -> Self { + let (sender, receiver) = channel::unbounded(); + Self { + base, + receiver, + sender, + runner: Arc::new(Mutex::new(Runner {})), + last_result: None, + } + } + + fn process(&mut self, _delta: f64) { + let channel_elements = self.receiver.len(); + for _ in 0..channel_elements { + let result = self.receiver.recv(); + let result = AsyncRuntime::block_on(result) + .expect("Failed to receive screencast request result"); + self.last_result = Some(result.clone()); + match result { + Ok(pipewire_node_id) => self + .base_mut() + .emit_signal("screencast_sucess", &[pipewire_node_id.to_variant()]), + Err(reason) => self + .base_mut() + .emit_signal("screencast_failure", &[reason.to_variant()]), + }; + } } } #[godot_api] -impl ScreencastResult { - fn capture_runner(&mut self) { - AsyncRuntime::block_on(async { - let proxy = Screencast::new().await.expect("Screencast proxy failed"); - let session = match proxy.create_session().await { - Ok(x) => x, - Err(err) => { - godot_warn!("Failed to create session: {}", err); - return; - } - }; - match proxy - .select_sources( - &session, - CursorMode::Metadata, - SourceType::Monitor | SourceType::Window, - true, - None, - PersistMode::DoNot, - ) - .await - { - Ok(_) => {} - Err(err) => { - godot_warn!("Err while selecting source: {}", err); - return; - } +impl ScreencastRunner { + /// Emited after a request to capture a window, if that request is successful. + /// Contains the id of the pipewire node with the stream + #[signal] + fn screencast_sucess(pipewire_node_id: u32); + /// Emited after a request to capture a window, if that request has failed. + /// Contains the reason (as string) for why the request failed + #[signal] + fn screencast_failure(reason: String); + /// Emited after a request to capture a window completes + /// pipe_wire_node_id will contain the id of the pipewire node with the stream if ok is true + /// reason will contain the reason (as String) why the request failed if ok is false + /// ok indicates whether the request was successful or not + #[signal] + fn screencast_result(pipewire_node_id: u32, reason: String, ok: bool); + /// Request a new window/screen capture from the user. + /// Returns the async runner node. + /// You can either await the `completed` result on that node + #[func] + fn request_capture(&mut self) -> Gd { + let sender = self.sender.clone(); + let binding = self.runner.clone(); + // This is cursed. First having to spawn the task on the background runtime + let x = AsyncRuntime::spawn(async move { + let runner = binding.lock().await; + runner.run_capture(sender).await; + }); + // And then using the engine's runtime to await the completion in the background + self.start_async_task(async move { + let _ = x.await; + }) + } +} +impl Runner { + async fn run_capture(&self, result_chan: smol::channel::Sender>) { + let proxy = Screencast::new().await.expect("Screencast proxy failed"); + let session = match proxy.create_session().await { + Ok(x) => x, + Err(err) => { + godot_warn!("Failed to create session: {}", err); + return; } + }; + match proxy + .select_sources( + &session, + CursorMode::Metadata, + SourceType::Monitor | SourceType::Window, + true, + None, + PersistMode::DoNot, + ) + .await + { + Ok(_) => {} + Err(err) => { + godot_warn!("Err while selecting source: {}", err); + return; + } + } - let starter = match proxy.start(&session, None).await { - Ok(x) => x, - Err(err) => { - godot_warn!("Error while starting: {}", err); - return; - } - }; - let result = match starter.response() { - Ok(x) => x, - Err(err) => { - godot_warn!("Err for response: {}", err); - return; - } - }; - godot_print!("Got response"); - result.streams().iter().for_each(|stream| { + let starter = match proxy.start(&session, None).await { + Ok(x) => x, + Err(err) => { + godot_warn!("Error while starting: {}", err); + return; + } + }; + let result = match starter.response() { + Ok(x) => x, + Err(err) => { + godot_warn!("Err for response: {}", err); + return; + } + }; + godot_print!("Got response"); + // This is ugly. But I can't await inside for_each + // so instead just collect all futures and await them in a manual loop + let things: Vec<_> = result + .streams() + .iter() + .map(|stream| { godot_print!("node id: {}", stream.pipe_wire_node_id()); godot_print!("size: {:?}", stream.size()); godot_print!("position: {:?}", stream.position()); - self.base_mut().emit_signal( - "screencast_sucess", - &[stream.pipe_wire_node_id().to_variant()], - ); - }); - }); + result_chan.send(Ok(stream.pipe_wire_node_id())) + }) + .collect(); + for future in things { + let _ = future.await; + } } } - -unsafe impl Send for ScreencastResult {} -unsafe impl Sync for ScreencastResult {} diff --git a/rust/src/screencapture.rs b/rust/src/screencapture.rs deleted file mode 100644 index 072c398..0000000 --- a/rust/src/screencapture.rs +++ /dev/null @@ -1,97 +0,0 @@ -use ashpd::desktop::{ - screencast::{CursorMode, Screencast, SourceType}, - PersistMode, -}; -use godot::classes::{INode, Node}; -use godot::prelude::*; -use godot_tokio::AsyncRuntime; - -#[derive(GodotClass)] -#[class(base=Node)] -struct XdgPortals { - base: Base, - last_request_completed: bool, -} - -#[godot_api] -impl INode for XdgPortals { - fn init(base: Base) -> Self { - Self { - base, - last_request_completed: true, - } - } -} - -#[godot_api] -impl XdgPortals { - #[signal] - fn screencast_sucess(pipewire_node_id: u32); - - #[func] - fn was_last_request_completed(&self) -> bool { - self.last_request_completed - } - - #[func] - fn request_screencast(&mut self) { - godot_print!("Starting request"); - self.last_request_completed = false; - AsyncRuntime::block_on(async { - godot_print!("Starting request async"); - let proxy = Screencast::new().await.expect("Screencast proxy failed"); - let session = match proxy.create_session().await { - Ok(x) => x, - Err(err) => { - godot_warn!("Failed to create session: {}", err); - return; - } - }; - godot_print!("Got proxy"); - match proxy - .select_sources( - &session, - CursorMode::Metadata, - SourceType::Monitor | SourceType::Window, - true, - None, - PersistMode::DoNot, - ) - .await - { - Ok(_) => {} - Err(err) => { - godot_warn!("Err while selecting source: {}", err); - return; - } - } - godot_print!("Selected source"); - - let starter = match proxy.start(&session, None).await { - Ok(x) => x, - Err(err) => { - godot_warn!("Error while starting: {}", err); - return; - } - }; - let result = match starter.response() { - Ok(x) => x, - Err(err) => { - godot_warn!("Err for response: {}", err); - return; - } - }; - godot_print!("Got response"); - result.streams().iter().for_each(|stream| { - godot_print!("node id: {}", stream.pipe_wire_node_id()); - godot_print!("size: {:?}", stream.size()); - godot_print!("position: {:?}", stream.position()); - self.base_mut().emit_signal( - "screencast_sucess", - &[stream.pipe_wire_node_id().to_variant()], - ); - self.last_request_completed = true; - }); - }); - } -}