feat: Add streaming support (#125)

This adds:
- [x] Keyboard and mouse handling on the frontend
- [x] Video and audio streaming from the backend to the frontend
- [x] Input server that works with Websockets

Update - 17/11
- [ ] Master docker container to run this
- [ ] Steam runtime
- [ ] Entrypoint.sh

---------

Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Kristian Ollikainen <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
Wanjohi
2024-12-08 14:54:56 +03:00
committed by GitHub
parent 5eb21eeadb
commit 379db1c87b
137 changed files with 12737 additions and 5234 deletions

233
packages/server/src/gpu.rs Normal file
View File

@@ -0,0 +1,233 @@
use regex::Regex;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::str;
#[derive(Debug, Eq, PartialEq, Clone)]
pub enum GPUVendor {
UNKNOWN,
INTEL,
NVIDIA,
AMD,
}
#[derive(Debug, Clone)]
pub struct GPUInfo {
vendor: GPUVendor,
card_path: String,
render_path: String,
device_name: String,
}
impl GPUInfo {
pub fn vendor(&self) -> &GPUVendor {
&self.vendor
}
pub fn vendor_string(&self) -> &str {
match self.vendor {
GPUVendor::INTEL => "Intel",
GPUVendor::NVIDIA => "NVIDIA",
GPUVendor::AMD => "AMD",
GPUVendor::UNKNOWN => "Unknown",
}
}
pub fn card_path(&self) -> &str {
&self.card_path
}
pub fn render_path(&self) -> &str {
&self.render_path
}
pub fn device_name(&self) -> &str {
&self.device_name
}
}
fn get_gpu_vendor(vendor_id: &str) -> GPUVendor {
match vendor_id {
"8086" => GPUVendor::INTEL, // Intel
"10de" => GPUVendor::NVIDIA, // NVIDIA
"1002" => GPUVendor::AMD, // AMD/ATI
_ => GPUVendor::UNKNOWN,
}
}
/// Retrieves a list of GPUs available on the system.
/// # Returns
/// * `Vec<GPUInfo>` - A vector containing information about each GPU.
pub fn get_gpus() -> Vec<GPUInfo> {
let mut gpus = Vec::new();
// Run lspci to get PCI devices related to GPUs
let lspci_output = Command::new("lspci")
.arg("-mmnn") // Get machine-readable output with IDs
.output()
.expect("Failed to execute lspci");
let output = str::from_utf8(&lspci_output.stdout).unwrap();
// Filter lines that mention VGA or 3D controller
for line in output.lines() {
if line.to_lowercase().contains("vga compatible controller")
|| line.to_lowercase().contains("3d controller")
|| line.to_lowercase().contains("display controller")
{
if let Some((pci_addr, vendor_id, device_name)) = parse_pci_device(line) {
// Run udevadm to get the device path
if let Some((card_path, render_path)) = get_dri_device_path(&pci_addr) {
let vendor = get_gpu_vendor(&vendor_id);
gpus.push(GPUInfo {
vendor,
card_path,
render_path,
device_name,
});
}
}
}
}
gpus
}
/// Parses a line from the lspci output to extract PCI device information.
/// # Arguments
/// * `line` - A string slice that holds a line from the 'lspci -mmnn' output.
/// # Returns
/// * `Option<(String, String, String)>` - A tuple containing the PCI address, vendor ID, and device name if parsing is successful.
fn parse_pci_device(line: &str) -> Option<(String, String, String)> {
// Define a regex pattern to match PCI device lines
let re = Regex::new(r#"(?P<pci_addr>[0-9a-fA-F]{1,2}:[0-9a-fA-F]{2}\.[0-9]) "[^"]+" "(?P<vendor_name>[^"]+)" "(?P<device_name>[^"]+)"#).unwrap();
// Collect all matched groups
let parts: Vec<(String, String, String)> = re
.captures_iter(line)
.filter_map(|cap| {
Some((
cap["pci_addr"].to_string(),
cap["vendor_name"].to_string(),
cap["device_name"].to_string(),
))
})
.collect();
if let Some((pci_addr, vendor_name, device_name)) = parts.first() {
// Create a mutable copy of the device name to modify
let mut device_name = device_name.clone();
// If more than 1 square-bracketed item is found, remove last one, otherwise remove all
if let Some(start) = device_name.rfind('[') {
if let Some(_) = device_name.rfind(']') {
device_name = device_name[..start].trim().to_string();
}
}
// Extract vendor ID from vendor name (e.g., "Intel Corporation [8086]" or "Advanced Micro Devices, Inc. [AMD/ATI] [1002]")
let vendor_id = vendor_name
.split_whitespace()
.last()
.unwrap_or_default()
.trim_matches(|c: char| !c.is_ascii_hexdigit())
.to_string();
return Some((pci_addr.clone(), vendor_id, device_name));
}
None
}
/// Retrieves the DRI device paths for a given PCI address.
/// Doubles as a way to verify the device is a GPU.
/// # Arguments
/// * `pci_addr` - A string slice that holds the PCI address.
/// # Returns
/// * `Option<(String, String)>` - A tuple containing the card path and render path if found.
fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
// Construct the base directory in /sys/bus/pci/devices to start the search
let base_dir = Path::new("/sys/bus/pci/devices");
// Define the target PCI address with "0000:" prefix
let target_addr = format!("0000:{}", pci_addr);
// Search for a matching directory that contains the target PCI address
for entry in fs::read_dir(base_dir).ok()?.flatten() {
let path = entry.path();
// Check if the path matches the target PCI address
if path.to_string_lossy().contains(&target_addr) {
// Look for any files under the 'drm' subdirectory, like 'card0' or 'renderD128'
let drm_path = path.join("drm");
if drm_path.exists() {
let mut card_path = String::new();
let mut render_path = String::new();
for drm_entry in fs::read_dir(drm_path).ok()?.flatten() {
let file_name = drm_entry.file_name();
if let Some(name) = file_name.to_str() {
if name.starts_with("card") {
card_path = format!("/dev/dri/{}", name);
}
if name.starts_with("renderD") {
render_path = format!("/dev/dri/{}", name);
}
// If both paths are found, break the loop
if !card_path.is_empty() && !render_path.is_empty() {
break;
}
}
}
return Some((card_path, render_path));
}
}
}
None
}
/// Helper method to get GPUs from vector by vendor name (case-insensitive).
/// # Arguments
/// * `gpus` - A vector containing information about each GPU.
/// * `vendor` - A string slice that holds the vendor name.
/// # Returns
/// * `Vec<GPUInfo>` - A vector containing GPUInfo structs if found.
pub fn get_gpus_by_vendor(gpus: &Vec<GPUInfo>, vendor: &str) -> Vec<GPUInfo> {
let vendor = vendor.to_lowercase();
gpus.iter()
.filter(|gpu| gpu.vendor_string().to_lowercase().contains(&vendor))
.cloned()
.collect()
}
/// Helper method to get GPUs from vector by device name substring (case-insensitive).
/// # Arguments
/// * `gpus` - A vector containing information about each GPU.
/// * `device_name` - A string slice that holds the device name substring.
/// # Returns
/// * `Vec<GPUInfo>` - A vector containing GPUInfo structs if found.
pub fn get_gpus_by_device_name(gpus: &Vec<GPUInfo>, device_name: &str) -> Vec<GPUInfo> {
let device_name = device_name.to_lowercase();
gpus.iter()
.filter(|gpu| gpu.device_name.to_lowercase().contains(&device_name))
.cloned()
.collect()
}
/// Helper method to get a GPU from vector of GPUInfo by card path (either /dev/dri/cardX or /dev/dri/renderDX, case-insensitive).
/// # Arguments
/// * `gpus` - A vector containing information about each GPU.
/// * `card_path` - A string slice that holds the card path.
/// # Returns
/// * `Option<GPUInfo>` - A reference to a GPUInfo struct if found.
pub fn get_gpu_by_card_path(gpus: &Vec<GPUInfo>, card_path: &str) -> Option<GPUInfo> {
for gpu in gpus {
if gpu.card_path().to_lowercase() == card_path.to_lowercase()
|| gpu.render_path().to_lowercase() == card_path.to_lowercase()
{
return Some(gpu.clone());
}
}
None
}