diff --git a/.gitmodules b/.gitmodules index cbf45cb..73b50d7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -8,3 +8,6 @@ [submodule "libs/libyuv-sys"] path = libs/libyuv-sys url = https://github.com/MahitMehta/libyuv-sys.git +[submodule "libs/scrap"] + path = libs/scrap + url = https://github.com/MahitMehta/scrap.git \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 4273716..ab6507d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3454,7 +3454,7 @@ dependencies = [ [[package]] name = "mrial_fs" -version = "0.1.16" +version = "0.1.17" dependencies = [ "dirs", "log", @@ -3466,7 +3466,7 @@ dependencies = [ [[package]] name = "mrial_player" -version = "0.1.16" +version = "0.1.17" dependencies = [ "base64", "chacha20poly1305", @@ -3492,7 +3492,7 @@ dependencies = [ [[package]] name = "mrial_proto" -version = "0.1.16" +version = "0.1.17" dependencies = [ "chacha20poly1305", "log", @@ -3505,7 +3505,7 @@ dependencies = [ [[package]] name = "mrial_server" -version = "0.1.16" +version = "0.1.17" dependencies = [ "base64", "cfg-if 0.1.10", @@ -4744,12 +4744,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scrap" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3e5e5b3bd8d65b04de768fe0bdbd1e2da0831be9f117e1cc1d71785c9126c" dependencies = [ "block", "cfg-if 0.1.10", "libc", + "log", + "pretty_env_logger", "winapi 0.2.8", ] diff --git a/Cargo.toml b/Cargo.toml index a0c2755..822b1cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ exclude = [ "libs" ] [workspace.package] -version = "0.1.16" +version = "0.1.17" authors = ["Mahit Mehta", "Marco Ngai"] license = "GPL-3.0-only" edition = "2021" diff --git a/libs/scrap b/libs/scrap new file mode 160000 index 0000000..de3cd4c --- /dev/null +++ b/libs/scrap @@ -0,0 +1 @@ +Subproject commit de3cd4cd8610f1cac7f80e29f59dbdb93ffefc6d diff --git a/linux/debian/mrial_server.service b/linux/debian/mrial_server.service index 822a15d..32381fc 100644 --- a/linux/debian/mrial_server.service +++ b/linux/debian/mrial_server.service @@ -1,13 +1,15 @@ [Unit] Description=Mrial Server -After=syslog.target network.target network-online.target sshd.service http-daem> +After=syslog.target network.target network-online.target sshd.service Wants=network-online.target [Service] Type=simple Restart=always -RestartSec=10 +RestartSec=1s +StartLimitIntervalSec=15s +StartLimitBurst=3 ExecStart=/var/lib/mrial_server/scripts/startup.sh [Install] -WantedBy=default.target \ No newline at end of file +WantedBy=multi-user.target \ No newline at end of file diff --git a/linux/debian/startup.sh b/linux/debian/startup.sh index 4118bf6..263b680 100755 --- a/linux/debian/startup.sh +++ b/linux/debian/startup.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Uncomment if using LightDM -# export XAUTHORITY=/var/lib/lightdm/.Xauthority -export DISPLAY=:0 +export RUST_LOG=debug + /usr/bin/mrial_server \ No newline at end of file diff --git a/mrial_server/Cargo.toml b/mrial_server/Cargo.toml index 6e2ee09..577b925 100644 --- a/mrial_server/Cargo.toml +++ b/mrial_server/Cargo.toml @@ -9,7 +9,8 @@ version.workspace = true [dependencies] futures = { version = "0.3.29", features = ["thread-pool", "executor"]} -scrap = "0.5" +# scrap = "0.5" +scrap = { path = "../libs/scrap" } tokio = { version = "1.34.0", features = ["full"]} mouse-rs = "0.4.2" enigo = "0.2.0-rc2" @@ -42,4 +43,5 @@ assets = [ ["../target/release/mrial_server", "usr/bin/mrial_server", "755"], ["../linux/debian/*", "var/lib/mrial_server/scripts/", "755"] ] -maintainer-scripts = "../linux/debian" \ No newline at end of file +maintainer-scripts = "../linux/debian" +systemd-units = { enable = true } diff --git a/mrial_server/src/audio/linux.rs b/mrial_server/src/audio/linux.rs index b8a7658..3943978 100644 --- a/mrial_server/src/audio/linux.rs +++ b/mrial_server/src/audio/linux.rs @@ -1,6 +1,7 @@ use crate::conn::Connection; use super::{AudioEncoder, AudioServerThread, IAudioController}; +use log::{debug}; use mrial_proto::*; use pipewire as pw; @@ -39,7 +40,14 @@ impl IAudioController for AudioServerThread { // 1. systemctl --user restart pipewire.service // 2. "systemctl --user restart pipewire-pulse.service" // 3. pactl load-module module-null-sink media.class=Audio/Sink sink_name=mrial_sink channel_map=stereo - let core = context.connect(None).unwrap(); + let core = match context.connect(None) { + Ok(core) => core, + Err(e) => { + debug!("Failed to connect to PipeWire Context: {}", e); + // TODO: Should attempt to reconnect + return; + } + }; let data = UserData { format: Default::default(), diff --git a/mrial_server/src/main.rs b/mrial_server/src/main.rs index 9d44fd3..bc0404f 100644 --- a/mrial_server/src/main.rs +++ b/mrial_server/src/main.rs @@ -15,7 +15,14 @@ async fn main() { pretty_env_logger::init_timed(); let conn: Connection = Connection::new(); - let mut video_server = VideoServerThread::new(conn.clone()); + let mut video_server = match VideoServerThread::new(conn.clone()) { + Ok(server) => server, + Err(e) => { + log::error!("Failed to start Video Server: {}", e); + return; + } + }; + let audio_server = AudioServerThread::new(); audio_server.run(conn); diff --git a/mrial_server/src/video/display.rs b/mrial_server/src/video/display.rs index 2b7f912..b320c27 100644 --- a/mrial_server/src/video/display.rs +++ b/mrial_server/src/video/display.rs @@ -1,14 +1,23 @@ #[cfg(target_os = "linux")] use xrandr::{ScreenResources, XHandle}; +use xrandr::XrandrError; pub struct DisplayMeta {} impl DisplayMeta { #[cfg(target_os = "linux")] - pub fn get_display_resolutions() -> Result<(Vec, Vec), xrandr::XrandrError> { + pub fn get_current_resolution() -> Result<(usize, usize), XrandrError> { let mut handle = XHandle::open().unwrap(); let mon1 = &handle.monitors()?[0]; + Ok((mon1.width_px as usize, mon1.height_px as usize)) + } + + #[cfg(target_os = "linux")] + pub fn get_display_resolutions() -> Result<(Vec, Vec), xrandr::XrandrError> { + let mut handle = XHandle::open().unwrap(); + // let mon1 = &handle.monitors()?[0]; + let mut widths: Vec = Vec::new(); let mut heights: Vec = Vec::new(); diff --git a/mrial_server/src/video/mod.rs b/mrial_server/src/video/mod.rs index ce1162d..ea39778 100644 --- a/mrial_server/src/video/mod.rs +++ b/mrial_server/src/video/mod.rs @@ -9,11 +9,7 @@ use log::debug; use mrial_proto::*; use scrap::{Capturer, Display}; use std::{ - collections::VecDeque, - fs::File, - io::{ErrorKind::WouldBlock, Write}, - sync::RwLockReadGuard, - time::{Duration, Instant}, + collections::VecDeque, env, fs::File, io::{ErrorKind::WouldBlock, Write}, process::Command, sync::RwLockReadGuard, thread, time::{Duration, Instant} }; use x264::{Encoder, Param, Picture}; use yuv::YUVBuffer; @@ -29,9 +25,18 @@ use self::yuv::EColorSpace; pub enum VideoServerAction { Inactive, ConfigUpdate, + NewUserSession, + RestartStream, SymKey, } +#[derive(PartialEq)] +pub enum Setting { + Unknown, + PreLogin, + PostLogin +} + pub struct VideoServerThread { pool: ThreadPool, yuv_handles: VecDeque>, @@ -39,40 +44,139 @@ pub struct VideoServerThread { row_len: usize, par: Param, pic: Picture, - capturer: Capturer, + capturer: Option, encoder: Encoder, deployer: PacketDeployer, conn: Connection, + setting: Setting, +} + +fn get_x11_authenicated_client() -> Option { + let gui_users_output = Command::new("sh") + .arg("-c") + .arg("who | grep tty7") + .output() + .unwrap(); + + if gui_users_output.stdout.is_empty() || !gui_users_output.status.success() { + return None; + } + + let output_str = String::from_utf8(gui_users_output.stdout).unwrap(); + if let Some(user) = output_str.split_whitespace().next() { + return Some(user.to_string()); + } + + None +} + +struct SessionSettingThread { +} + +impl SessionSettingThread { + pub fn run(video_server_ch_sender: kanal::Sender) { + let _ = thread::spawn(move || { + loop { + if get_x11_authenicated_client().is_some() { + debug!("User has logged in"); + video_server_ch_sender.send(VideoServerAction::NewUserSession).unwrap(); + break; + } + debug!("Waiting for user to login"); + + thread::sleep(Duration::from_secs(1)); + } + }); + } } impl VideoServerThread { - pub fn new(conn: Connection) -> Self { - let display: Display = Display::primary().unwrap(); - let capturer = Capturer::new(display).unwrap(); + pub fn new(conn: Connection) -> Result> { + let mut setting = Setting::Unknown; + if cfg!(target_os = "linux") { + setting = VideoServerThread::config_xenv()?; + } + + let display: Display = Display::primary()?; + let capturer = Capturer::new(display)?; + conn.set_dimensions(capturer.width(), capturer.height()); - let pool = ThreadPool::builder().pool_size(1).create().unwrap(); + let pool = ThreadPool::builder().pool_size(1).create()?; let yuv_handles = VecDeque::new(); - let row_len = 4 * conn.get_meta().width * conn.get_meta().width; + let row_len = 4 * capturer.width() * capturer.height(); let mut par: Param = VideoServerThread::get_parameters(conn.get_meta()); - let encoder = x264::Encoder::open(&mut par).unwrap(); - let pic = Picture::from_param(&par).unwrap(); + let encoder = x264::Encoder::open(&mut par)?; + let pic = Picture::from_param(&par)?; - Self { + Ok(Self { pool, yuv_handles, row_len, file: None, par, pic, - capturer, + capturer: Some(capturer), encoder, - deployer: PacketDeployer::new(EPacketType::NAL, true), - conn, + setting, + deployer: PacketDeployer::new(EPacketType::NAL, false), + conn + }) + } + + /* + * Configures the X environment for the server by setting + * correct display and Xauthority variables. + * + * Additionally, it sets the XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS + * for pipewire connection from root. + * + */ + + // TODO: Make DISPLAY variable dynamic AND + // TODO: not assume the display manager is lightdm + + #[cfg(target_os = "linux")] + fn config_xenv() -> Result> { + env::set_var("DISPLAY", ":0"); + + if let Some(username) = get_x11_authenicated_client() { + /* + * Environment variables needed to connect to + * user graphical user session from root + */ + let xauthority_path = format!("/home/{}/.Xauthority", username); + debug!("Xauthority User Path: {}", xauthority_path); + env::set_var("XAUTHORITY", xauthority_path); + + /* + * Environment variables needed for pipewire connection from root. + */ + let user_id_cmd = format!("id -u {}", username); + let user_id_output = Command::new("sh") + .arg("-c") + .arg(user_id_cmd) + .output() + .unwrap(); + + let user_id = String::from_utf8(user_id_output.stdout).unwrap(); + let xdg_runtime_dir = format!("/run/user/{}", user_id.trim()); + let dbus_session_bus_address = format!("unix:path={}/bus", xdg_runtime_dir); + + debug!("XDG_RUNTIME_DIR: {}", &xdg_runtime_dir); + debug!("DBUS_SESSION_BUS_ADDRESS: {}", &dbus_session_bus_address); + + env::set_var("XDG_RUNTIME_DIR", xdg_runtime_dir); + env::set_var("DBUS_SESSION_BUS_ADDRESS", dbus_session_bus_address); + + return Ok(Setting::PostLogin); } + + env::set_var("XAUTHORITY", "/var/lib/lightdm/.Xauthority"); + return Ok(Setting::PreLogin); } #[inline] @@ -101,8 +205,39 @@ impl VideoServerThread { par } - fn handle_server_action(&mut self, server_action: Option) { + fn drop_capturer(&mut self) { + let capturer = self.capturer.take().unwrap(); + debug!("Dropping Capturer"); + drop(capturer); + } + + fn handle_server_action( + &mut self, + server_action: Option, + video_server_ch_sender: &kanal::Sender + ) { match server_action { + Some(VideoServerAction::NewUserSession) => { + match VideoServerThread::config_xenv() { + Ok(Setting::PostLogin) => { + self.setting = Setting::PostLogin; + + // TODO: This does not work, maybe this needs to be done after more time, + // TODO: because the resolution does not change, or maybe it just doesn't know the + // TODO: correct resolution + if let Ok((width, height)) = DisplayMeta::get_current_resolution() { + debug!("Post-Login Resolution: {}x{}", width, height); + + if let Err(e) = DisplayMeta::update_display_resolution(width, height) { + debug!("Error syncing display resolution after login: {}", e.to_string()); + } + } + + video_server_ch_sender.send(VideoServerAction::RestartStream).unwrap(); + } + _ => {} + } + } Some(VideoServerAction::Inactive) => { self.encoder = x264::Encoder::open(&mut self.par).unwrap(); } @@ -111,31 +246,18 @@ impl VideoServerThread { self.deployer.set_sym_key(sym_key.clone()); } } - Some(VideoServerAction::ConfigUpdate) => { - let requested_width = self.conn.get_meta().width; - let requested_height = self.conn.get_meta().height; + Some(VideoServerAction::RestartStream) => { + self.drop_capturer(); - if requested_width == self.capturer.width() as usize - && requested_height == self.capturer.height() as usize - { - return; - } + let display = Display::primary().unwrap(); + self.capturer = Some(Capturer::new(display).unwrap()); - let _updated_resolution = - match DisplayMeta::update_display_resolution(requested_width, requested_height) - { - Ok(updated) => updated, - Err(e) => { - debug!("Error updating display resolution: {}", e); - false - } - }; + let capturer = self.capturer.as_ref().unwrap(); - let display = Display::primary().unwrap(); - self.capturer = Capturer::new(display).unwrap(); + self.row_len = 4 * capturer.width() * capturer.height(); self.conn - .set_dimensions(self.capturer.width(), self.capturer.height()); + .set_dimensions(capturer.width(), capturer.height()); self.par = VideoServerThread::get_parameters(self.conn.get_meta()); self.encoder = x264::Encoder::open(&mut self.par).unwrap(); @@ -155,6 +277,29 @@ impl VideoServerThread { self.yuv_handles.clear(); } + Some(VideoServerAction::ConfigUpdate) => { + let requested_width = self.conn.get_meta().width; + let requested_height = self.conn.get_meta().height; + + let capturer = match &self.capturer { + Some(capturer) => capturer, + None => { + return; + } + }; + + if requested_width == capturer.width() as usize + && requested_height == capturer.height() as usize + { + return; + } + + if let Err(e) = DisplayMeta::update_display_resolution(requested_width, requested_height) { + debug!("Error updating display resolution: {}", e); + } + + video_server_ch_sender.send(VideoServerAction::RestartStream).unwrap(); + } None => { return; } @@ -171,14 +316,25 @@ impl VideoServerThread { // Send update to client to update headers let headers = self.encoder.get_headers().unwrap(); EventsThread::run(&self.conn, headers.as_bytes().to_vec(), ch_sender.clone()); + + if self.setting == Setting::PreLogin { + SessionSettingThread::run(ch_sender.clone()); + } loop { while ch_receiver.len() > 0 { if let Ok(server_action) = ch_receiver.try_recv_realtime() { - self.handle_server_action(server_action); + self.handle_server_action(server_action, &ch_sender); } } + let capturer = match &mut self.capturer { + Some(capturer) => capturer, + None => { + continue; + } + }; + if !self.conn.has_clients() { self.conn.filter_clients(); std::thread::sleep(Duration::from_millis(250)); @@ -186,13 +342,22 @@ impl VideoServerThread { } let sleep = Instant::now(); - let width = self.capturer.width(); - let height = self.capturer.height(); + let width = capturer.width(); + let height = capturer.height(); - match self.capturer.frame() { + match capturer.frame() { Ok(frame) => { let bgra_frame = frame.chunks(self.row_len).next().unwrap().to_vec(); + if (width * height * 4) != bgra_frame.len() { + debug!("Frame size: {} Expected: {}", bgra_frame.len(), width * height * 4); + } + + if bgra_frame.len() < (width * height * 4) { + debug!("Frame size less than expected"); + continue; + } + let cvt_rgb_yuv = async move { // TODO: figure out why this is neccessary let yuv = YUVBuffer::with_bgra_for_444(