Compare commits

...

10 Commits

Author SHA1 Message Date
Riccardo Zaglia a42f673c75 fix(dashboard): ♻️ Refactor connections UI code and fix rustfmt 2023-11-02 14:40:31 +08:00
Meister1593 0b47edc1b5
Added basic toggle - cabled mode for devices (#1877)
* Added basic toggle cabled mode for devices

* Remove unnecessary imports. Change link to specific adb topic.

* Reformat

* Reformat server_io lib.rs

* Changed cabled toggle wording

* WIP work on ui, better state handling

* Changed import, added mut to trusted clients

* Improved ui margins. Fixed toggle state being unused.

* Removed unused imports

* Added darker frame to New clients
2023-11-02 11:07:29 +08:00
Riccardo Zaglia 7d627ca8fa fix(xtask): 🐛 Fix macOS compilation 2023-11-01 12:13:00 +08:00
Nova King 7eee4e947a
fix: lynx r1 hand tracking (#1874) 2023-10-26 08:53:06 +08:00
Bryce Torcello 5e54bf19c9
docs: add audio setup for Flatpak release (#1870)
* docs: add audio setup for Flatpak release

* docs: fix incorrect numbering
2023-10-19 23:44:32 -05:00
Riccardo Zaglia 7be28bbd6c chore: 🔧 Update MSRV 2023-10-20 11:26:42 +08:00
Jarett Millard efe629bda3
Download and build nv-codec-headers in prepare-deps (#1869)
* Download and build nv-codec-headers in prepare-deps

* Remove nv-codec-headers installation section from wiki
2023-10-19 02:16:28 -05:00
Riccardo Zaglia 2b17f057a0 build(xtask): Add xtask build-server-lib 2023-10-18 13:48:16 +08:00
Riccardo Zaglia 6374dee48e
feat(dashboard): Add notification tips (#1865)
* feat(dashboard):  Add notification tips

* Add more tips
2023-10-17 17:18:02 +08:00
Riccardo Zaglia 85990bee0d feat(client_core): 🐛 Retry game audio thread in case of error 2023-10-16 18:46:43 +08:00
23 changed files with 514 additions and 163 deletions

1
Cargo.lock generated
View File

@ -270,6 +270,7 @@ dependencies = [
"gloo-net",
"ico",
"instant",
"rand",
"serde",
"serde_json",
"settings-schema",

View File

@ -5,7 +5,7 @@ members = ["alvr/*"]
[workspace.package]
version = "21.0.0-dev00"
edition = "2021"
rust-version = "1.65"
rust-version = "1.70"
authors = ["alvr-org"]
license = "MIT"

View File

@ -350,7 +350,7 @@ pub fn get_next_frame_batch(
// continuity will not be affected.
pub fn receive_samples_loop(
running: Arc<RelaxedAtomic>,
mut receiver: StreamReceiver<()>,
receiver: &mut StreamReceiver<()>,
sample_buffer: Arc<Mutex<VecDeque<f32>>>,
channels_count: usize,
batch_frames_count: usize,
@ -500,11 +500,11 @@ impl Iterator for StreamingSource {
pub fn play_audio_loop(
running: Arc<RelaxedAtomic>,
device: AudioDevice,
device: &AudioDevice,
channels_count: u16,
sample_rate: u32,
config: AudioBufferingConfig,
receiver: StreamReceiver<()>,
receiver: &mut StreamReceiver<()>,
) -> Result<()> {
// Size of a chunk of frames. It corresponds to the duration if a fade-in/out in frames.
let batch_frames_count = sample_rate as usize * config.batch_ms as usize / 1000;

View File

@ -51,6 +51,10 @@ impl AudioInputCallback for RecorderCallback {
fn on_error_before_close(&mut self, _: &mut dyn AudioInputStreamSafe, error: oboe::Error) {
*self.state.lock() = AudioRecordState::Err(Some(error.into()));
}
fn on_error_after_close(&mut self, _: &mut dyn AudioInputStreamSafe, error: oboe::Error) {
*self.state.lock() = AudioRecordState::Err(Some(error.into()));
}
}
#[allow(unused_variables)]
@ -131,11 +135,11 @@ impl AudioOutputCallback for PlayerCallback {
#[allow(unused_variables)]
pub fn play_audio_loop(
running: Arc<RelaxedAtomic>,
device: AudioDevice,
device: &AudioDevice,
channels_count: u16,
sample_rate: u32,
config: AudioBufferingConfig,
receiver: StreamReceiver<()>,
receiver: &mut StreamReceiver<()>,
) -> Result<()> {
// the client sends invalid sample rates sometimes, and we crash if we try and use one
// (batch_frames_count ends up zero and the audio callback gets confused)

View File

@ -273,7 +273,7 @@ fn connection_pipeline(
let mut video_receiver =
stream_socket.subscribe_to_stream::<VideoPacketHeader>(VIDEO, MAX_UNREAD_PACKETS);
let game_audio_receiver = stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS);
let mut game_audio_receiver = stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS);
let tracking_sender = stream_socket.request_stream(TRACKING);
let mut haptics_receiver =
stream_socket.subscribe_to_stream::<Haptics>(HAPTICS, MAX_UNREAD_PACKETS);
@ -343,14 +343,16 @@ fn connection_pipeline(
let device = AudioDevice::new_output(None, None).to_con()?;
thread::spawn(move || {
alvr_common::show_err(audio::play_audio_loop(
Arc::clone(&IS_STREAMING),
device,
2,
game_audio_sample_rate,
config.buffering,
game_audio_receiver,
));
while IS_STREAMING.value() {
alvr_common::show_err(audio::play_audio_loop(
Arc::clone(&IS_STREAMING),
&device,
2,
game_audio_sample_rate,
config.buffering.clone(),
&mut game_audio_receiver,
));
}
})
} else {
thread::spawn(|| ())

View File

@ -61,6 +61,8 @@ name = "android.permission.ACCESS_WIFI_STATE"
[[package.metadata.android.uses_permission]]
name = "android.permission.INTERNET"
[[package.metadata.android.uses_permission]]
name = "android.permission.ACCESS_NETWORK_STATE"
[[package.metadata.android.uses_permission]]
name = "android.permission.RECORD_AUDIO"
[[package.metadata.android.uses_permission]]
name = "android.permission.WAKE_LOCK"

View File

@ -19,6 +19,7 @@ chrono = "0.4"
eframe = "0.23"
env_logger = "0.10"
ico = "0.3"
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
settings-schema = { git = "https://github.com/alvr-org/settings-schema-rs", rev = "676185f" }

View File

@ -1,9 +1,9 @@
use crate::dashboard::ServerRequest;
use crate::dashboard::{basic_components, ServerRequest};
use alvr_gui_common::theme::{self, log_colors};
use alvr_packets::ClientListAction;
use alvr_session::{ClientConnectionConfig, ConnectionState, SessionConfig};
use eframe::{
egui::{Frame, Grid, Layout, RichText, TextEdit, Ui, Window},
egui::{self, Frame, Grid, Layout, RichText, TextEdit, Ui, Window},
emath::{Align, Align2},
epaint::Color32,
};
@ -76,104 +76,19 @@ impl ConnectionsTab {
ui.vertical_centered_justified(|ui| {
if let Some(clients) = &self.new_clients {
Frame::group(ui.style())
.fill(theme::SECTION_BG)
.show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(5.0);
ui.heading("New clients");
});
Grid::new(1).num_columns(2).show(ui, |ui| {
for (hostname, _) in clients {
ui.horizontal(|ui| {
ui.add_space(10.0);
ui.label(hostname);
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.button("Trust").clicked() {
requests.push(ServerRequest::UpdateClientList {
hostname: hostname.clone(),
action: ClientListAction::Trust,
});
};
});
ui.end_row();
}
})
});
if let Some(request) = new_clients_section(ui, clients) {
requests.push(request);
}
}
ui.add_space(10.0);
if let Some(clients) = &self.trusted_clients {
Frame::group(ui.style())
.fill(theme::SECTION_BG)
.show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(5.0);
ui.heading("Trusted clients");
});
Grid::new(2).num_columns(2).show(ui, |ui| {
for (hostname, data) in clients {
ui.horizontal(|ui| {
ui.add_space(10.0);
ui.label(format!(
"{hostname}: {} ({})",
data.current_ip
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
data.display_name
));
match data.connection_state {
ConnectionState::Disconnected => {
ui.colored_label(Color32::GRAY, "Disconnected")
}
ConnectionState::Connecting => ui
.colored_label(log_colors::WARNING_LIGHT, "Connecting"),
ConnectionState::Connected => {
ui.colored_label(theme::OK_GREEN, "Connected")
}
ConnectionState::Streaming => {
ui.colored_label(theme::OK_GREEN, "Streaming")
}
ConnectionState::Disconnecting { .. } => ui.colored_label(
log_colors::WARNING_LIGHT,
"Disconnecting",
),
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.button("Remove").clicked() {
requests.push(ServerRequest::UpdateClientList {
hostname: hostname.clone(),
action: ClientListAction::RemoveEntry,
});
}
if ui.button("Edit").clicked() {
self.edit_popup_state = Some(EditPopupState {
new_client: false,
hostname: hostname.to_owned(),
ips: data
.manual_ips
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<String>>(),
});
}
});
ui.end_row();
}
});
if ui.button("Add client manually").clicked() {
self.edit_popup_state = Some(EditPopupState {
hostname: "XXXX.client.alvr".into(),
new_client: true,
ips: Vec::new(),
});
}
});
if let Some(clients) = &mut self.trusted_clients {
if let Some(request) =
trusted_clients_section(ui, clients, &mut self.edit_popup_state)
{
requests.push(request);
}
}
});
@ -230,3 +145,157 @@ impl ConnectionsTab {
requests
}
}
fn new_clients_section(
ui: &mut Ui,
clients: &[(String, ClientConnectionConfig)],
) -> Option<ServerRequest> {
let mut request = None;
Frame::group(ui.style())
.fill(theme::SECTION_BG)
.show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(5.0);
ui.heading("New clients");
});
for (hostname, _) in clients {
Frame::group(ui.style())
.fill(theme::DARKER_BG)
.inner_margin(egui::vec2(15.0, 12.0))
.show(ui, |ui| {
Grid::new(format!("{}-new-clients", hostname))
.num_columns(2)
.spacing(egui::vec2(8.0, 8.0))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(hostname);
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.button("Trust").clicked() {
request = Some(ServerRequest::UpdateClientList {
hostname: hostname.clone(),
action: ClientListAction::Trust,
});
};
});
ui.end_row();
});
});
}
});
request
}
fn trusted_clients_section(
ui: &mut Ui,
clients: &mut [(String, ClientConnectionConfig)],
edit_popup_state: &mut Option<EditPopupState>,
) -> Option<ServerRequest> {
let mut request = None;
Frame::group(ui.style())
.fill(theme::SECTION_BG)
.show(ui, |ui| {
ui.vertical_centered_justified(|ui| {
ui.add_space(5.0);
ui.heading("Trusted clients");
});
ui.vertical(|ui| {
for (hostname, data) in clients {
Frame::group(ui.style())
.fill(theme::DARKER_BG)
.inner_margin(egui::vec2(15.0, 12.0))
.show(ui, |ui| {
Grid::new(format!("{}-clients", hostname))
.num_columns(2)
.spacing(egui::vec2(8.0, 8.0))
.show(ui, |ui| {
ui.label(format!(
"{hostname}: {} ({})",
data.current_ip
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
data.display_name
));
ui.horizontal(|ui| {
ui.with_layout(
Layout::right_to_left(Align::Center),
|ui| match data.connection_state {
ConnectionState::Disconnected => {
ui.colored_label(Color32::GRAY, "Disconnected")
}
ConnectionState::Connecting => ui.colored_label(
log_colors::WARNING_LIGHT,
"Connecting",
),
ConnectionState::Connected => {
ui.colored_label(theme::OK_GREEN, "Connected")
}
ConnectionState::Streaming => {
ui.colored_label(theme::OK_GREEN, "Streaming")
}
ConnectionState::Disconnecting { .. } => ui
.colored_label(
log_colors::WARNING_LIGHT,
"Disconnecting",
),
},
);
});
ui.end_row();
ui.horizontal(|ui| {
ui.hyperlink_to(
"Use Cable:",
format!(
"https://github.com/alvr-org/ALVR/wiki/{}#{}",
"ALVR-wired-setup-(ALVR-over-USB)",
"letting-your-pc-communicate-with-your-hmd"
),
);
if basic_components::switch(ui, &mut data.cabled).changed()
{
request = Some(ServerRequest::UpdateClientList {
hostname: hostname.clone(),
action: ClientListAction::SetCabled(data.cabled),
});
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.button("Remove").clicked() {
request = Some(ServerRequest::UpdateClientList {
hostname: hostname.clone(),
action: ClientListAction::RemoveEntry,
});
}
if ui.button("Edit").clicked() {
*edit_popup_state = Some(EditPopupState {
new_client: false,
hostname: hostname.to_owned(),
ips: data
.manual_ips
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<String>>(),
});
}
});
});
});
}
});
if ui.button("Add client manually").clicked() {
*edit_popup_state = Some(EditPopupState {
hostname: "XXXX.client.alvr".into(),
new_client: true,
ips: Vec::new(),
});
}
});
request
}

View File

@ -6,6 +6,7 @@ use eframe::{
emath::Align,
epaint::{Color32, Stroke},
};
use rand::seq::SliceRandom;
use std::time::Duration;
#[cfg(target_arch = "wasm32")]
@ -13,30 +14,77 @@ use instant::Instant;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
const NO_NOTIFICATIONS: &str = "No new notifications";
const TIMEOUT: Duration = Duration::from_secs(5);
const NO_NOTIFICATIONS_MESSAGE: &str = "No new notifications";
const NOTIFICATION_TIPS: &[&str] = &[
// The following tips are ordered roughtly in the order settings appear
r#"If you started having crashes after changing some settings, reset ALVR by deleting "session.json"."#,
r#"Some settings are hidden by default. Click the "Expand" button next to some settings to expand the submenus."#,
r#"It's highly advisable to keep audio setting as default in ALVR and modify the default audio device in the taskbar tray."#,
r#"Increasing "Maximum buffering" may reduce stutters at the cost of more latency."#,
r#"Turning off "Optimize game render latency" may improve streaming smoothness."#,
r#"Sometimes switching between h264 and HEVC codecs is necessary on certain GPUs to fix crashing or fallback to software encoding."#,
r#"If you're using NVIDIA gpu, best to use high bitrate H264, if you're using AMD gpu, HEVC might look better."#,
r#"If you experience "white snow" flickering, reduce the resolution to "Low" and disable "Foveated encoding"."#,
r#"Increasing "Color correction"->"Sharpness" may improve the perceived image quality."#,
r#"If you have problems syncing external controllers or trackers to ALVR tracking space, add one element to "Extra openvr props", then set a custom "Tracking system name"."#,
r#"To change the visual appearance of controllers, set "Controllers"->"Emulation mode"."#,
r#"ALVR supports custom button bindings! If you need help please ask us in the Discord server."#,
r#"ALVR supports hand tracking gestures. Use thumb-index/middle/ring/pinky to activate different buttons. Joystick is enabled by moving the thumb on a closed fist."#,
r#"If hand tracking gestures are annoying, you can disable them in "Controllers"->"Gestures". Alternatively you can enable "Gestures"->"Only touch"."#,
r#"You can fine-tune the controllers responsiveness with "Controllers"->"Prediction"."#,
r#"If the visual controller/hand models does not match the physical controller, you can tweak the offset in "Controllers"->"Left controller position/rotation offset" (affects both controllers)."#,
r#"When using external trackers or controllers you should set both "Position/Rotation recentering mode" to "Disabled"."#,
r#"You can enable tilt mode. Set "Position recentering mode to "Local" and "Rotation recentering mode" to "Tilted"."#,
r#"If you often experience image glitching, you can trade that with stutter frames using "Avoid video glitching"."#,
r#"You can run custom commands/programs at client connection/disconnection using "On connect/disconnect script"."#,
r#"In case you want to report a bug, to get a log file enable "Log to disk". The log will be inside "session_log.txt"."#,
r#"For hacking purposes, you can enable "Log tracking", "Log button presses", "Log haptics". You can get the data using a websocket at ws://localhost:8082/api/events"#,
r#"In case you want to report a bug and share your log, you should enable "Prefer backtrace"."#,
r#"You can quickly cycle through tips like this one by toggling "Show notification tip"."#,
r#"If you want to use body trackers or other SteamVR drivers together with ALVR, set "Driver launch action" to "Unregister ALVR at shutdown""#,
r#"It's handy to enable "Open and close SteamVR with dashboard"."#,
r#"If you want to share a video recording for reporting a bug, you can enable "Rolling video files" to limit the file size of the upload."#,
// Miscellaneous
r#"If your headset does not appear in the clients list it might be in a different subnet. Try "Add client manually"."#,
r#"For audio setup on Linux, check the wiki at https://github.com/alvr-org/ALVR/wiki/Installation-guide#automatic-audio--microphone-setup"#,
r#"ALVR supports wired connection using USB. Check the wiki at https://github.com/alvr-org/ALVR/wiki/ALVR-wired-setup-(ALVR-over-USB)"#,
r#"You can record a video of the gameplay using "Start recording" in the "Debug" category in the sidebar."#,
];
pub struct NotificationBar {
message: String,
current_level: LogSeverity,
receive_instant: Instant,
min_notification_level: LogSeverity,
tip_message: Option<String>,
expanded: bool,
}
impl NotificationBar {
pub fn new() -> Self {
Self {
message: NO_NOTIFICATIONS.into(),
message: NO_NOTIFICATIONS_MESSAGE.into(),
current_level: LogSeverity::Debug,
receive_instant: Instant::now(),
min_notification_level: LogSeverity::Debug,
tip_message: None,
expanded: false,
}
}
pub fn update_settings(&mut self, settings: &Settings) {
self.min_notification_level = settings.logging.notification_level;
if settings.logging.show_notification_tip {
if self.tip_message.is_none() {
self.tip_message = NOTIFICATION_TIPS
.choose(&mut rand::thread_rng())
.map(|s| format!("Tip: {s}"));
}
} else {
self.tip_message = None;
}
}
pub fn push_notification(&mut self, event: LogEntry) {
@ -53,7 +101,10 @@ impl NotificationBar {
pub fn ui(&mut self, context: &egui::Context) {
let now = Instant::now();
if now > self.receive_instant + TIMEOUT {
self.message = NO_NOTIFICATIONS.into();
self.message = self
.tip_message
.clone()
.unwrap_or_else(|| NO_NOTIFICATIONS_MESSAGE.into());
self.current_level = LogSeverity::Debug;
}

View File

@ -106,7 +106,8 @@ pub struct Layout {
impl Layout {
pub fn new(root: &Path) -> Self {
if cfg!(target_os = "linux") {
#[cfg(target_os = "linux")]
{
// Get paths from environment or use FHS compliant paths
let executables_dir = if !env!("executables_dir").is_empty() {
PathBuf::from(env!("executables_dir"))
@ -177,20 +178,20 @@ impl Layout {
ufw_config_dir,
vulkan_layer_manifest_dir,
}
} else {
Self {
executables_dir: root.to_owned(),
libraries_dir: root.to_owned(),
static_resources_dir: root.to_owned(),
config_dir: root.to_owned(),
log_dir: root.to_owned(),
openvr_driver_root_dir: root.to_owned(),
vrcompositor_wrapper_dir: root.to_owned(),
firewall_script_dir: root.to_owned(),
firewalld_config_dir: root.to_owned(),
ufw_config_dir: root.to_owned(),
vulkan_layer_manifest_dir: root.to_owned(),
}
}
#[cfg(not(target_os = "linux"))]
Self {
executables_dir: root.to_owned(),
libraries_dir: root.to_owned(),
static_resources_dir: root.to_owned(),
config_dir: root.to_owned(),
log_dir: root.to_owned(),
openvr_driver_root_dir: root.to_owned(),
vrcompositor_wrapper_dir: root.to_owned(),
firewall_script_dir: root.to_owned(),
firewalld_config_dir: root.to_owned(),
ufw_config_dir: root.to_owned(),
vulkan_layer_manifest_dir: root.to_owned(),
}
}

View File

@ -188,6 +188,7 @@ pub enum ClientListAction {
RemoveEntry,
UpdateCurrentIp(Option<IpAddr>),
SetConnectionState(ConnectionState),
SetCabled(bool),
}
#[derive(Serialize, Deserialize, Default, Clone)]

10
alvr/server/cbindgen.toml Normal file
View File

@ -0,0 +1,10 @@
language = "C"
header = "/* ALVR is licensed under the MIT license. https://github.com/alvr-org/ALVR/blob/master/LICENSE */"
pragma_once = true
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
cpp_compat = true
tab_width = 4
documentation_style = "c99"
[enum]
rename_variants = "QualifiedScreamingSnakeCase"

View File

@ -1,6 +1,5 @@
#![allow(dead_code, unused_variables)]
use ash::vk;
use std::{
ffi::{c_char, CStr},
time::Instant,
@ -239,27 +238,29 @@ pub unsafe extern "C" fn alvr_post_vulkan() {
pub unsafe extern "C" fn alvr_create_vk_target_swapchain(
width: u32,
height: u32,
color_format: vk::Format,
color_space: vk::ColorSpaceKHR,
image_usage: vk::ImageUsageFlags,
present_mode: vk::PresentModeKHR,
vk_color_format: i32,
vk_color_space: i32,
vk_image_usage: u32,
vk_present_mode: i32,
image_count: u64,
) {
todo!()
}
// returns vkResult
#[no_mangle]
pub unsafe extern "C" fn alvr_acquire_image(out_swapchain_index: u64) -> vk::Result {
pub unsafe extern "C" fn alvr_acquire_image(out_swapchain_index: u64) -> i32 {
todo!()
}
// returns vkResult
#[no_mangle]
pub unsafe extern "C" fn alvr_present(
queue: vk::Queue,
vk_queue: u64,
swapchain_index: u64,
timeline_semaphore_value: u64,
timestamp_ns: u64,
) -> vk::Result {
) -> i32 {
todo!()
}

View File

@ -524,7 +524,7 @@ fn try_connect(mut client_ips: HashMap<IpAddr, String>) -> ConResult {
let mut video_sender = stream_socket.request_stream(VIDEO);
let game_audio_sender = stream_socket.request_stream(AUDIO);
let microphone_receiver = stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS);
let mut microphone_receiver = stream_socket.subscribe_to_stream(AUDIO, MAX_UNREAD_PACKETS);
let mut tracking_receiver =
stream_socket.subscribe_to_stream::<Tracking>(TRACKING, MAX_UNREAD_PACKETS);
let haptics_sender = stream_socket.request_stream(HAPTICS);
@ -639,11 +639,11 @@ fn try_connect(mut client_ips: HashMap<IpAddr, String>) -> ConResult {
thread::spawn(move || {
alvr_common::show_err(alvr_audio::play_audio_loop(
Arc::clone(&IS_STREAMING),
sink,
&sink,
1,
streaming_caps.microphone_sample_rate,
config.buffering,
microphone_receiver,
&mut microphone_receiver,
));
})
} else {

View File

@ -12,12 +12,15 @@ use alvr_common::{
};
use alvr_events::EventType;
use alvr_packets::{AudioDevicesList, ClientListAction, PathSegment, PathValuePair};
use alvr_session::{ClientConnectionConfig, ConnectionState, SessionConfig, Settings};
use alvr_session::{
ClientConnectionConfig, ConnectionState, SessionConfig, Settings, SocketProtocolDefaultVariant,
};
use cpal::traits::{DeviceTrait, HostTrait};
use serde_json as json;
use std::{
collections::{hash_map::Entry, HashMap},
fs,
net::{IpAddr, Ipv4Addr},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
@ -203,6 +206,7 @@ impl ServerDataManager {
manual_ips: manual_ips.into_iter().collect(),
trusted,
connection_state: ConnectionState::Disconnected,
cabled: false,
};
new_entry.insert(client_connection_desc);
@ -255,6 +259,40 @@ impl ServerDataManager {
}
}
}
ClientListAction::SetCabled(state) => {
if let Entry::Occupied(mut entry) = maybe_client_entry {
entry.get_mut().cabled = state;
if entry.get().cabled {
entry
.get_mut()
.manual_ips
.insert(IpAddr::V4(Ipv4Addr::LOCALHOST));
self.session
.session_settings
.connection
.client_discovery
.enabled = false;
self.session
.session_settings
.connection
.stream_protocol
.variant = SocketProtocolDefaultVariant::Tcp;
} else {
entry
.get_mut()
.manual_ips
.remove(&IpAddr::V4(Ipv4Addr::LOCALHOST));
self.session
.session_settings
.connection
.client_discovery
.enabled = true;
}
updated = true;
}
}
}
if updated {

View File

@ -116,6 +116,7 @@ pub struct ClientConnectionConfig {
pub manual_ips: HashSet<IpAddr>,
pub trusted: bool,
pub connection_state: ConnectionState,
pub cabled: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@ -975,21 +975,31 @@ pub struct RawEventsConfig {
#[schema(collapsible)]
pub struct LoggingConfig {
pub client_log_report_level: Switch<LogSeverity>,
#[schema(strings(help = "Write logs into the session_log.txt file."))]
pub log_to_disk: bool,
#[schema(flag = "real-time")]
pub log_tracking: bool,
#[schema(flag = "real-time")]
pub log_button_presses: bool,
#[schema(flag = "real-time")]
pub log_haptics: bool,
#[schema(flag = "real-time")]
pub notification_level: LogSeverity,
#[schema(flag = "real-time")]
pub show_raw_events: Switch<RawEventsConfig>,
#[schema(strings(help = "This applies only to certain error or warning messages."))]
#[schema(flag = "steamvr-restart")]
pub prefer_backtrace: bool,
#[schema(strings(help = "Notification tips teach you how to use ALVR"))]
pub show_notification_tip: bool,
}
#[derive(SettingsSchema, Serialize, Deserialize, Clone)]
@ -1506,6 +1516,7 @@ pub fn session_settings_default() -> SettingsDefault {
},
},
prefer_backtrace: false,
show_notification_tip: true,
},
steamvr_launcher: SteamvrLauncherDefault {
gui_collapsed: false,

View File

@ -0,0 +1,70 @@
#!/bin/bash
function get_alvr_playback_source_id() {
local last_node_name=''
local last_node_id=''
pactl list $1 | while read -r line; do
node_id=$(echo "$line" | grep -oP "$2 #\K.+" | sed -e 's/^[ \t]*//')
node_name=$(echo "$line" | grep -oP 'node.name = "\K[^"]+' | sed -e 's/^[ \t]*//')
if [[ "$node_id" != '' ]] && [[ "$last_node_id" != "$node_id" ]]; then
last_node_id="$node_id"
fi
if [[ -n "$node_name" ]] && [[ "$last_node_name" != "$node_name" ]]; then
last_node_name="$node_name"
if [[ "$last_node_name" == "$3" ]]; then
echo "$last_node_id"
return
fi
fi
done
}
function get_sink_id() {
local sink_name
sink_name=$1
pactl list short sinks | grep "$sink_name" | cut -d$'\t' -f1
}
function setup_mic() {
echo "Creating microphone sink & source and linking alvr playback to it"
# This sink is required so that it persistently auto-connects to alvr playback later
pactl load-module module-null-sink sink_name=ALVR-MIC-Sink media.class=Audio/Sink
# This source is required so that any app can use it as microphone
pactl load-module module-null-sink sink_name=ALVR-MIC-Source media.class=Audio/Source/Virtual
# We link them together
pw-link ALVR-MIC-Sink ALVR-MIC-Source
# And we assign playback of pipewire alsa playback to created alvr sink
pactl move-sink-input "$(get_alvr_playback_source_id sink-inputs 'Sink Input' 'ALSA plug-in [vrserver]')" "$(get_sink_id ALVR-MIC-Sink)"
}
function setup_audio() {
echo "Setting up audio"
pactl load-module module-null-sink sink_name=ALVR-AUDIO-Sink media.class=Audio/Sink
pactl set-default-sink ALVR-AUDIO-Sink
pactl move-source-output "$(get_alvr_playback_source_id source-outputs 'Source Output' 'ALSA plug-in [vrserver]')" "$(get_sink_id ALVR-AUDIO-Sink)"
}
function unload_mic() {
echo "Unloading microphone sink & source"
pw-cli destroy ALVR-MIC-Sink
pw-cli destroy ALVR-MIC-Source
}
function unload_sink() {
echo "Unloading audio sink"
pw-cli destroy ALVR-AUDIO-Sink
}
case $ACTION in
connect)
unload_sink
unload_mic
sleep 1
setup_mic
setup_audio
;;
disconnect)
unload_mic
unload_sink
;;
esac

View File

@ -26,6 +26,64 @@ impl Display for Profile {
}
}
pub fn build_server_lib(
profile: Profile,
enable_messagebox: bool,
gpl: bool,
root: Option<String>,
reproducible: bool,
) {
let sh = Shell::new().unwrap();
let mut flags = vec![];
match profile {
Profile::Distribution => {
flags.push("--profile");
flags.push("distribution");
}
Profile::Release => flags.push("--release"),
Profile::Debug => (),
}
if enable_messagebox {
flags.push("--features");
flags.push("alvr_common/enable-messagebox");
}
if gpl {
flags.push("--features");
flags.push("gpl");
}
if reproducible {
flags.push("--locked");
}
let flags_ref = &flags;
let artifacts_dir = afs::target_dir().join(profile.to_string());
let build_dir = afs::build_dir().join("alvr_server_core");
sh.create_dir(&build_dir).unwrap();
if let Some(root) = root {
sh.set_var("ALVR_ROOT_DIR", root);
}
let _push_guard = sh.push_dir(afs::crate_dir("server"));
cmd!(sh, "cargo build {flags_ref...}").run().unwrap();
sh.copy_file(
artifacts_dir.join(afs::dynlib_fname("alvr_server")),
&build_dir,
)
.unwrap();
if cfg!(windows) {
sh.copy_file(artifacts_dir.join("alvr_server_core.pdb"), &build_dir)
.unwrap();
}
let out = build_dir.join("alvr_server_core.h");
cmd!(sh, "cbindgen --output {out}").run().unwrap();
}
pub fn build_streamer(
profile: Profile,
enable_messagebox: bool,
@ -64,9 +122,8 @@ pub fn build_streamer(
None
};
sh.remove_path(&afs::streamer_build_dir()).unwrap();
sh.create_dir(&build_layout.openvr_driver_lib_dir())
.unwrap();
sh.remove_path(afs::streamer_build_dir()).unwrap();
sh.create_dir(build_layout.openvr_driver_lib_dir()).unwrap();
sh.create_dir(&build_layout.executables_dir).unwrap();
if let Some(config) = maybe_config {
@ -235,9 +292,6 @@ pub fn build_launcher(profile: Profile, enable_messagebox: bool, reproducible: b
pub fn build_client_lib(profile: Profile, link_stdcpp: bool) {
let sh = Shell::new().unwrap();
let build_dir = afs::build_dir().join("alvr_client_core");
sh.create_dir(&build_dir).unwrap();
let strip_flag = matches!(profile, Profile::Debug).then_some("--no-strip");
let mut flags = vec![];
@ -254,6 +308,9 @@ pub fn build_client_lib(profile: Profile, link_stdcpp: bool) {
}
let flags_ref = &flags;
let build_dir = afs::build_dir().join("alvr_client_core");
sh.create_dir(&build_dir).unwrap();
let _push_guard = sh.push_dir(afs::crate_dir("client_core"));
cmd!(

View File

@ -130,6 +130,7 @@ pub fn build_ffmpeg_linux(nvenc_flag: bool) {
"--enable-libdrm",
"--enable-pic",
"--enable-rpath",
"--fatal-warnings",
];
let install_prefix = format!("--prefix={}", final_path.join("alvr_build").display());
// The reason for 4x$ in LDSOFLAGS var refer to https://stackoverflow.com/a/71429999
@ -156,6 +157,29 @@ pub fn build_ffmpeg_linux(nvenc_flag: bool) {
*/
#[cfg(target_os = "linux")]
{
let codec_header_version = "12.1.14.0";
let temp_download_dir = download_path.join("dl_temp");
command::download_and_extract_zip(
&format!("https://github.com/FFmpeg/nv-codec-headers/archive/refs/tags/n{codec_header_version}.zip"),
&temp_download_dir
)
.unwrap();
let header_dir = download_path.join("nv-codec-headers");
let header_build_dir = header_dir.join("build");
fs::rename(
temp_download_dir.join(format!("nv-codec-headers-n{codec_header_version}")),
&header_dir,
)
.unwrap();
fs::remove_dir_all(temp_download_dir).unwrap();
{
let make_header_cmd =
format!("make install PREFIX='{}'", header_build_dir.display());
let _header_push_guard = sh.push_dir(&header_dir);
cmd!(sh, "bash -c {make_header_cmd}").run().unwrap();
}
let cuda = pkg_config::Config::new().probe("cuda").unwrap();
let include_flags = cuda
.include_paths
@ -181,11 +205,16 @@ pub fn build_ffmpeg_linux(nvenc_flag: bool) {
&format!("--extra-ldflags=\"{link_flags}\""),
];
let env_vars = format!(
"PKG_CONFIG_PATH='{}'",
header_build_dir.join("lib/pkgconfig").display()
);
let flags_combined = flags.join(" ");
let nvenc_flags_combined = nvenc_flags.join(" ");
let command =
format!("./configure {install_prefix} {flags_combined} {nvenc_flags_combined}");
let command = format!(
"{env_vars} ./configure {install_prefix} {flags_combined} {nvenc_flags_combined}"
);
cmd!(sh, "bash -c {command}").run().unwrap();
}

View File

@ -22,14 +22,15 @@ SUBCOMMANDS:
prepare-deps Download and compile streamer and client external dependencies
build-streamer Build streamer, then copy binaries to build folder
build-launcher Build launcher, then copy binaries to build folder
build-server-lib Build a C-ABI ALVR server library and header
build-client Build client, then copy binaries to build folder
build-client-lib Build a C-ABI ALVR client library and header.
build-client-lib Build a C-ABI ALVR client library and header
run-streamer Build streamer and then open the dashboard
run-launcher Build launcher and then open it
package-streamer Build streamer with distribution profile, make archive
package-launcher Build launcher in release mode, make portable and installer versions
package-client-lib Build client library then zip it
clean Removes all build artifacts and dependencies.
clean Removes all build artifacts and dependencies
bump Bump streamer and client package versions
clippy Show warnings for selected clippy lints
kill-oculus Kill all Oculus processes
@ -190,6 +191,7 @@ fn main() {
build::build_streamer(profile, true, gpl, None, false, keep_config)
}
"build-launcher" => build::build_launcher(profile, true, false),
"build-server-lib" => build::build_server_lib(profile, true, gpl, None, false),
"build-client" => build::build_android_client(profile),
"build-client-lib" => build::build_client_lib(profile, link_stdcpp),
"run-streamer" => {

View File

@ -8,18 +8,6 @@ You need to install [rustup](https://www.rust-lang.org/tools/install).
On Windows you need also [Chocolatey](https://chocolatey.org/install).
# Linux Users
Before building the streamer, those on Linux would have to build and install [`FFmpeg/nv-codec-headers`](https://github.com/FFmpeg/nv-codec-headers). The nv-codec-headers for nvidia users requires at least driver version `520.56.06` to work properly, taken from nv-codec-header's README.
Run the following commands as shown:
```bash
git clone https://github.com/FFmpeg/nv-codec-headers.git
cd nv-codec-headers/
sudo make install
```
# Streamer Building
First you need to gather some additional resources in preparation for the build.

View File

@ -110,6 +110,18 @@ flatpak run --command=alvr_dashboard com.valvesoftware.Steam
A desktop file named `com.valvesoftware.Steam.Utility.alvr.desktop` is supplied within the `alvr/xtask/flatpak` directory. Move this to where other desktop files are located on your system in order to run the dashboard without the terminal.
### Automatic Audio & Microphone setup
Currently the game audio and microphone to and from the headset isn't routed automatically. The setup of this script will therefore run every time the headset connects or disconnects to the ALVR dashboard. This is based on [the steps](Installation-guide.md#automatic-audio--microphone-setup) in the installation guide, modified for the Flatpak.
1. In the ALVR Dashboard under All Settings (Advanced) > Audio, enable Game Audio and Microphone.
2. In the same place under Microphone, click Expand and set Devices to custom. Enter `default` for the name for both Sink and Source.
3. Download the [audio-flatpak-setup.sh](../alvr/xtask/flatpak/audio-flatpak-setup.sh) script and place it into the Flatpak app data directory located at `~/.var/app/com.valvesoftware.Steam/`. Make sure it has execute permissions (e.g. `chmod +x audio-flatpak-setup.sh`).
4. In the ALVR Dashboard, under All Settings (Advanced) > Connection, set the On connect script and On disconnect script to the absolute path of the script (relative to the Flatpak environment), e.g. `/var/home/$USERNAME/audio-flatpak-setup.sh`.
### Other Applications
The support for other applications that are not launched via Steam is non-existent due to the Flatpak sandbox.