mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +02:00
✨ feat(input): Migrate to moq for input transmission (#38)
## Description **What issue are you solving (or what feature are you adding) and how are you doing it?** We cannot use golang for our input binary as we will be redoing the Webtransport stack, plus we will have to use CGO in-order to hook into X11. Like what [neko](https://github.com/m1k1o/neko) does. However, we could go down the Rust route, where X11 mouse/keyboard drivers are in pretty, and moq-rs (the MoQ library using Webtransport) works really well. So, that is what am trying to do here; implement input using rust.
This commit is contained in:
1
bin/input/.gitignore
vendored
Normal file
1
bin/input/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
1701
bin/input/Cargo.lock
generated
Normal file
1701
bin/input/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
bin/input/Cargo.toml
Normal file
19
bin/input/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "warp-input"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
clap = "4.5.4"
|
||||
enigo = "0.2.1"
|
||||
env_logger = "0.11.3"
|
||||
log = "0.4.21"
|
||||
moq-native = { git = "https://github.com/kixelated/moq-rs", version = "0.1.0" }
|
||||
moq-transport = { git = "https://github.com/kixelated/moq-rs", version = "0.5.0" }
|
||||
serde = { version="1.0.202" , features = ["derive"]}
|
||||
serde_json = "1.0.117"
|
||||
tokio = "1.37.0"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
url = "2.5.0"
|
||||
@@ -1,3 +0,0 @@
|
||||
module github.com/wanjohiryan/netris
|
||||
|
||||
go 1.22.2
|
||||
@@ -1,7 +0,0 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
fmt.Println("hello world")
|
||||
}
|
||||
43
bin/input/src/cli.rs
Normal file
43
bin/input/src/cli.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use clap::Parser;
|
||||
use std::{net, path};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Parser, Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// Listen for UDP packets on the given address.
|
||||
#[arg(long, default_value = "[::]:0")]
|
||||
pub bind: net::SocketAddr,
|
||||
|
||||
/// Connect to the given URL starting with https://
|
||||
#[arg(value_parser = moq_url)]
|
||||
pub url: Url,
|
||||
|
||||
/// Use the TLS root CA at this path, encoded as PEM.
|
||||
///
|
||||
/// This value can be provided multiple times for multiple roots.
|
||||
/// If this is empty, system roots will be used instead
|
||||
#[arg(long)]
|
||||
pub tls_root: Vec<path::PathBuf>,
|
||||
|
||||
/// Danger: Disable TLS certificate verification.
|
||||
///
|
||||
/// Fine for local development, but should be used in caution in production.
|
||||
#[arg(long)]
|
||||
pub tls_disable_verify: bool,
|
||||
|
||||
/// Publish the current time to the relay, otherwise only subscribe.
|
||||
// #[arg(long)]
|
||||
// pub publish: bool,
|
||||
|
||||
/// The name of the input track.
|
||||
#[arg(long, default_value = "netris")]
|
||||
pub namespace: String,
|
||||
|
||||
/// The name of the input track.
|
||||
#[arg(long, default_value = ".catalog")]
|
||||
pub track: String,
|
||||
}
|
||||
|
||||
fn moq_url(s: &str) -> Result<Url, String> {
|
||||
Url::try_from(s).map_err(|e| e.to_string())
|
||||
}
|
||||
220
bin/input/src/input.rs
Normal file
220
bin/input/src/input.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use anyhow::Context;
|
||||
use enigo::{
|
||||
Axis::Horizontal,
|
||||
Coordinate::Abs,
|
||||
Direction::{Press, Release},
|
||||
Enigo, Keyboard, Mouse, Settings,
|
||||
};
|
||||
use moq_transport::serve::{
|
||||
DatagramsReader, GroupsReader, ObjectsReader, StreamReader, TrackReader, TrackReaderMode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub struct Subscriber {
|
||||
track: TrackReader,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct MessageObject {
|
||||
input_type: String,
|
||||
delta_y: Option<i32>,
|
||||
delta_x: Option<i32>,
|
||||
button: Option<i32>,
|
||||
key_code: Option<i32>,
|
||||
}
|
||||
|
||||
impl Subscriber {
|
||||
pub fn new(track: TrackReader) -> Self {
|
||||
Self { track }
|
||||
}
|
||||
|
||||
pub async fn run(self) -> anyhow::Result<()> {
|
||||
match self.track.mode().await.context("failed to get mode")? {
|
||||
TrackReaderMode::Stream(stream) => Self::recv_stream(stream).await,
|
||||
TrackReaderMode::Groups(groups) => Self::recv_groups(groups).await,
|
||||
TrackReaderMode::Objects(objects) => Self::recv_objects(objects).await,
|
||||
TrackReaderMode::Datagrams(datagrams) => Self::recv_datagrams(datagrams).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_stream(mut track: StreamReader) -> anyhow::Result<()> {
|
||||
while let Some(mut group) = track.next().await? {
|
||||
println!("received a stream");
|
||||
while let Some(object) = group.read_next().await? {
|
||||
println!("received a stream 1");
|
||||
let str = String::from_utf8_lossy(&object);
|
||||
println!("{}", str);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_groups(mut groups: GroupsReader) -> anyhow::Result<()> {
|
||||
while let Some(mut group) = groups.next().await? {
|
||||
let base = group
|
||||
.read_next()
|
||||
.await
|
||||
.context("failed to get first object")?
|
||||
.context("empty group")?;
|
||||
|
||||
//TODO: Use this to allow for authorisation (admin, player or guest) etc etc
|
||||
let _base = String::from_utf8_lossy(&base);
|
||||
// let json = serde_json::from_str(&str)?;
|
||||
|
||||
//TODO: Handle clipboard
|
||||
let mut enigo = Enigo::new(&Settings::default()).unwrap();
|
||||
while let Some(object) = group.read_next().await? {
|
||||
let str = String::from_utf8_lossy(&object);
|
||||
let parsed: MessageObject = serde_json::from_str(&str)?;
|
||||
match parsed.input_type.as_str() {
|
||||
"mouse_move" => {
|
||||
if let (Some(x), Some(y)) = (parsed.delta_x, parsed.delta_y) {
|
||||
// println!("Handling mouse_move with delta_x: {}, delta_y: {}", x, y);
|
||||
enigo.move_mouse(x, y, Abs).unwrap();
|
||||
}
|
||||
}
|
||||
"mouse_key_down" => {
|
||||
if let Some(button) = parsed.button {
|
||||
// println!("Handling mouse_key_down with key: {}", button);
|
||||
if let Some(key) = mouse_key_to_enigo(button) {
|
||||
enigo.button(key, Press).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
"mouse_key_up" => {
|
||||
if let Some(button) = parsed.button {
|
||||
// println!("Handling mouse_key_up with key: {}", button);
|
||||
if let Some(key) = mouse_key_to_enigo(button) {
|
||||
enigo.button(key, Release).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
"mouse_wheel_up" => {
|
||||
//TODO: handle vertical scrolling
|
||||
// println!("Handling mouse_wheel_up with key");
|
||||
enigo.scroll(-2, Horizontal).unwrap();
|
||||
}
|
||||
"mouse_wheel_down" => {
|
||||
//TODO: handle vertical scrolling
|
||||
// println!("Handling mouse_wheel_down with key");
|
||||
enigo.scroll(2, Horizontal).unwrap();
|
||||
}
|
||||
"key_up" => {
|
||||
if let Some(key_code) = parsed.key_code {
|
||||
// println!("Handling key_up with key: {}", key_code);
|
||||
if let Some(key) = key_to_enigo(key_code as u8) {
|
||||
enigo.key(key, Release).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
"key_down" => {
|
||||
if let Some(key_code) = parsed.key_code {
|
||||
// println!("Handling key_down with key: {}", key_code);
|
||||
if let Some(key) = key_to_enigo(key_code as u8) {
|
||||
enigo.key(key, Press).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown input_type: {}", parsed.input_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_objects(mut objects: ObjectsReader) -> anyhow::Result<()> {
|
||||
while let Some(mut object) = objects.next().await? {
|
||||
let payload = object.read_all().await?;
|
||||
let str = String::from_utf8_lossy(&payload);
|
||||
println!("{}", str);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_datagrams(mut datagrams: DatagramsReader) -> anyhow::Result<()> {
|
||||
while let Some(datagram) = datagrams.read().await? {
|
||||
let str = String::from_utf8_lossy(&datagram.payload);
|
||||
println!("{}", str);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouse_key_to_enigo(key: i32) -> Option<enigo::Button> {
|
||||
match key {
|
||||
0 => Some(enigo::Button::Left),
|
||||
1 => Some(enigo::Button::Middle),
|
||||
2 => Some(enigo::Button::Right),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_to_enigo(key: u8) -> Option<enigo::Key> {
|
||||
match key {
|
||||
27 => Some(enigo::Key::Escape),
|
||||
112 => Some(enigo::Key::F1),
|
||||
113 => Some(enigo::Key::F2),
|
||||
114 => Some(enigo::Key::F3),
|
||||
115 => Some(enigo::Key::F4),
|
||||
116 => Some(enigo::Key::F5),
|
||||
117 => Some(enigo::Key::F6),
|
||||
118 => Some(enigo::Key::F7),
|
||||
119 => Some(enigo::Key::F8),
|
||||
120 => Some(enigo::Key::F9),
|
||||
121 => Some(enigo::Key::F10),
|
||||
122 => Some(enigo::Key::F11),
|
||||
123 => Some(enigo::Key::F12),
|
||||
// 19 => Some(enigo::Key::Pause), // Pause
|
||||
// 97 => Some(enigo::Key::Print), // Print
|
||||
46 => Some(enigo::Key::Delete),
|
||||
35 => Some(enigo::Key::End),
|
||||
192 => Some(enigo::Key::Unicode('`')),
|
||||
48 => Some(enigo::Key::Unicode('0')),
|
||||
49 => Some(enigo::Key::Unicode('1')),
|
||||
50 => Some(enigo::Key::Unicode('2')),
|
||||
51 => Some(enigo::Key::Unicode('3')),
|
||||
52 => Some(enigo::Key::Unicode('4')),
|
||||
53 => Some(enigo::Key::Unicode('5')),
|
||||
54 => Some(enigo::Key::Unicode('6')),
|
||||
55 => Some(enigo::Key::Unicode('7')),
|
||||
56 => Some(enigo::Key::Unicode('8')),
|
||||
57 => Some(enigo::Key::Unicode('9')),
|
||||
189 => Some(enigo::Key::Unicode('-')),
|
||||
187 => Some(enigo::Key::Unicode('=')),
|
||||
8 => Some(enigo::Key::Backspace),
|
||||
9 => Some(enigo::Key::Tab),
|
||||
219 => Some(enigo::Key::Unicode('[')),
|
||||
221 => Some(enigo::Key::Unicode(']')),
|
||||
220 => Some(enigo::Key::Unicode('\\')),
|
||||
20 => Some(enigo::Key::CapsLock),
|
||||
186 => Some(enigo::Key::Unicode(';')),
|
||||
222 => Some(enigo::Key::Unicode('\'')),
|
||||
13 => Some(enigo::Key::Return),
|
||||
16 => Some(enigo::Key::Shift), // ShiftL
|
||||
188 => Some(enigo::Key::Unicode(',')),
|
||||
190 => Some(enigo::Key::Unicode('.')),
|
||||
191 => Some(enigo::Key::Unicode('/')),
|
||||
161 => Some(enigo::Key::Shift), // ShiftR
|
||||
38 => Some(enigo::Key::UpArrow),
|
||||
17 => Some(enigo::Key::Control), // ControlL
|
||||
18 => Some(enigo::Key::Alt), // AltL
|
||||
32 => Some(enigo::Key::Space),
|
||||
165 => Some(enigo::Key::Alt), // AltR
|
||||
// 103 => Some(enigo::Key::Menu),
|
||||
163 => Some(enigo::Key::Control), // ControlR
|
||||
37 => Some(enigo::Key::LeftArrow),
|
||||
40 => Some(enigo::Key::DownArrow),
|
||||
39 => Some(enigo::Key::RightArrow),
|
||||
// 99 => Some(enigo::Key::Raw(45)), // Insert
|
||||
34 => Some(enigo::Key::PageDown),
|
||||
36 => Some(enigo::Key::Home),
|
||||
33 => Some(enigo::Key::PageUp),
|
||||
a if a >= 65 && a <= 90 => Some(enigo::Key::Unicode((a - 65 + ('a' as u8)) as char)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
76
bin/input/src/main.rs
Normal file
76
bin/input/src/main.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use moq_transport::{serve, session::Subscriber};
|
||||
use moq_native::quic;
|
||||
use std::net;
|
||||
use url::Url;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
|
||||
mod input;
|
||||
|
||||
|
||||
#[derive(Parser, Clone)]
|
||||
pub struct Cli {
|
||||
/// Listen for UDP packets on the given address.
|
||||
#[arg(long, default_value = "[::]:0")]
|
||||
pub bind: net::SocketAddr,
|
||||
|
||||
/// Connect to the given URL starting with https://
|
||||
#[arg()]
|
||||
pub url: Url,
|
||||
|
||||
/// The TLS configuration.
|
||||
#[command(flatten)]
|
||||
pub tls: moq_native::tls::Cli,
|
||||
|
||||
// /// Publish the current time to the relay, otherwise only subscribe.
|
||||
// #[arg(long)]
|
||||
// pub publish: bool,
|
||||
/// The name of the input track.
|
||||
#[arg(long, default_value = "netris")]
|
||||
pub namespace: String,
|
||||
|
||||
/// The name of the input track.
|
||||
#[arg(long, default_value = ".catalog")]
|
||||
pub track: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
// Disable tracing so we don't get a bunch of Quinn spam.
|
||||
let tracer = tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::WARN)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(tracer).unwrap();
|
||||
|
||||
let config = Cli::parse();
|
||||
let tls = config.tls.load()?;
|
||||
|
||||
let quic = quic::Endpoint::new(quic::Config {
|
||||
bind: config.bind,
|
||||
tls,
|
||||
})?;
|
||||
|
||||
log::info!("connecting to server: url={}", config.url);
|
||||
|
||||
let session = quic.client.connect(&config.url).await?;
|
||||
|
||||
let (session, mut subscriber) = Subscriber::connect(session)
|
||||
.await
|
||||
.context("failed to create MoQ Transport session")?;
|
||||
|
||||
let (prod, sub) = serve::Track::new(config.namespace, config.track).produce();
|
||||
|
||||
let input = input::Subscriber::new(sub);
|
||||
|
||||
//TODO: Make sure to retry until the input server comes [Use Supervisord for now]
|
||||
tokio::select! {
|
||||
res = session.run() => res.context("session error")?,
|
||||
res = input.run() => res.context("input error")?,
|
||||
res = subscriber.subscribe(prod) => res.context("failed to subscribe to track")?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user