diff --git a/Cargo.lock b/Cargo.lock index d8702aceca..1946a143f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alvr_server_mock" +version = "21.0.0-dev10" +dependencies = [ + "alvr_common", + "alvr_filesystem", + "alvr_packets", + "alvr_server_core", + "alvr_session", + "mp4", +] + [[package]] name = "alvr_server_openvr" version = "21.0.0-dev10" @@ -3436,6 +3448,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "mp4" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3" +dependencies = [ + "byteorder", + "bytes", + "num-rational", + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "naga" version = "24.0.0" @@ -3703,6 +3729,7 @@ dependencies = [ "num-bigint", "num-integer", "num-traits", + "serde", ] [[package]] diff --git a/alvr/dashboard/Cargo.toml b/alvr/dashboard/Cargo.toml index a89655f81a..74ca9fd96a 100644 --- a/alvr/dashboard/Cargo.toml +++ b/alvr/dashboard/Cargo.toml @@ -6,6 +6,12 @@ rust-version.workspace = true authors.workspace = true license.workspace = true +[features] +steamvr-server = [] +# monado-server = [] # todo +mock-server = [] +default = ["steamvr-server"] + [dependencies] alvr_adb.workspace = true alvr_common.workspace = true diff --git a/alvr/dashboard/src/dashboard/components/devices.rs b/alvr/dashboard/src/dashboard/components/devices.rs index e330d91b26..3009841a74 100644 --- a/alvr/dashboard/src/dashboard/components/devices.rs +++ b/alvr/dashboard/src/dashboard/components/devices.rs @@ -75,8 +75,8 @@ impl DevicesTab { #[cfg(not(target_arch = "wasm32"))] ui.with_layout(Layout::right_to_left(eframe::emath::Align::Center), |ui| { - if ui.button("Launch SteamVR").clicked() { - crate::steamvr_launcher::LAUNCHER.lock().launch_steamvr(); + if ui.button("Launch server").clicked() { + crate::server_launcher::LAUNCHER.lock().launch_server(); } }); }) diff --git a/alvr/dashboard/src/dashboard/mod.rs b/alvr/dashboard/src/dashboard/mod.rs index f8467d605e..98d6bc263e 100644 --- a/alvr/dashboard/src/dashboard/mod.rs +++ b/alvr/dashboard/src/dashboard/mod.rs @@ -103,7 +103,7 @@ impl Dashboard { let server_restarting = Arc::clone(&self.server_restarting); let condvar = Arc::clone(&self.server_restarting_condvar); move || { - crate::steamvr_launcher::LAUNCHER.lock().restart_steamvr(); + crate::server_launcher::LAUNCHER.lock().restart_server(); *server_restarting.lock() = false; condvar.notify_one(); @@ -170,7 +170,7 @@ impl eframe::App for Dashboard { // todo: find a way to center both vertically and horizontally ui.vertical_centered(|ui| { ui.add_space(100.0); - ui.heading(RichText::new("SteamVR is restarting").size(30.0)); + ui.heading(RichText::new("The server is restarting").size(30.0)); }); }); @@ -231,16 +231,16 @@ impl eframe::App for Dashboard { ui.add_space(5.0); if connected_to_server { - if ui.button("Restart SteamVR").clicked() { + if ui.button("Restart server").clicked() { self.restart_steamvr(&mut requests); } - } else if ui.button("Launch SteamVR").clicked() { - crate::steamvr_launcher::LAUNCHER.lock().launch_steamvr(); + } else if ui.button("Launch server").clicked() { + crate::server_launcher::LAUNCHER.lock().launch_server(); } ui.horizontal(|ui| { ui.add_space(5.0); - ui.label(RichText::new("SteamVR:").size(13.0)); + ui.label(RichText::new("Server:").size(13.0)); ui.add_space(-10.0); if connected_to_server { ui.label( @@ -307,9 +307,9 @@ impl eframe::App for Dashboard { let shutdown_alvr = || { self.data_sources.request(ServerRequest::ShutdownSteamvr); - crate::steamvr_launcher::LAUNCHER + crate::server_launcher::LAUNCHER .lock() - .ensure_steamvr_shutdown(); + .ensure_server_shutdown(); }; if let Some(popup) = &self.new_version_popup @@ -330,8 +330,8 @@ impl eframe::App for Dashboard { && self.session.as_ref().is_some_and(|s| { s.to_settings() .extra - .steamvr_launcher - .open_close_steamvr_with_dashboard + .server_launcher + .open_close_server_with_dashboard }) { shutdown_alvr(); diff --git a/alvr/dashboard/src/main.rs b/alvr/dashboard/src/main.rs index bbd3c48e5e..f5a4b54c3a 100644 --- a/alvr/dashboard/src/main.rs +++ b/alvr/dashboard/src/main.rs @@ -13,7 +13,7 @@ mod linux_checks; #[cfg(not(target_arch = "wasm32"))] mod logging_backend; #[cfg(not(target_arch = "wasm32"))] -mod steamvr_launcher; +mod server_launcher; #[cfg(not(target_arch = "wasm32"))] use data_sources::DataSources; @@ -65,10 +65,10 @@ fn main() { if data_sources::get_read_only_local_session() .settings() .extra - .steamvr_launcher - .open_close_steamvr_with_dashboard + .server_launcher + .open_close_server_with_dashboard { - steamvr_launcher::LAUNCHER.lock().launch_steamvr() + server_launcher::LAUNCHER.lock().launch_server() } let ico = IconDir::read(Cursor::new(include_bytes!("../resources/dashboard.ico"))).unwrap(); diff --git a/alvr/dashboard/src/steamvr_launcher/linux_steamvr.rs b/alvr/dashboard/src/server_launcher/linux_steamvr.rs similarity index 100% rename from alvr/dashboard/src/steamvr_launcher/linux_steamvr.rs rename to alvr/dashboard/src/server_launcher/linux_steamvr.rs diff --git a/alvr/dashboard/src/server_launcher/mod.rs b/alvr/dashboard/src/server_launcher/mod.rs new file mode 100644 index 0000000000..5d1e4e9016 --- /dev/null +++ b/alvr/dashboard/src/server_launcher/mod.rs @@ -0,0 +1,198 @@ +#[cfg(target_os = "linux")] +mod linux_steamvr; +#[cfg(windows)] +mod windows_steamvr; + +use crate::data_sources; +use alvr_adb::commands as adb; +use alvr_common::{ + anyhow::{Context, Result}, + debug, + glam::bool, + parking_lot::Mutex, + warn, +}; +use alvr_filesystem as afs; +use serde_json::{self, json}; +use std::{ + ffi::OsStr, + fs, + marker::PhantomData, + process::Command, + slice, thread, + time::{Duration, Instant}, +}; +use sysinfo::{ProcessesToUpdate, System}; + +const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); +const DRIVER_KEY: &str = "driver_alvr_server"; +const BLOCKED_KEY: &str = "blocked_by_safe_mode"; + +pub fn is_server_running() -> bool { + #[cfg(feature = "steamvr-server")] + let process_name = afs::exec_fname("vrserver"); + #[cfg(feature = "mock-server")] + let process_name = afs::mock_server_fname(); + + System::new_all() + .processes_by_name(OsStr::new(&process_name)) + .count() + != 0 +} + +pub fn maybe_kill_server() { + let mut system = System::new_all(); + + #[cfg(feature = "steamvr-server")] + let process_names = [afs::exec_fname("vrmonitor"), afs::exec_fname("vrserver")]; + #[cfg(feature = "mock-server")] + let process_names = [afs::mock_server_fname()]; + + #[cfg_attr(target_os = "macos", expect(unused_variables))] + for process_name in process_names { + system.refresh_processes(ProcessesToUpdate::All, true); + + for process in system.processes_by_name(OsStr::new(&process_name)) { + debug!("Killing {}", process_name); + + #[cfg(target_os = "linux")] + linux_steamvr::terminate_process(process); + #[cfg(windows)] + windows_steamvr::kill_process(process.pid().as_u32()); + + thread::sleep(Duration::from_secs(1)); + } + } +} + +fn unblock_alvr_driver() -> Result<()> { + if !cfg!(target_os = "linux") { + return Ok(()); + } + + let path = alvr_server_io::steamvr_settings_file_path()?; + let text = fs::read_to_string(&path).with_context(|| format!("Failed to read {path:?}"))?; + let new_text = unblock_alvr_driver_within_vrsettings(text.as_str()) + .with_context(|| "Failed to rewrite .vrsettings.")?; + fs::write(&path, new_text) + .with_context(|| "Failed to write .vrsettings back after changing it.")?; + Ok(()) +} + +// Reads and writes back steamvr.vrsettings in order to +// ensure the ALVR driver is not blocked (safe mode). +fn unblock_alvr_driver_within_vrsettings(text: &str) -> Result { + let mut settings = serde_json::from_str::(text)?; + let values = settings + .as_object_mut() + .with_context(|| "Failed to parse .vrsettings.")?; + let blocked = values + .get(DRIVER_KEY) + .and_then(|driver| driver.get(BLOCKED_KEY)) + .and_then(|blocked| blocked.as_bool()) + .unwrap_or(false); + + if blocked { + debug!("Unblocking ALVR driver in SteamVR."); + if !values.contains_key(DRIVER_KEY) { + values.insert(DRIVER_KEY.into(), json!({})); + } + let driver = settings[DRIVER_KEY] + .as_object_mut() + .with_context(|| "Did not find ALVR key in settings.")?; + driver.insert(BLOCKED_KEY.into(), json!(false)); // overwrites if present + } else { + debug!("ALVR is not blocked in SteamVR."); + } + + Ok(serde_json::to_string_pretty(&settings)?) +} + +pub struct Launcher { + _phantom: PhantomData<()>, +} + +impl Launcher { + pub fn launch_server(&self) { + let filesystem_layout = crate::get_filesystem_layout(); + + // The ADB server might be left running because of a unclean termination of SteamVR + // Note that this will also kill a system wide ADB server not started by ALVR + let wired_enabled = data_sources::get_read_only_local_session() + .session() + .client_connections + .contains_key(alvr_sockets::WIRED_CLIENT_HOSTNAME); + if wired_enabled && let Some(path) = adb::get_adb_path(&filesystem_layout) { + adb::kill_server(&path).ok(); + } + + if cfg!(feature = "steamvr-server") { + #[cfg(target_os = "linux")] + linux_steamvr::linux_hardware_checks(); + + let alvr_driver_dir = &filesystem_layout.openvr_driver_root_dir; + + // Make sure to unregister any other ALVR driver because it would cause a socket conflict + let other_alvr_dirs = alvr_server_io::get_registered_drivers() + .unwrap_or_default() + .into_iter() + .filter(|path| { + path.to_string_lossy().to_lowercase().contains("alvr") + && path != alvr_driver_dir + }) + .collect::>(); + alvr_server_io::driver_registration(&other_alvr_dirs, false).ok(); + + alvr_server_io::driver_registration(slice::from_ref(alvr_driver_dir), true).ok(); + + if let Err(err) = unblock_alvr_driver() { + warn!("Failed to unblock ALVR driver: {:?}", err); + } + + #[cfg(target_os = "linux")] + { + let vrcompositor_wrap_result = linux_steamvr::maybe_wrap_vrcompositor_launcher(); + alvr_common::show_err(linux_steamvr::maybe_wrap_vrcompositor_launcher()); + if vrcompositor_wrap_result.is_err() { + return; + } + } + } + + if !is_server_running() { + debug!("Server is dead. Launching..."); + + if cfg!(feature = "steamvr-server") { + #[cfg(windows)] + windows_steamvr::start_steamvr(); + + #[cfg(target_os = "linux")] + linux_steamvr::start_steamvr(); + } else if cfg!(feature = "mock-server") { + Command::new(filesystem_layout.mock_server_exe()) + .spawn() + .ok(); + } + } + } + + pub fn ensure_server_shutdown(&self) { + debug!("Waiting for server to shutdown..."); + let start_time = Instant::now(); + while start_time.elapsed() < SHUTDOWN_TIMEOUT && is_server_running() { + thread::sleep(Duration::from_millis(500)); + } + + maybe_kill_server(); + } + + pub fn restart_server(&self) { + self.ensure_server_shutdown(); + self.launch_server(); + } +} + +// Singleton with exclusive access +pub static LAUNCHER: Mutex = Mutex::new(Launcher { + _phantom: PhantomData, +}); diff --git a/alvr/dashboard/src/steamvr_launcher/windows_steamvr.rs b/alvr/dashboard/src/server_launcher/windows_steamvr.rs similarity index 100% rename from alvr/dashboard/src/steamvr_launcher/windows_steamvr.rs rename to alvr/dashboard/src/server_launcher/windows_steamvr.rs diff --git a/alvr/dashboard/src/steamvr_launcher/mod.rs b/alvr/dashboard/src/steamvr_launcher/mod.rs deleted file mode 100644 index 012e7d1aee..0000000000 --- a/alvr/dashboard/src/steamvr_launcher/mod.rs +++ /dev/null @@ -1,186 +0,0 @@ -#[cfg(target_os = "linux")] -mod linux_steamvr; -#[cfg(windows)] -mod windows_steamvr; - -use crate::data_sources; -use alvr_adb::commands as adb; -use alvr_common::{ - anyhow::{Context, Result}, - debug, - glam::bool, - parking_lot::Mutex, - warn, -}; -use alvr_filesystem as afs; -use serde_json::{self, json}; -use std::{ - ffi::OsStr, - fs, - marker::PhantomData, - thread, - time::{Duration, Instant}, -}; -use sysinfo::{ProcessesToUpdate, System}; - -const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); -const DRIVER_KEY: &str = "driver_alvr_server"; -const BLOCKED_KEY: &str = "blocked_by_safe_mode"; - -pub fn is_steamvr_running() -> bool { - System::new_all() - .processes_by_name(OsStr::new(&afs::exec_fname("vrserver"))) - .count() - != 0 -} - -pub fn maybe_kill_steamvr() { - let mut system = System::new_all(); - - #[allow(unused_variables)] - for process in system.processes_by_name(OsStr::new(&afs::exec_fname("vrmonitor"))) { - debug!("Killing vrmonitor"); - - #[cfg(target_os = "linux")] - linux_steamvr::terminate_process(process); - #[cfg(windows)] - windows_steamvr::kill_process(process.pid().as_u32()); - - thread::sleep(Duration::from_secs(1)); - } - - system.refresh_processes(ProcessesToUpdate::All, true); - - #[allow(unused_variables)] - for process in system.processes_by_name(OsStr::new(&afs::exec_fname("vrserver"))) { - debug!("Killing vrserver"); - - #[cfg(target_os = "linux")] - linux_steamvr::terminate_process(process); - #[cfg(windows)] - windows_steamvr::kill_process(process.pid().as_u32()); - - thread::sleep(Duration::from_secs(1)); - } -} - -fn unblock_alvr_driver() -> Result<()> { - if !cfg!(target_os = "linux") { - return Ok(()); - } - - let path = alvr_server_io::steamvr_settings_file_path()?; - let text = fs::read_to_string(&path).with_context(|| format!("Failed to read {path:?}"))?; - let new_text = unblock_alvr_driver_within_vrsettings(text.as_str()) - .with_context(|| "Failed to rewrite .vrsettings.")?; - fs::write(&path, new_text) - .with_context(|| "Failed to write .vrsettings back after changing it.")?; - Ok(()) -} - -// Reads and writes back steamvr.vrsettings in order to -// ensure the ALVR driver is not blocked (safe mode). -fn unblock_alvr_driver_within_vrsettings(text: &str) -> Result { - let mut settings = serde_json::from_str::(text)?; - let values = settings - .as_object_mut() - .with_context(|| "Failed to parse .vrsettings.")?; - let blocked = values - .get(DRIVER_KEY) - .and_then(|driver| driver.get(BLOCKED_KEY)) - .and_then(|blocked| blocked.as_bool()) - .unwrap_or(false); - - if blocked { - debug!("Unblocking ALVR driver in SteamVR."); - if !values.contains_key(DRIVER_KEY) { - values.insert(DRIVER_KEY.into(), json!({})); - } - let driver = settings[DRIVER_KEY] - .as_object_mut() - .with_context(|| "Did not find ALVR key in settings.")?; - driver.insert(BLOCKED_KEY.into(), json!(false)); // overwrites if present - } else { - debug!("ALVR is not blocked in SteamVR."); - } - - Ok(serde_json::to_string_pretty(&settings)?) -} - -pub struct Launcher { - _phantom: PhantomData<()>, -} - -impl Launcher { - pub fn launch_steamvr(&self) { - // The ADB server might be left running because of a unclean termination of SteamVR - // Note that this will also kill a system wide ADB server not started by ALVR - let wired_enabled = data_sources::get_read_only_local_session() - .session() - .client_connections - .contains_key(alvr_sockets::WIRED_CLIENT_HOSTNAME); - if wired_enabled && let Some(path) = adb::get_adb_path(&crate::get_filesystem_layout()) { - adb::kill_server(&path).ok(); - } - - #[cfg(target_os = "linux")] - linux_steamvr::linux_hardware_checks(); - - let alvr_driver_dir = crate::get_filesystem_layout().openvr_driver_root_dir; - - // Make sure to unregister any other ALVR driver because it would cause a socket conflict - let other_alvr_dirs = alvr_server_io::get_registered_drivers() - .unwrap_or_default() - .into_iter() - .filter(|path| { - path.to_string_lossy().to_lowercase().contains("alvr") && *path != alvr_driver_dir - }) - .collect::>(); - alvr_server_io::driver_registration(&other_alvr_dirs, false).ok(); - - alvr_server_io::driver_registration(&[alvr_driver_dir], true).ok(); - - if let Err(err) = unblock_alvr_driver() { - warn!("Failed to unblock ALVR driver: {:?}", err); - } - - #[cfg(target_os = "linux")] - { - let vrcompositor_wrap_result = linux_steamvr::maybe_wrap_vrcompositor_launcher(); - alvr_common::show_err(linux_steamvr::maybe_wrap_vrcompositor_launcher()); - if vrcompositor_wrap_result.is_err() { - return; - } - } - - if !is_steamvr_running() { - debug!("SteamVR is dead. Launching..."); - - #[cfg(windows)] - windows_steamvr::start_steamvr(); - - #[cfg(target_os = "linux")] - linux_steamvr::start_steamvr(); - } - } - - pub fn ensure_steamvr_shutdown(&self) { - debug!("Waiting for SteamVR to shutdown..."); - let start_time = Instant::now(); - while start_time.elapsed() < SHUTDOWN_TIMEOUT && is_steamvr_running() { - thread::sleep(Duration::from_millis(500)); - } - - maybe_kill_steamvr(); - } - - pub fn restart_steamvr(&self) { - self.ensure_steamvr_shutdown(); - self.launch_steamvr(); - } -} - -// Singleton with exclusive access -pub static LAUNCHER: Mutex = Mutex::new(Launcher { - _phantom: PhantomData, -}); diff --git a/alvr/filesystem/src/lib.rs b/alvr/filesystem/src/lib.rs index d647aa94b2..9cfd7212f6 100644 --- a/alvr/filesystem/src/lib.rs +++ b/alvr/filesystem/src/lib.rs @@ -56,6 +56,10 @@ pub fn streamer_build_dir() -> PathBuf { build_dir().join(format!("alvr_streamer_{OS}")) } +pub fn streamer_mock_build_dir() -> PathBuf { + build_dir().join("alvr_streamer_mock") +} + pub fn launcher_fname() -> String { exec_fname("ALVR Launcher") } @@ -80,6 +84,10 @@ pub fn dashboard_fname() -> &'static str { } } +pub fn mock_server_fname() -> String { + exec_fname("alvr_mock_server") +} + // Layout of the ALVR installation. All paths are absolute #[derive(Clone, Default, Debug)] pub struct Layout { @@ -177,6 +185,10 @@ impl Layout { self.executables_dir.join(dashboard_fname()) } + pub fn mock_server_exe(&self) -> PathBuf { + self.executables_dir.join(mock_server_fname()) + } + pub fn local_adb_exe(&self) -> PathBuf { self.executables_dir .join("platform-tools") diff --git a/alvr/server_core/Cargo.toml b/alvr/server_core/Cargo.toml index 5f56ec5df4..c1f3cfeb47 100644 --- a/alvr/server_core/Cargo.toml +++ b/alvr/server_core/Cargo.toml @@ -40,7 +40,6 @@ hyper = { version = "0.14", features = [ mdns-sd = "0.13" profiling = { version = "1", optional = true } reqwest = "0.11" # not used but webserver does not work without it. todo: investigate -rfd = "0.15" rosc = "0.10" tokio = { version = "1", features = [ "rt-multi-thread", @@ -55,3 +54,6 @@ tokio-util = { version = "0.7", features = ["codec"] } serde = "1" serde_json = "1" sysinfo = "0.33" + +[target.'cfg(not(target_os = "macos"))'.dependencies] +rfd = "0.15" diff --git a/alvr/server_core/src/logging_backend.rs b/alvr/server_core/src/logging_backend.rs index 690cd2f903..6ad6d7ff41 100644 --- a/alvr/server_core/src/logging_backend.rs +++ b/alvr/server_core/src/logging_backend.rs @@ -106,20 +106,23 @@ pub fn init_logging(session_log_path: Option, crash_log_path: Option rfd::MessageLevel::Error, - LogSeverity::Warning => rfd::MessageLevel::Warning, - LogSeverity::Info | LogSeverity::Debug => rfd::MessageLevel::Info, - }; + #[cfg(not(target_os = "macos"))] + { + fn popup_callback(title: &str, message: &str, severity: LogSeverity) { + let level = match severity { + LogSeverity::Error => rfd::MessageLevel::Error, + LogSeverity::Warning => rfd::MessageLevel::Warning, + LogSeverity::Info | LogSeverity::Debug => rfd::MessageLevel::Info, + }; - rfd::MessageDialog::new() - .set_title(title) - .set_description(message) - .set_level(level) - .show(); + rfd::MessageDialog::new() + .set_title(title) + .set_description(message) + .set_level(level) + .show(); + } + alvr_common::set_popup_callback(popup_callback); } - alvr_common::set_popup_callback(popup_callback); alvr_common::set_panic_hook(); } diff --git a/alvr/server_mock/Cargo.toml b/alvr/server_mock/Cargo.toml new file mode 100644 index 0000000000..0fb5532d24 --- /dev/null +++ b/alvr/server_mock/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "alvr_server_mock" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +alvr_common.workspace = true +alvr_filesystem.workspace = true +alvr_packets.workspace = true +alvr_server_core.workspace = true +alvr_session.workspace = true + +mp4 = "0.14" diff --git a/alvr/server_mock/src/main.rs b/alvr/server_mock/src/main.rs new file mode 100644 index 0000000000..dde479f5d4 --- /dev/null +++ b/alvr/server_mock/src/main.rs @@ -0,0 +1,173 @@ +use alvr_common::{ + HEAD_ID, Pose, RelaxedAtomic, ViewParams, error, info, + parking_lot::{Mutex, RwLock}, +}; +use alvr_filesystem as afs; +use alvr_server_core::{ServerCoreContext, ServerCoreEvent}; +use alvr_session::CodecType; +use mp4::MediaType; +use std::{ + env, + fs::File, + sync::{Arc, mpsc}, + thread, + time::{Duration, Instant}, +}; + +fn main() { + let filesystem_layout = afs::Layout::new(env::current_exe().unwrap().parent().unwrap()); + alvr_server_core::initialize_environment(filesystem_layout.clone()); + + let log_to_disk = alvr_server_core::settings().extra.logging.log_to_disk; + + alvr_server_core::init_logging( + log_to_disk.then_some(filesystem_layout.session_log()), + Some(filesystem_layout.crash_log()), + ); + + let (context, events_receiver) = ServerCoreContext::new(); + + let mut video_thread = None; + let is_client_connected = Arc::new(RelaxedAtomic::new(false)); + let last_pose_timestamp = Arc::new(Mutex::new(Duration::ZERO)); + let last_head_pose = Arc::new(Mutex::new(Pose::default())); + let local_view_params = Arc::new(RwLock::new([ViewParams::DUMMY; 2])); + + context.start_connection(); + + let context = Arc::new(RwLock::new(Some(context))); + + loop { + let event = match events_receiver.recv_timeout(Duration::from_millis(5)) { + Ok(event) => event, + Err(mpsc::RecvTimeoutError::Timeout) => continue, + Err(mpsc::RecvTimeoutError::Disconnected) => break, + }; + + match event { + ServerCoreEvent::SetOpenvrProperty { .. } => (), + ServerCoreEvent::ClientConnected => { + is_client_connected.set(true); + + let video_path = filesystem_layout + .static_resources_dir + .join("test_video.mp4"); + + let context = Arc::clone(&context); + let is_client_connected = Arc::clone(&is_client_connected); + let last_pose_timestamp = Arc::clone(&last_pose_timestamp); + let last_head_pose = Arc::clone(&last_head_pose); + let local_view_params = Arc::clone(&local_view_params); + + video_thread = Some(thread::spawn(move || { + let video_file = File::open(video_path).unwrap(); + + let mut mp4 = mp4::read_mp4(video_file).unwrap(); + + let Some(h264_track) = mp4 + .tracks() + .values() + .find(|track| track.media_type().unwrap() == MediaType::H264) + else { + // Note: the crate mp4 provides only SPS and PPS. the HEVC VPS might be + // inside the stream but I haven't checked + error!("The video does not contain a H264 track"); + return; + }; + + if let Some(context) = &*context.read() { + let mut config_nals = h264_track.sequence_parameter_set().unwrap().to_vec(); + config_nals.extend(h264_track.picture_parameter_set().unwrap()); + + context.set_video_config_nals(config_nals, CodecType::H264); + } + + let track_id = h264_track.track_id(); + let sample_count = h264_track.sample_count(); + let frametime = Duration::from_secs_f64(1.0 / h264_track.frame_rate()); + + info!( + "Video: track {track_id}, sample count {sample_count}, frame time {frametime:?}" + ); + + let mut deadline = Instant::now() + frametime; + let mut sample_id = 1; + while is_client_connected.value() + && let Some(context) = &*context.read() + { + let head_pose = *last_head_pose.lock(); + let timestamp = *last_pose_timestamp.lock(); + + info!("Reading video sample {sample_id}"); + let sample = mp4.read_sample(track_id, sample_id).unwrap().unwrap(); + + context.report_composed(timestamp, Duration::ZERO); + context.report_present(timestamp, Duration::ZERO); + + let local_views_params = local_view_params.read(); + + let global_view_params = [ + ViewParams { + pose: head_pose * local_views_params[0].pose, + fov: local_views_params[0].fov, + }, + ViewParams { + pose: head_pose * local_views_params[1].pose, + fov: local_views_params[1].fov, + }, + ]; + + context.send_video_nal( + timestamp, + global_view_params, + true, + sample.bytes.to_vec(), + ); + + thread::sleep( + deadline + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::ZERO), + ); + deadline += frametime; + + sample_id += 1; + if sample_id >= sample_count { + sample_id = 1; + } + } + })); + } + ServerCoreEvent::ClientDisconnected => { + is_client_connected.set(false); + if let Some(thread) = video_thread.take() { + thread.join().ok(); + } + } + ServerCoreEvent::Battery(_) => (), + ServerCoreEvent::PlayspaceSync(_) => (), + ServerCoreEvent::LocalViewParams(params) => *local_view_params.write() = params, + ServerCoreEvent::Tracking { poll_timestamp } => { + if let Some(context) = &*context.read() + && let Some(motion) = context.get_device_motion(*HEAD_ID, poll_timestamp) + { + *last_pose_timestamp.lock() = poll_timestamp; + + let motion = motion.predict(poll_timestamp, poll_timestamp); + *last_head_pose.lock() = motion.pose; + } + } + ServerCoreEvent::Buttons(_) => (), + ServerCoreEvent::RequestIDR => (), + ServerCoreEvent::CaptureFrame => (), + ServerCoreEvent::GameRenderLatencyFeedback(_) => (), + ServerCoreEvent::ShutdownPending => break, + ServerCoreEvent::RestartPending => { + if let Some(context) = context.write().take() { + context.restart(); + } + break; + } + } + } +} diff --git a/alvr/session/src/settings.rs b/alvr/session/src/settings.rs index 318e830eab..02d559908b 100644 --- a/alvr/session/src/settings.rs +++ b/alvr/session/src/settings.rs @@ -1469,9 +1469,12 @@ pub struct LoggingConfig { } #[derive(SettingsSchema, Serialize, Deserialize, Clone)] -pub struct SteamvrLauncher { - #[schema(strings(display_name = "Open and close SteamVR with dashboard"))] - pub open_close_steamvr_with_dashboard: bool, +pub struct ServerLauncher { + #[schema(strings( + display_name = "Open and close server with dashboard", + notice = "This may kill the currently open VR game" + ))] + pub open_close_server_with_dashboard: bool, } #[derive(SettingsSchema, Serialize, Deserialize, Clone)] @@ -1513,7 +1516,7 @@ pub struct NewVersionPopupConfig { #[derive(SettingsSchema, Serialize, Deserialize, Clone)] pub struct ExtraConfig { - pub steamvr_launcher: SteamvrLauncher, + pub server_launcher: ServerLauncher, pub capture: CaptureConfig, pub logging: LoggingConfig, #[cfg_attr(not(target_os = "linux"), schema(flag = "hidden"))] @@ -2131,8 +2134,8 @@ pub fn session_settings_default() -> SettingsDefault { decoder: false, }, }, - steamvr_launcher: SteamvrLauncherDefault { - open_close_steamvr_with_dashboard: false, + server_launcher: ServerLauncherDefault { + open_close_server_with_dashboard: false, }, capture: CaptureConfigDefault { startup_video_recording: false, diff --git a/alvr/xtask/src/build.rs b/alvr/xtask/src/build.rs index 8b2ef9e147..1d34548ab5 100644 --- a/alvr/xtask/src/build.rs +++ b/alvr/xtask/src/build.rs @@ -273,6 +273,57 @@ pub fn build_streamer( } } +pub fn build_streamer_mock(keep_config: bool) { + let sh = Shell::new().unwrap(); + + let build_root = afs::streamer_mock_build_dir(); + + let build_layout = Layout::new(&build_root); + + let artifacts_dir = afs::target_dir().join("debug"); + + let maybe_config = if keep_config { + fs::read_to_string(build_layout.session()).ok() + } else { + None + }; + + sh.remove_path(build_root).ok(); + sh.create_dir(&build_layout.executables_dir).unwrap(); + + if let Some(config) = maybe_config { + fs::write(build_layout.session(), config).ok(); + } + + // Build mock server + cmd!(sh, "cargo build -p alvr_server_mock").run().unwrap(); + sh.copy_file( + artifacts_dir.join(afs::exec_fname("alvr_server_mock")), + build_layout.mock_server_exe(), + ) + .unwrap(); + + // Build dashboard + cmd!( + sh, + "cargo build -p alvr_dashboard --no-default-features --features mock-server" + ) + .run() + .unwrap(); + sh.copy_file( + artifacts_dir.join(afs::exec_fname("alvr_dashboard")), + build_layout.dashboard_exe(), + ) + .unwrap(); + + // Copy test video + sh.copy_file( + afs::deps_dir().join("test_video.mp4"), + build_layout.static_resources_dir.join("test_video.mp4"), + ) + .unwrap(); +} + pub fn build_launcher(profile: Profile, reproducible: bool) { let sh = Shell::new().unwrap(); diff --git a/alvr/xtask/src/dependencies.rs b/alvr/xtask/src/dependencies.rs index 3ac3d94c87..1d81d5899c 100644 --- a/alvr/xtask/src/dependencies.rs +++ b/alvr/xtask/src/dependencies.rs @@ -1,4 +1,7 @@ -use crate::{BuildPlatform, command}; +use crate::{ + BuildPlatform, + command::{self, download}, +}; use alvr_filesystem as afs; use std::{fs, path::Path}; use xshell::{Shell, cmd}; @@ -276,6 +279,13 @@ pub fn build_ffmpeg_linux(enable_nvenc: bool, deps_path: &Path) { pub fn prepare_macos_deps() {} +pub fn server_mock_dependencies() { + let sh = Shell::new().unwrap(); + + download(&sh, "https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_30MB.mp4", &afs::deps_dir().join("test_video.mp4")) + .unwrap(); +} + pub fn prepare_server_deps( platform: Option, skip_admin_priv: bool, @@ -298,6 +308,8 @@ pub fn prepare_server_deps( } } } + + server_mock_dependencies(); } fn get_android_openxr_loaders(selection: OpenXRLoadersSelection) { diff --git a/alvr/xtask/src/main.rs b/alvr/xtask/src/main.rs index ccd0c6fc5a..e8fdad6b88 100644 --- a/alvr/xtask/src/main.rs +++ b/alvr/xtask/src/main.rs @@ -12,7 +12,7 @@ use alvr_filesystem as afs; use dependencies::OpenXRLoadersSelection; use packaging::ReleaseFlavor; use pico_args::Arguments; -use std::{fs, process, time::Instant}; +use std::{fs, path::Path, process, time::Instant}; use xshell::{Shell, cmd}; const HELP_STR: &str = r#" @@ -31,6 +31,7 @@ SUBCOMMANDS: build-client-lib Build a C-ABI ALVR client library and header build-client-xr-lib Build a C-ABI ALVR OpenXR entry point client library and header run-streamer Build streamer and then open the dashboard + run-streamer-mock Build mock streamer and then open the dashboard run-launcher Build launcher and then open it format Autoformat all code check-format Check if code is correctly formatted @@ -78,10 +79,10 @@ pub fn print_help_and_exit(message: &str) -> ! { process::exit(1); } -pub fn run_streamer() { +pub fn run_streamer(root: &Path) { let sh = Shell::new().unwrap(); - let dashboard_exe = Layout::new(&afs::streamer_build_dir()).dashboard_exe(); + let dashboard_exe = Layout::new(root).dashboard_exe(); cmd!(sh, "{dashboard_exe}").run().unwrap(); } @@ -244,7 +245,13 @@ fn main() { if !no_rebuild { build::build_streamer(profile, gpl, None, false, profiling, keep_config); } - run_streamer(); + run_streamer(&afs::streamer_build_dir()); + } + "run-streamer-mock" => { + if !no_rebuild { + build::build_streamer_mock(keep_config); + } + run_streamer(&afs::streamer_mock_build_dir()); } "run-launcher" => { if !no_rebuild {