Compare commits
10 Commits
e04ff1aef3
...
a42f673c75
Author | SHA1 | Date |
---|---|---|
Riccardo Zaglia | a42f673c75 | |
Meister1593 | 0b47edc1b5 | |
Riccardo Zaglia | 7d627ca8fa | |
Nova King | 7eee4e947a | |
Bryce Torcello | 5e54bf19c9 | |
Riccardo Zaglia | 7be28bbd6c | |
Jarett Millard | efe629bda3 | |
Riccardo Zaglia | 2b17f057a0 | |
Riccardo Zaglia | 6374dee48e | |
Riccardo Zaglia | 85990bee0d |
|
@ -270,6 +270,7 @@ dependencies = [
|
|||
"gloo-net",
|
||||
"ico",
|
||||
"instant",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings-schema",
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(|| ())
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -188,6 +188,7 @@ pub enum ClientListAction {
|
|||
RemoveEntry,
|
||||
UpdateCurrentIp(Option<IpAddr>),
|
||||
SetConnectionState(ConnectionState),
|
||||
SetCabled(bool),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Clone)]
|
||||
|
|
|
@ -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"
|
|
@ -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!()
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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!(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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" => {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue