mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
feat(runner): Container detection and handling, video bit-depth flags and script updates (#303)
## Description Works in apptainer now.. podman is still the goat since apptainer needs docker treatment and even more.. - Added container detection so podman can be used to it's fullest, the non-sane ones are handled separately.. - Added video bit-depth option, cuz AV1 and 10-bit encoding go well together. - Some other package updates to nestri-server. - General tidying up of scripts to make multi-container-engine handling less of a pain. - Updated old wireplumber lua script to new json format. Further changes: - Removed unused debug arg from nestri-server. - Moved configs to config file folder rather than keeping them in containerfile. - Improved audio configs, moved some into wireplumber to keep things tidy. - Bit better arg handling in nestri-server. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Optional 10‑bit video support and auto‑launch of an app after display setup. * **Changes** * Standardized runtime/user env to NESTRI_* with updated home/cache paths and explicit LANG; password generation now logged. * Improved container/GPU detection and startup logging; reduced blanket root usage during startup; SSH setup surfaced. * WirePlumber/PipeWire moved to JSON configs; low‑latency clock and loopback audio policies added; audio capture defaults to PipeWire. * **Chores** * GStreamer/libp2p dependency upgrades and Rust toolchain pinned; NVIDIA driver capability exposed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
aba0bc3be1
commit
590fe5e196
@@ -86,7 +86,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
|
||||
|
||||
# Clone repository
|
||||
RUN git clone --depth 1 -b "dev-dmabuf" https://github.com/DatCaptainHorse/gst-wayland-display.git
|
||||
RUN git clone --depth 1 --rev "dfeebb19b48f32207469e166a3955f5d65b5e6c6" https://github.com/games-on-whales/gst-wayland-display.git
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
FROM gst-wayland-deps AS gst-wayland-planner
|
||||
@@ -121,58 +121,50 @@ RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
||||
#******************************************************************************
|
||||
FROM base AS runtime
|
||||
|
||||
### System Configuration ###
|
||||
RUN sed -i \
|
||||
-e '/#\[multilib\]/,/#Include = \/etc\/pacman.d\/mirrorlist/ s/#//' \
|
||||
-e "s/#Color/Color/" /etc/pacman.conf && \
|
||||
pacman --noconfirm -Sy archlinux-keyring && \
|
||||
dirmngr </dev/null > /dev/null 2>&1
|
||||
|
||||
### Package Installation ###
|
||||
# Core system components
|
||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||
pacman -Sy --needed --noconfirm \
|
||||
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
||||
vulkan-radeon lib32-vulkan-radeon \
|
||||
mesa \
|
||||
steam steam-native-runtime proton-cachyos gtk3 lib32-gtk3 \
|
||||
mesa steam-native-runtime proton-cachyos lib32-mesa \
|
||||
steam gtk3 lib32-gtk3 \
|
||||
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
||||
libssh2 curl wget \
|
||||
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
||||
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
|
||||
openssh && \
|
||||
hwdata openssh \
|
||||
# GStreamer stack
|
||||
pacman -Sy --needed --noconfirm \
|
||||
gstreamer gst-plugins-base gst-plugins-good \
|
||||
gst-plugins-bad gst-plugin-pipewire \
|
||||
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
|
||||
gst-plugin-va gst-plugin-qsv && \
|
||||
gst-plugin-va gst-plugin-qsv \
|
||||
# lib32 GStreamer stack to fix some games with videos
|
||||
pacman -Sy --needed --noconfirm \
|
||||
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
|
||||
# Cleanup
|
||||
paccache -rk1 && \
|
||||
rm -rf /usr/share/{info,man,doc}/*
|
||||
|
||||
### User Configuration ###
|
||||
ENV USER="nestri" \
|
||||
UID=1000 \
|
||||
GID=1000 \
|
||||
USER_PWD="nestri1234" \
|
||||
XDG_RUNTIME_DIR=/run/user/1000 \
|
||||
HOME=/home/nestri \
|
||||
ARG NESTRI_USER_PWD=""
|
||||
ENV NESTRI_USER="nestri" \
|
||||
NESTRI_UID=1000 \
|
||||
NESTRI_GID=1000 \
|
||||
NESTRI_LANG=en_US.UTF-8 \
|
||||
NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \
|
||||
NESTRI_HOME=/home/nestri \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all
|
||||
|
||||
RUN mkdir -p /home/${USER} && \
|
||||
groupadd -g ${GID} ${USER} && \
|
||||
useradd -d /home/${USER} -u ${UID} -g ${GID} -s /bin/bash ${USER} && \
|
||||
chown -R ${USER}:${USER} /home/${USER} && \
|
||||
echo "${USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
||||
echo "${USER}:${USER_PWD}" | chpasswd && \
|
||||
mkdir -p /run/user/${UID} && \
|
||||
chown ${USER}:${USER} /run/user/${UID} && \
|
||||
usermod -aG input,video,render,seat root && \
|
||||
usermod -aG input,video,render,seat ${USER}
|
||||
RUN mkdir -p "/home/${NESTRI_USER}" && \
|
||||
groupadd -g "${NESTRI_GID}" "${NESTRI_USER}" && \
|
||||
useradd -d "/home/${NESTRI_USER}" -u "${NESTRI_UID}" -g "${NESTRI_GID}" -s /bin/bash "${NESTRI_USER}" && \
|
||||
echo "${NESTRI_USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
||||
NESTRI_USER_PWD="${NESTRI_USER_PWD:-$(openssl rand -base64 12)}" && \
|
||||
echo "Setting password for ${NESTRI_USER} as: ${NESTRI_USER_PWD}" && \
|
||||
echo "${NESTRI_USER}:${NESTRI_USER_PWD}" | chpasswd && \
|
||||
mkdir -p "${NESTRI_XDG_RUNTIME_DIR}" && \
|
||||
chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_XDG_RUNTIME_DIR}" && \
|
||||
usermod -aG input,video,render,seat "${NESTRI_USER}"
|
||||
|
||||
### System Services Configuration ###
|
||||
RUN mkdir -p /run/dbus && \
|
||||
@@ -182,29 +174,17 @@ RUN mkdir -p /run/dbus && \
|
||||
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
|
||||
/usr/share/wireplumber/wireplumber.conf
|
||||
|
||||
### PipeWire Latency Optimizations (1-5ms instead of 20ms) ###
|
||||
### Audio Systems Configs - Latency optimizations + Loopback ###
|
||||
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
|
||||
echo "[audio]\
|
||||
\n default.clock.rate = 48000\
|
||||
\n default.clock.quantum = 128\
|
||||
\n default.clock.min-quantum = 128\
|
||||
\n default.clock.max-quantum = 256" > /etc/pipewire/pipewire.conf.d/low-latency.conf && \
|
||||
mkdir -p /etc/wireplumber/main.lua.d && \
|
||||
echo 'table.insert(default_nodes.rules, {\
|
||||
\n matches = { { { "node.name", "matches", ".*" } } },\
|
||||
\n apply_properties = {\
|
||||
\n ["audio.format"] = "S16LE",\
|
||||
\n ["audio.rate"] = 48000,\
|
||||
\n ["audio.channels"] = 2,\
|
||||
\n ["api.alsa.period-size"] = 128,\
|
||||
\n ["api.alsa.headroom"] = 0,\
|
||||
\n ["session.suspend-timeout-seconds"] = 0\
|
||||
\n }\
|
||||
\n})' > /etc/wireplumber/main.lua.d/50-low-latency.lua && \
|
||||
echo "default-fragments = 2\
|
||||
\ndefault-fragment-size-msec = 2" >> /etc/pulse/daemon.conf && \
|
||||
echo "load-module module-loopback latency_msec=1" >> /etc/pipewire/pipewire.conf.d/loopback.conf
|
||||
mkdir -p /etc/wireplumber/wireplumber.conf.d
|
||||
|
||||
COPY packages/configs/wireplumber.conf.d/* /etc/wireplumber/wireplumber.conf.d/
|
||||
COPY packages/configs/pipewire.conf.d/* /etc/pipewire/pipewire.conf.d/
|
||||
|
||||
## Steam Configs - Proton (CachyOS flavor) ##
|
||||
RUN mkdir -p "${NESTRI_HOME}/.local/share/Steam/config"
|
||||
|
||||
COPY packages/configs/steam/config.vdf "${NESTRI_HOME}/.local/share/Steam/config/"
|
||||
|
||||
### Artifacts and Verification ###
|
||||
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
|
||||
@@ -215,6 +195,10 @@ RUN which nestri-server && ls -la /usr/lib/ | grep 'gstwaylanddisplay'
|
||||
### Scripts and Final Configuration ###
|
||||
COPY packages/scripts/ /etc/nestri/
|
||||
RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
|
||||
locale-gen
|
||||
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" && \
|
||||
sed -i 's/^#\(en_US\.UTF-8\)/\1/' /etc/locale.gen && \
|
||||
LANG=en_US.UTF-8 locale-gen
|
||||
|
||||
# Root for most container engines, nestri-user compatible for apptainer without fakeroot
|
||||
USER root
|
||||
ENTRYPOINT ["supervisord", "-c", "/etc/nestri/supervisord.conf"]
|
||||
|
||||
16
packages/configs/pipewire.conf.d/nestri-loopback.conf
Normal file
16
packages/configs/pipewire.conf.d/nestri-loopback.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
context.modules = [
|
||||
{
|
||||
name = libpipewire-module-loopback
|
||||
args = {
|
||||
node.description = "Loopback"
|
||||
capture.props = {
|
||||
node.name = "Loopback Capture"
|
||||
media.class = "Audio/Sink"
|
||||
}
|
||||
playback.props = {
|
||||
node.name = "Loopback Playback"
|
||||
media.class = "Audio/Source"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
7
packages/configs/pipewire.conf.d/nestri-low-latency.conf
Normal file
7
packages/configs/pipewire.conf.d/nestri-low-latency.conf
Normal file
@@ -0,0 +1,7 @@
|
||||
context.properties = {
|
||||
default.clock.rate = 48000
|
||||
default.clock.allowed-rates = [48000]
|
||||
default.clock.min-quantum = 128
|
||||
default.clock.max-quantum = 256
|
||||
default.clock.quantum = 128
|
||||
}
|
||||
21
packages/configs/steam/config.vdf
Normal file
21
packages/configs/steam/config.vdf
Normal file
@@ -0,0 +1,21 @@
|
||||
"InstallConfigStore"
|
||||
{
|
||||
"Software"
|
||||
{
|
||||
"Valve"
|
||||
{
|
||||
"Steam"
|
||||
{
|
||||
"CompatToolMapping"
|
||||
{
|
||||
"0"
|
||||
{
|
||||
"name" "proton-cachyos"
|
||||
"config" ""
|
||||
"priority" "75"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
packages/configs/wireplumber.conf.d/nestri-low-latency.conf
Normal file
30
packages/configs/wireplumber.conf.d/nestri-low-latency.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"wireplumber.rules": [
|
||||
{
|
||||
"description": "Global audio format and rate for audio nodes",
|
||||
"matches": [
|
||||
{ "media.class": "Audio/Sink" },
|
||||
{ "media.class": "Audio/Source" }
|
||||
],
|
||||
"apply_properties": {
|
||||
"audio.format": "F32P",
|
||||
"audio.rate": 48000,
|
||||
"audio.channels": 2,
|
||||
"session.suspend-timeout-seconds": 0
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"description": "PulseAudio bridge specific tweaks",
|
||||
"matches": [
|
||||
{ "node.name": "pulse_sink" },
|
||||
{ "node.name": "pulse_source" }
|
||||
],
|
||||
"apply_properties": {
|
||||
"pulse.min.req": 128,
|
||||
"pulse.max.req": 256,
|
||||
"pulse.idle.timeout": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
23
packages/scripts/common.sh
Normal file
23
packages/scripts/common.sh
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
function log {
|
||||
printf '[%s] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*"
|
||||
}
|
||||
|
||||
if [[ ! -f /etc/nestri/envs.sh ]]; then
|
||||
log "Error: Environment variables script not found at /etc/nestri/envs.sh"
|
||||
exit 1
|
||||
fi
|
||||
source /etc/nestri/envs.sh || { log "Error: Failed to source /etc/nestri/envs.sh"; exit 1; }
|
||||
|
||||
if [[ ! -f /etc/nestri/container_helpers.sh ]]; then
|
||||
log "Error: Container helpers script not found at /etc/nestri/container_helpers.sh"
|
||||
exit 1
|
||||
fi
|
||||
source /etc/nestri/container_helpers.sh || { log "Error: Failed to source /etc/nestri/container_helpers.sh"; exit 1; }
|
||||
|
||||
if [[ ! -f /etc/nestri/gpu_helpers.sh ]]; then
|
||||
log "Error: GPU helpers script not found at /etc/nestri/gpu_helpers.sh"
|
||||
exit 1
|
||||
fi
|
||||
source /etc/nestri/gpu_helpers.sh || { log "Error: Failed to source /etc/nestri/gpu_helpers.sh"; exit 1; }
|
||||
79
packages/scripts/container_helpers.sh
Normal file
79
packages/scripts/container_helpers.sh
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
declare container_runtime="none"
|
||||
declare -Ag container_info=()
|
||||
|
||||
function detect_container_runtime {
|
||||
if [[ -n "${SINGULARITY_CONTAINER:-}" ]] || [[ -n "${APPTAINER_CONTAINER:-}" ]] || [[ -d "/.singularity.d" ]]; then
|
||||
echo "apptainer"
|
||||
elif [[ "${container:-}" == "podman" ]] || [[ -f "/run/.containerenv" ]]; then
|
||||
echo "podman"
|
||||
elif [[ -f "/.dockerenv" ]]; then
|
||||
echo "docker"
|
||||
else
|
||||
# General check for containerization signs
|
||||
if grep -qE "docker|lxc|kubepods|containerd" "/proc/1/cgroup" 2>/dev/null; then
|
||||
echo "unknown"
|
||||
else
|
||||
echo "none"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function collect_container_info {
|
||||
local runtime="$1"
|
||||
case "$runtime" in
|
||||
apptainer)
|
||||
container_info["runtime"]="apptainer"
|
||||
container_info["version"]="${SINGULARITY_VERSION:-${APPTAINER_VERSION:-unknown}}"
|
||||
container_info["image"]="${SINGULARITY_CONTAINER:-${APPTAINER_CONTAINER:-unknown}}"
|
||||
;;
|
||||
podman)
|
||||
container_info["runtime"]="podman"
|
||||
if [[ -f "/run/.containerenv" ]]; then
|
||||
if grep -q "name=" "/run/.containerenv" 2>/dev/null; then
|
||||
container_info["name"]=$(grep "^name=" "/run/.containerenv" | sed 's/^name=//' | tr -d '"' | xargs)
|
||||
fi
|
||||
if grep -q "image=" "/run/.containerenv" 2>/dev/null; then
|
||||
container_info["image"]=$(grep "^image=" "/run/.containerenv" | sed 's/^image=//' | tr -d '"' | xargs)
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
docker)
|
||||
container_info["runtime"]="docker"
|
||||
container_info["detected_via"]="dockerenv"
|
||||
;;
|
||||
unknown)
|
||||
container_info["runtime"]="unknown"
|
||||
container_info["detected_via"]="cgroup_generic"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function get_container_info {
|
||||
container_runtime=$(detect_container_runtime)
|
||||
if [[ "${container_runtime}" != "none" ]]; then
|
||||
collect_container_info "$container_runtime"
|
||||
fi
|
||||
}
|
||||
|
||||
function debug_container_info {
|
||||
echo "Container Detection Results:"
|
||||
echo "> Runtime: $container_runtime"
|
||||
|
||||
if [[ "$container_runtime" != "none" ]]; then
|
||||
for key in "${!container_info[@]}"; do
|
||||
echo ">> $key: ${container_info[$key]}"
|
||||
done
|
||||
else
|
||||
echo "> Status: Not running in a known container"
|
||||
fi
|
||||
}
|
||||
|
||||
# # Usage examples:
|
||||
# get_container_info
|
||||
# debug_container_info
|
||||
|
||||
# # Get runtime
|
||||
# echo "Container runtime: ${container_runtime}"
|
||||
@@ -1,20 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Common helpers as requirement
|
||||
if [[ -f /etc/nestri/common.sh ]]; then
|
||||
source /etc/nestri/common.sh
|
||||
else
|
||||
echo "Error: Common script not found at /etc/nestri/common.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
CACHE_DIR="/home/nestri/.cache/nvidia"
|
||||
CACHE_DIR="${NESTRI_HOME}/.cache/nestri"
|
||||
NVIDIA_INSTALLER_DIR="/tmp"
|
||||
TIMEOUT_SECONDS=10
|
||||
|
||||
log() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
ENTCMD_PREFIX=""
|
||||
|
||||
# Ensures user directory ownership
|
||||
chown_user_directory() {
|
||||
local user_group="${USER}:${GID}"
|
||||
if ! chown -h --no-preserve-root "$user_group" "${HOME}" 2>/dev/null; then
|
||||
echo "Error: Failed to change ownership of ${HOME} to ${user_group}" >&2
|
||||
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" 2>/dev/null; then
|
||||
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -38,8 +42,8 @@ wait_for_socket() {
|
||||
|
||||
# Prepares environment for namespace-less applications (like Steam)
|
||||
setup_namespaceless() {
|
||||
rm -f /run/systemd/container || true
|
||||
mkdir -p /run/pressure-vessel || true
|
||||
$ENTCMD_PREFIX rm -f /run/systemd/container || true
|
||||
$ENTCMD_PREFIX mkdir -p /run/pressure-vessel || true
|
||||
}
|
||||
|
||||
# Ensures cache directory exists
|
||||
@@ -49,7 +53,7 @@ setup_cache() {
|
||||
log "Warning: Failed to create cache directory, continuing without cache."
|
||||
return 1
|
||||
}
|
||||
chown nestri:nestri "$CACHE_DIR" 2>/dev/null || {
|
||||
$ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "$CACHE_DIR" 2>/dev/null || {
|
||||
log "Warning: Failed to set cache directory ownership, continuing..."
|
||||
}
|
||||
}
|
||||
@@ -94,7 +98,7 @@ get_nvidia_installer() {
|
||||
|
||||
# Cache the downloaded file
|
||||
cp "$tmp_file" "$cached_file" 2>/dev/null && \
|
||||
chown nestri:nestri "$cached_file" 2>/dev/null || \
|
||||
$ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "$cached_file" 2>/dev/null || \
|
||||
log "Warning: Failed to cache NVIDIA driver, continuing..."
|
||||
fi
|
||||
|
||||
@@ -109,7 +113,7 @@ get_nvidia_installer() {
|
||||
install_nvidia_driver() {
|
||||
local filename="$1"
|
||||
log "Installing NVIDIA driver components from $filename..."
|
||||
bash ./"$filename" \
|
||||
$ENTCMD_PREFIX bash ./"$filename" \
|
||||
--silent \
|
||||
--skip-depmod \
|
||||
--skip-module-unload \
|
||||
@@ -120,24 +124,32 @@ install_nvidia_driver() {
|
||||
--no-systemd \
|
||||
--no-rpms \
|
||||
--no-backup \
|
||||
--no-distro-scripts \
|
||||
--no-libglx-indirect \
|
||||
--no-install-libglvnd \
|
||||
--no-check-for-alternate-installs || {
|
||||
log "Error: NVIDIA driver installation failed."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install CUDA package
|
||||
log "Checking if CUDA is already installed"
|
||||
if ! pacman -Q cuda &>/dev/null; then
|
||||
log "Installing CUDA package"
|
||||
pacman -S --noconfirm cuda --assume-installed opencl-nvidia
|
||||
else
|
||||
log "CUDA package is already installed, skipping"
|
||||
fi
|
||||
|
||||
log "NVIDIA driver installation completed."
|
||||
return 0
|
||||
}
|
||||
|
||||
log_container_info() {
|
||||
if ! declare -p container_runtime &>/dev/null; then
|
||||
log "Warning: container_runtime is not defined"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "${container_runtime:-none}" != "none" ]]; then
|
||||
log "Detected container:"
|
||||
log "> ${container_runtime}"
|
||||
else
|
||||
log "No container runtime detected"
|
||||
fi
|
||||
}
|
||||
|
||||
log_gpu_info() {
|
||||
if ! declare -p vendor_devices &>/dev/null; then
|
||||
log "Warning: vendor_devices array is not defined"
|
||||
@@ -164,23 +176,17 @@ configure_ssh() {
|
||||
log "Configuring SSH server on port ${SSH_ENABLE_PORT} with public key authentication"
|
||||
|
||||
# Ensure SSH host keys exist
|
||||
ssh-keygen -A 2>/dev/null || {
|
||||
$ENTCMD_PREFIX ssh-keygen -A 2>/dev/null || {
|
||||
log "Error: Failed to generate SSH host keys"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Create .ssh directory and authorized_keys file for nestri user
|
||||
mkdir -p /home/nestri/.ssh
|
||||
echo "${SSH_ALLOWED_KEY}" > /home/nestri/.ssh/authorized_keys
|
||||
chmod 700 /home/nestri/.ssh
|
||||
chmod 600 /home/nestri/.ssh/authorized_keys
|
||||
chown -R nestri:nestri /home/nestri/.ssh
|
||||
|
||||
# Update SSHD config
|
||||
sed -i -E "s/^#?Port .*/Port ${SSH_ENABLE_PORT}/" /etc/ssh/sshd_config || {
|
||||
log "Error: Failed to update SSH port configuration"
|
||||
return 1
|
||||
}
|
||||
mkdir -p "${NESTRI_HOME}/.ssh"
|
||||
echo "${SSH_ALLOWED_KEY}" > "${NESTRI_HOME}/.ssh/authorized_keys"
|
||||
chmod 700 "${NESTRI_HOME}/.ssh"
|
||||
chmod 600 "${NESTRI_HOME}/.ssh/authorized_keys"
|
||||
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.ssh"
|
||||
|
||||
# Configure secure SSH settings
|
||||
{
|
||||
@@ -190,12 +196,14 @@ configure_ssh() {
|
||||
echo "UsePAM no"
|
||||
echo "PubkeyAuthentication yes"
|
||||
} | while read -r line; do
|
||||
grep -qF "$line" /etc/ssh/sshd_config || echo "$line" >> /etc/ssh/sshd_config
|
||||
if ! grep -qF "$line" /etc/ssh/sshd_config; then
|
||||
printf '%s\n' "$line" | $ENTCMD_PREFIX tee -a /etc/ssh/sshd_config >/dev/null
|
||||
fi
|
||||
done
|
||||
|
||||
# Start SSH server
|
||||
log "Starting SSH server on port ${SSH_ENABLE_PORT}"
|
||||
/usr/sbin/sshd -D -p "${SSH_ENABLE_PORT}" &
|
||||
$ENTCMD_PREFIX /usr/sbin/sshd -D -p "${SSH_ENABLE_PORT}" &
|
||||
SSH_PID=$!
|
||||
|
||||
# Verify the process started
|
||||
@@ -209,6 +217,20 @@ configure_ssh() {
|
||||
}
|
||||
|
||||
main() {
|
||||
# Wait for required sockets
|
||||
wait_for_socket "${NESTRI_XDG_RUNTIME_DIR}/dbus-1" "DBus" || exit 1
|
||||
wait_for_socket "${NESTRI_XDG_RUNTIME_DIR}/pipewire-0" "PipeWire" || exit 1
|
||||
|
||||
# Start by getting the container we are running under
|
||||
get_container_info || {
|
||||
log "Warning: Failed to detect container information."
|
||||
}
|
||||
log_container_info
|
||||
|
||||
if [[ "$container_runtime" != "apptainer" ]]; then
|
||||
ENTCMD_PREFIX="sudo -E"
|
||||
fi
|
||||
|
||||
# Configure SSH
|
||||
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
|
||||
[ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then
|
||||
@@ -220,17 +242,7 @@ main() {
|
||||
log "SSH not configured (missing SSH_ENABLE_PORT or SSH_ALLOWED_KEY)"
|
||||
fi
|
||||
|
||||
# Wait for required sockets
|
||||
wait_for_socket "/run/dbus/system_bus_socket" "DBus" || exit 1
|
||||
wait_for_socket "/run/user/${UID}/pipewire-0" "PipeWire" || exit 1
|
||||
|
||||
# Load GPU helpers and detect GPU
|
||||
log "Detecting GPU vendor..."
|
||||
if [[ ! -f /etc/nestri/gpu_helpers.sh ]]; then
|
||||
log "Error: GPU helpers script not found at /etc/nestri/gpu_helpers.sh."
|
||||
exit 1
|
||||
fi
|
||||
source /etc/nestri/gpu_helpers.sh
|
||||
# Get and detect GPU(s)
|
||||
get_gpu_info || {
|
||||
log "Error: Failed to detect GPU information."
|
||||
exit 1
|
||||
@@ -307,20 +319,26 @@ main() {
|
||||
log "Ensuring user directory permissions..."
|
||||
chown_user_directory || exit 1
|
||||
|
||||
# Setup namespaceless env
|
||||
log "Applying namespace-less configuration"
|
||||
setup_namespaceless
|
||||
# Setup namespaceless env if needed for container runtime
|
||||
if [[ "$container_runtime" != "podman" ]]; then
|
||||
log "Applying namespace-less configuration"
|
||||
setup_namespaceless
|
||||
fi
|
||||
|
||||
# Switch to nestri user
|
||||
log "Switching to nestri user for application startup..."
|
||||
if [[ ! -x /etc/nestri/entrypoint_nestri.sh ]]; then
|
||||
log "Error: Entry point script /etc/nestri/entrypoint_nestri.sh not found or not executable."
|
||||
# Switch to nestri runner entrypoint
|
||||
log "Switching to application startup entrypoint..."
|
||||
if [[ ! -f /etc/nestri/entrypoint_nestri.sh ]]; then
|
||||
log "Error: Application entrypoint script /etc/nestri/entrypoint_nestri.sh not found"
|
||||
exit 1
|
||||
fi
|
||||
exec sudo -E -u nestri /etc/nestri/entrypoint_nestri.sh
|
||||
if [[ "$container_runtime" == "apptainer" ]]; then
|
||||
exec /etc/nestri/entrypoint_nestri.sh
|
||||
else
|
||||
exec sudo -E -u "${NESTRI_USER}" /etc/nestri/entrypoint_nestri.sh
|
||||
fi
|
||||
}
|
||||
|
||||
# Trap signals for clean exit
|
||||
trap 'log "Received termination signal, exiting..."; exit 1' SIGINT SIGTERM
|
||||
|
||||
main
|
||||
main
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
log() {
|
||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
# Common helpers as requirement
|
||||
if [[ -f /etc/nestri/common.sh ]]; then
|
||||
source /etc/nestri/common.sh
|
||||
else
|
||||
echo "Error: Common script not found at /etc/nestri/common.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parses resolution string
|
||||
parse_resolution() {
|
||||
@@ -23,20 +27,10 @@ parse_resolution() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Loads environment variables
|
||||
load_envs() {
|
||||
if [[ -f /etc/nestri/envs.sh ]]; then
|
||||
log "Sourcing environment variables from envs.sh..."
|
||||
source /etc/nestri/envs.sh
|
||||
else
|
||||
log "Error: envs.sh not found at /etc/nestri/envs.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Configuration
|
||||
MAX_RETRIES=3
|
||||
RETRY_COUNT=0
|
||||
WAYLAND_READY_DELAY=3
|
||||
|
||||
# Kills process if running
|
||||
kill_if_running() {
|
||||
@@ -125,15 +119,15 @@ start_nestri_server() {
|
||||
WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1"
|
||||
for ((i=1; i<=15; i++)); do
|
||||
if [[ -e "$WAYLAND_SOCKET" ]]; then
|
||||
log "Wayland display 'wayland-1' ready."
|
||||
sleep 3
|
||||
log "Wayland display 'wayland-1' ready"
|
||||
sleep "${WAYLAND_READY_DELAY:-3}"
|
||||
start_compositor
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "Error: Wayland display 'wayland-1' not available."
|
||||
log "Error: Wayland display 'wayland-1' not available"
|
||||
|
||||
# Workaround for gstreamer being bit slow at times
|
||||
log "Clearing gstreamer cache.."
|
||||
@@ -156,8 +150,8 @@ start_compositor() {
|
||||
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
|
||||
fi
|
||||
|
||||
# Start Steam patcher only if Steam command is present
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
|
||||
# Start Steam patcher only if Steam command is present and if needed for container runtime
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]] && [[ "${container_runtime:-}" != "podman" ]]; then
|
||||
start_steam_namespaceless_patcher
|
||||
fi
|
||||
|
||||
@@ -200,17 +194,23 @@ start_compositor() {
|
||||
local OUTPUT_NAME
|
||||
OUTPUT_NAME=$(WAYLAND_DISPLAY=wayland-0 wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name' | head -n 1)
|
||||
if [ -z "$OUTPUT_NAME" ]; then
|
||||
log "Warning: No enabled outputs detected. Skipping wlr-randr resolution patch."
|
||||
log "Warning: No enabled outputs detected. Skipping wlr-randr resolution patch"
|
||||
return
|
||||
fi
|
||||
WAYLAND_DISPLAY=wayland-0 wlr-randr --output "$OUTPUT_NAME" --custom-mode "$WIDTH"x"$HEIGHT"
|
||||
log "Patched resolution with wlr-randr."
|
||||
log "Patched resolution with wlr-randr"
|
||||
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
|
||||
log "Starting application: $NESTRI_LAUNCH_CMD"
|
||||
WAYLAND_DISPLAY=wayland-0 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||
APP_PID=$!
|
||||
fi
|
||||
fi
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)."
|
||||
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)"
|
||||
else
|
||||
# Launch standalone application if no compositor
|
||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
|
||||
@@ -218,7 +218,7 @@ start_compositor() {
|
||||
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||
APP_PID=$!
|
||||
else
|
||||
log "No compositor or application configured."
|
||||
log "No compositor or application configured"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -228,7 +228,7 @@ increment_retry() {
|
||||
local component="$1"
|
||||
((RETRY_COUNT++))
|
||||
if [[ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]]; then
|
||||
log "Error: Max retries reached for $component."
|
||||
log "Error: Max retries reached for $component"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -259,22 +259,22 @@ main_loop() {
|
||||
sleep 1
|
||||
# Check nestri-server
|
||||
if [[ -n "${NESTRI_PID:-}" ]] && ! kill -0 "${NESTRI_PID}" 2>/dev/null; then
|
||||
log "nestri-server died."
|
||||
log "nestri-server died"
|
||||
increment_retry "nestri-server"
|
||||
restart_chain
|
||||
# Check compositor
|
||||
elif [[ -n "${COMPOSITOR_PID:-}" ]] && ! kill -0 "${COMPOSITOR_PID}" 2>/dev/null; then
|
||||
log "compositor died."
|
||||
log "compositor died"
|
||||
increment_retry "compositor"
|
||||
start_compositor
|
||||
# Check application
|
||||
elif [[ -n "${APP_PID:-}" ]] && ! kill -0 "${APP_PID}" 2>/dev/null; then
|
||||
log "application died."
|
||||
log "application died"
|
||||
increment_retry "application"
|
||||
start_compositor
|
||||
# Check patcher
|
||||
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
|
||||
log "steam-patcher died."
|
||||
log "steam-patcher died"
|
||||
increment_retry "steam-patcher"
|
||||
start_steam_namespaceless_patcher
|
||||
fi
|
||||
@@ -282,10 +282,18 @@ main_loop() {
|
||||
}
|
||||
|
||||
main() {
|
||||
load_envs
|
||||
parse_resolution "${RESOLUTION:-1920x1080}" || exit 1
|
||||
get_container_info || {
|
||||
log "Warning: Failed to detect container information."
|
||||
}
|
||||
|
||||
# Ensure DBus session env exists
|
||||
if command -v dbus-launch >/dev/null 2>&1 && [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]]; then
|
||||
eval "$(dbus-launch)"
|
||||
fi
|
||||
|
||||
restart_chain
|
||||
main_loop
|
||||
}
|
||||
|
||||
main
|
||||
main
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
export XDG_RUNTIME_DIR=/run/user/${UID}/
|
||||
export USER=${NESTRI_USER}
|
||||
export LANG=${NESTRI_LANG}
|
||||
export HOME=${NESTRI_HOME}
|
||||
export XDG_RUNTIME_DIR=${NESTRI_XDG_RUNTIME_DIR}
|
||||
export XDG_SESSION_TYPE=x11
|
||||
export DISPLAY=:0
|
||||
export $(dbus-launch)
|
||||
|
||||
# Causes some setups to break
|
||||
export PROTON_NO_FSYNC=1
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
[supervisord]
|
||||
user=root
|
||||
nodaemon=true
|
||||
loglevel=info
|
||||
logfile=/tmp/supervisord.log
|
||||
|
||||
[program:dbus]
|
||||
user=root
|
||||
command=dbus-daemon --system --nofork --nopidfile
|
||||
autorestart=true
|
||||
autostart=true
|
||||
startretries=3
|
||||
priority=1
|
||||
|
||||
[program:seatd]
|
||||
user=root
|
||||
command=seatd
|
||||
autorestart=true
|
||||
autostart=true
|
||||
startretries=3
|
||||
priority=2
|
||||
environment=XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||
|
||||
[program:pipewire]
|
||||
user=nestri
|
||||
@@ -28,6 +19,7 @@ autostart=true
|
||||
startretries=3
|
||||
priority=3
|
||||
nice=-10
|
||||
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||
|
||||
[program:pipewire-pulse]
|
||||
user=nestri
|
||||
@@ -37,6 +29,7 @@ autostart=true
|
||||
startretries=3
|
||||
priority=4
|
||||
nice=-10
|
||||
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||
|
||||
[program:wireplumber]
|
||||
user=nestri
|
||||
@@ -46,9 +39,9 @@ autostart=true
|
||||
startretries=3
|
||||
priority=5
|
||||
nice=-10
|
||||
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||
|
||||
[program:entrypoint]
|
||||
user=root
|
||||
command=/etc/nestri/entrypoint.sh
|
||||
autorestart=false
|
||||
autostart=true
|
||||
|
||||
2090
packages/server/Cargo.lock
generated
2090
packages/server/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@ name = "nestri-server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
gstreamer = { version = "0.23", features = ["v1_26"] }
|
||||
gstreamer-webrtc = { version = "0.23", features = ["v1_26"] }
|
||||
gst-plugin-webrtc = { version = "0.13", features = ["v1_22"] }
|
||||
gstreamer = { version = "0.24", features = ["v1_26"] }
|
||||
gstreamer-webrtc = { version = "0.24", features = ["v1_26"] }
|
||||
gst-plugin-webrtc = { version = "0.14" }
|
||||
serde = {version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.45", features = ["full"] }
|
||||
tokio-stream = { version = "0.1", features = ["full"] }
|
||||
@@ -28,6 +28,14 @@ prost-types = "0.14"
|
||||
parking_lot = "0.12"
|
||||
atomic_refcell = "0.1"
|
||||
byteorder = "1.5"
|
||||
libp2p = { version = "0.55", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
|
||||
libp2p-stream = { version = "0.3.0-alpha" }
|
||||
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
|
||||
libp2p-identify = "0.47"
|
||||
libp2p-ping = "0.47"
|
||||
libp2p-autonat = { version = "0.15", features = ["v2"] }
|
||||
libp2p-stream = "0.4.0-alpha"
|
||||
libp2p-yamux = "0.47"
|
||||
libp2p-noise = "0.46"
|
||||
libp2p-dns = { version = "0.44", features = ["tokio"] }
|
||||
libp2p-tcp = { version = "0.44", features = ["tokio"] }
|
||||
libp2p-websocket = "0.45"
|
||||
dashmap = "6.1"
|
||||
|
||||
2
packages/server/rust-toolchain.toml
Normal file
2
packages/server/rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.89"
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::args::encoding_args::AudioCaptureMethod;
|
||||
use crate::enc_helper::{AudioCodec, EncoderType, VideoCodec};
|
||||
use clap::builder::TypedValueParser;
|
||||
use clap::builder::{BoolishValueParser, NonEmptyStringValueParser};
|
||||
use clap::{Arg, Command, value_parser};
|
||||
|
||||
@@ -25,15 +26,6 @@ impl Args {
|
||||
.value_parser(BoolishValueParser::new())
|
||||
.default_value("false"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("debug")
|
||||
.short('d')
|
||||
.long("debug")
|
||||
.env("DEBUG")
|
||||
.help("Enable additional debugging features")
|
||||
.value_parser(BoolishValueParser::new())
|
||||
.default_value("false"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("relay-url")
|
||||
.short('u')
|
||||
@@ -88,8 +80,8 @@ impl Args {
|
||||
.long("gpu-index")
|
||||
.env("GPU_INDEX")
|
||||
.help("GPU to use by index")
|
||||
.value_parser(value_parser!(i32).range(-1..))
|
||||
.default_value("-1"),
|
||||
.value_parser(value_parser!(u32).range(0..))
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("gpu-card-path")
|
||||
@@ -154,13 +146,24 @@ impl Args {
|
||||
.value_parser(value_parser!(EncoderType))
|
||||
.default_value("hardware"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("video-bit-depth")
|
||||
.long("video-bit-depth")
|
||||
.env("VIDEO_BIT_DEPTH")
|
||||
.help("Video bit depth (8 or 10), only with DMA-BUF and non-H264 codec")
|
||||
.value_parser(
|
||||
clap::builder::PossibleValuesParser::new(["8", "10"])
|
||||
.map(|s| s.parse::<u32>().unwrap()),
|
||||
)
|
||||
.default_value("8"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("audio-capture-method")
|
||||
.long("audio-capture-method")
|
||||
.env("AUDIO_CAPTURE_METHOD")
|
||||
.help("Audio capture method")
|
||||
.value_parser(value_parser!(AudioCaptureMethod))
|
||||
.default_value("pulseaudio"),
|
||||
.default_value("pipewire"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("audio-codec")
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
pub struct AppArgs {
|
||||
/// Verbose output mode
|
||||
pub verbose: bool,
|
||||
/// Enable additional debug information and features, may affect performance
|
||||
pub debug: bool,
|
||||
|
||||
/// Virtual display resolution
|
||||
pub resolution: (u32, u32),
|
||||
@@ -15,13 +13,13 @@ pub struct AppArgs {
|
||||
pub room: String,
|
||||
|
||||
/// Experimental DMA-BUF support
|
||||
/// TODO: Move to video encoding flags
|
||||
pub dma_buf: bool,
|
||||
}
|
||||
impl AppArgs {
|
||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||
Self {
|
||||
verbose: matches.get_one::<bool>("verbose").unwrap_or(&false).clone(),
|
||||
debug: matches.get_one::<bool>("debug").unwrap_or(&false).clone(),
|
||||
resolution: {
|
||||
let res = matches
|
||||
.get_one::<String>("resolution")
|
||||
@@ -54,7 +52,6 @@ impl AppArgs {
|
||||
pub fn debug_print(&self) {
|
||||
tracing::info!("AppArgs:");
|
||||
tracing::info!("> verbose: {}", self.verbose);
|
||||
tracing::info!("> debug: {}", self.debug);
|
||||
tracing::info!(
|
||||
"> resolution: '{}x{}'",
|
||||
self.resolution.0,
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
pub struct DeviceArgs {
|
||||
/// GPU vendor (e.g. "intel")
|
||||
pub gpu_vendor: String,
|
||||
pub gpu_vendor: Option<String>,
|
||||
/// GPU name (e.g. "a770")
|
||||
pub gpu_name: String,
|
||||
/// GPU index, if multiple same GPUs are present, -1 for auto-selection
|
||||
pub gpu_index: i32,
|
||||
pub gpu_name: Option<String>,
|
||||
/// GPU index, if multiple same GPUs are present, None for auto-selection
|
||||
pub gpu_index: Option<u32>,
|
||||
/// GPU card/render path, sets card explicitly from such path
|
||||
pub gpu_card_path: String,
|
||||
pub gpu_card_path: Option<String>,
|
||||
}
|
||||
impl DeviceArgs {
|
||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||
Self {
|
||||
gpu_vendor: matches
|
||||
.get_one::<String>("gpu-vendor")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
.cloned(),
|
||||
gpu_name: matches
|
||||
.get_one::<String>("gpu-name")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
gpu_index: matches.get_one::<i32>("gpu-index").unwrap_or(&-1).clone(),
|
||||
.cloned(),
|
||||
gpu_index: matches
|
||||
.get_one::<u32>("gpu-index")
|
||||
.cloned(),
|
||||
gpu_card_path: matches
|
||||
.get_one::<String>("gpu-card-path")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
.cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug_print(&self) {
|
||||
tracing::info!("DeviceArgs:");
|
||||
tracing::info!("> gpu_vendor: '{}'", self.gpu_vendor);
|
||||
tracing::info!("> gpu_name: '{}'", self.gpu_name);
|
||||
tracing::info!("> gpu_index: {}", self.gpu_index);
|
||||
tracing::info!("> gpu_card_path: '{}'", self.gpu_card_path);
|
||||
tracing::info!(
|
||||
"> gpu_vendor: '{}'",
|
||||
self.gpu_vendor.as_deref().unwrap_or("auto")
|
||||
);
|
||||
tracing::info!(
|
||||
"> gpu_name: '{}'",
|
||||
self.gpu_name.as_deref().unwrap_or("auto")
|
||||
);
|
||||
tracing::info!(
|
||||
"> gpu_index: {}",
|
||||
self.gpu_index.map_or("auto".to_string(), |i| i.to_string())
|
||||
);
|
||||
tracing::info!(
|
||||
"> gpu_card_path: '{}'",
|
||||
self.gpu_card_path.as_deref().unwrap_or("auto")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@ pub struct EncodingOptionsBase {
|
||||
/// Codec (e.g. "h264", "opus" etc.)
|
||||
pub codec: Codec,
|
||||
/// Overridable encoder (e.g. "vah264lpenc", "opusenc" etc.)
|
||||
pub encoder: String,
|
||||
pub encoder: Option<String>,
|
||||
/// Rate control method (e.g. "cqp", "vbr", "cbr")
|
||||
pub rate_control: RateControl,
|
||||
}
|
||||
impl EncodingOptionsBase {
|
||||
pub fn debug_print(&self) {
|
||||
tracing::info!("> Codec: '{}'", self.codec.as_str());
|
||||
tracing::info!("> Encoder: '{}'", self.encoder);
|
||||
tracing::info!("> Encoder: '{}'", self.encoder.as_deref().unwrap_or("auto"));
|
||||
match &self.rate_control {
|
||||
RateControl::CQP(cqp) => {
|
||||
tracing::info!("> Rate Control: CQP");
|
||||
@@ -93,6 +93,7 @@ impl EncodingOptionsBase {
|
||||
pub struct VideoEncodingOptions {
|
||||
pub base: EncodingOptionsBase,
|
||||
pub encoder_type: EncoderType,
|
||||
pub bit_depth: u32,
|
||||
}
|
||||
impl VideoEncodingOptions {
|
||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||
@@ -104,10 +105,7 @@ impl VideoEncodingOptions {
|
||||
.unwrap_or(&VideoCodec::H264)
|
||||
.clone(),
|
||||
),
|
||||
encoder: matches
|
||||
.get_one::<String>("video-encoder")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
encoder: matches.get_one::<String>("video-encoder").cloned(),
|
||||
rate_control: match matches
|
||||
.get_one::<RateControlMethod>("video-rate-control")
|
||||
.unwrap_or(&RateControlMethod::CBR)
|
||||
@@ -132,6 +130,10 @@ impl VideoEncodingOptions {
|
||||
.get_one::<EncoderType>("video-encoder-type")
|
||||
.unwrap_or(&EncoderType::HARDWARE)
|
||||
.clone(),
|
||||
bit_depth: matches
|
||||
.get_one::<u32>("video-bit-depth")
|
||||
.copied()
|
||||
.unwrap_or(8),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +141,7 @@ impl VideoEncodingOptions {
|
||||
tracing::info!("Video Encoding Options:");
|
||||
self.base.debug_print();
|
||||
tracing::info!("> Encoder Type: {}", self.encoder_type.as_str());
|
||||
tracing::info!("> Bit Depth: {}", self.bit_depth);
|
||||
}
|
||||
}
|
||||
impl Deref for VideoEncodingOptions {
|
||||
@@ -191,10 +194,7 @@ impl AudioEncodingOptions {
|
||||
.unwrap_or(&AudioCodec::OPUS)
|
||||
.clone(),
|
||||
),
|
||||
encoder: matches
|
||||
.get_one::<String>("audio-encoder")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone(),
|
||||
encoder: matches.get_one::<String>("audio-encoder").cloned(),
|
||||
rate_control: match matches
|
||||
.get_one::<RateControlMethod>("audio-rate-control")
|
||||
.unwrap_or(&RateControlMethod::CBR)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::args::encoding_args::RateControl;
|
||||
use crate::gpu::{GPUInfo, get_gpu_by_card_path, get_gpus_by_vendor, get_nvidia_gpu_by_cuda_id};
|
||||
use crate::gpu::{GPUInfo, GPUVendor, get_gpu_by_card_path, get_gpus_by_vendor};
|
||||
use clap::ValueEnum;
|
||||
use gstreamer::prelude::*;
|
||||
use std::error::Error;
|
||||
@@ -148,7 +148,7 @@ impl VideoEncoderInfo {
|
||||
|
||||
pub fn apply_parameters(&self, element: &gstreamer::Element, verbose: bool) {
|
||||
for (key, value) in &self.parameters {
|
||||
if element.has_property(key, None) {
|
||||
if element.has_property(key) {
|
||||
if verbose {
|
||||
tracing::debug!("Setting property {} to {}", key, value);
|
||||
}
|
||||
@@ -273,7 +273,7 @@ pub fn encoder_gop_params(encoder: &VideoEncoderInfo, gop_size: u32) -> VideoEnc
|
||||
|
||||
pub fn encoder_low_latency_params(
|
||||
encoder: &VideoEncoderInfo,
|
||||
rate_control: &RateControl,
|
||||
_rate_control: &RateControl,
|
||||
framerate: u32,
|
||||
) -> VideoEncoderInfo {
|
||||
// 2 second GOP size, maybe lower to 1 second for fast recovery, if needed?
|
||||
@@ -375,9 +375,9 @@ pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
|
||||
match api {
|
||||
EncoderAPI::QSV | EncoderAPI::VAAPI => {
|
||||
// Safe property access with panic protection, gstreamer-rs is fun
|
||||
let path = if element.has_property("device-path", None) {
|
||||
let path = if element.has_property("device-path") {
|
||||
Some(element.property::<String>("device-path"))
|
||||
} else if element.has_property("device", None) {
|
||||
} else if element.has_property("device") {
|
||||
Some(element.property::<String>("device"))
|
||||
} else {
|
||||
None
|
||||
@@ -385,15 +385,46 @@ pub fn get_compatible_encoders(gpus: &Vec<GPUInfo>) -> Vec<VideoEncoderInfo> {
|
||||
|
||||
path.and_then(|p| get_gpu_by_card_path(&gpus, &p))
|
||||
}
|
||||
EncoderAPI::NVENC if element.has_property("cuda-device-id", None) => {
|
||||
let cuda_id = element.property::<u32>("cuda-device-id");
|
||||
get_nvidia_gpu_by_cuda_id(&gpus, cuda_id as usize)
|
||||
EncoderAPI::NVENC => {
|
||||
if encoder_name.contains("device") {
|
||||
// Parse by element name's index (i.e. "nvh264device{N}enc")
|
||||
let re = regex::Regex::new(r"device(\d+)").unwrap();
|
||||
if let Some(caps) = re.captures(encoder_name.as_str()) {
|
||||
if let Some(m) = caps.get(1) {
|
||||
if let Ok(id) = m.as_str().parse::<usize>() {
|
||||
return get_gpus_by_vendor(&gpus, GPUVendor::NVIDIA)
|
||||
.get(id)
|
||||
.cloned();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
} else if element.has_property("cuda-device-id") {
|
||||
let device_id =
|
||||
match element.property_value("cuda-device-id").get::<i32>() {
|
||||
Ok(v) if v >= 0 => Some(v as usize),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// We'll just treat cuda-device-id as an index
|
||||
device_id.and_then(|id| {
|
||||
get_gpus_by_vendor(&gpus, GPUVendor::NVIDIA)
|
||||
.get(id)
|
||||
.cloned()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EncoderAPI::AMF if element.has_property("device", None) => {
|
||||
let device_id = element.property::<u32>("device");
|
||||
get_gpus_by_vendor(&gpus, "amd")
|
||||
.get(device_id as usize)
|
||||
.cloned()
|
||||
EncoderAPI::AMF if element.has_property("device") => {
|
||||
let device_id = match element.property_value("device").get::<u32>() {
|
||||
Ok(v) => Some(v as usize),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
device_id.and_then(|id| {
|
||||
get_gpus_by_vendor(&gpus, GPUVendor::AMD).get(id).cloned()
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
use regex::Regex;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::str;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||
pub enum GPUVendor {
|
||||
UNKNOWN,
|
||||
INTEL,
|
||||
NVIDIA,
|
||||
AMD,
|
||||
UNKNOWN = 0x0000,
|
||||
INTEL = 0x8086,
|
||||
NVIDIA = 0x10de,
|
||||
AMD = 0x1002,
|
||||
}
|
||||
impl From<u16> for GPUVendor {
|
||||
fn from(value: u16) -> Self {
|
||||
match value {
|
||||
0x8086 => GPUVendor::INTEL,
|
||||
0x10de => GPUVendor::NVIDIA,
|
||||
0x1002 => GPUVendor::AMD,
|
||||
_ => GPUVendor::UNKNOWN,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&str> for GPUVendor {
|
||||
fn from(value: &str) -> Self {
|
||||
match value.to_lowercase().as_str() {
|
||||
"intel" => GPUVendor::INTEL,
|
||||
"nvidia" => GPUVendor::NVIDIA,
|
||||
"amd" => GPUVendor::AMD,
|
||||
_ => GPUVendor::UNKNOWN,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<String> for GPUVendor {
|
||||
fn from(value: String) -> Self {
|
||||
GPUVendor::from(value.as_str())
|
||||
}
|
||||
}
|
||||
impl GPUVendor {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
GPUVendor::INTEL => "Intel",
|
||||
GPUVendor::NVIDIA => "NVIDIA",
|
||||
GPUVendor::AMD => "AMD",
|
||||
GPUVendor::UNKNOWN => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for GPUVendor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -19,21 +58,11 @@ pub struct GPUInfo {
|
||||
device_name: String,
|
||||
pci_bus_id: 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
|
||||
}
|
||||
@@ -49,73 +78,122 @@ impl GPUInfo {
|
||||
pub fn pci_bus_id(&self) -> &str {
|
||||
&self.pci_bus_id
|
||||
}
|
||||
}
|
||||
|
||||
fn get_gpu_vendor(vendor_id: &str) -> GPUVendor {
|
||||
match vendor_id {
|
||||
"8086" => GPUVendor::INTEL,
|
||||
"10de" => GPUVendor::NVIDIA,
|
||||
"1002" => GPUVendor::AMD,
|
||||
_ => GPUVendor::UNKNOWN,
|
||||
pub fn as_str(&self) -> String {
|
||||
format!(
|
||||
"{} (Vendor: {}, Card Path: {}, Render Path: {}, PCI Bus ID: {})",
|
||||
self.device_name, self.vendor, self.card_path, self.render_path, self.pci_bus_id
|
||||
)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for GPUInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 output = Command::new("lspci")
|
||||
.args(["-mm", "-nn"])
|
||||
.output()
|
||||
.expect("Failed to execute lspci");
|
||||
pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
|
||||
// Use "/sys/class/drm/card{}" to find all GPU devices
|
||||
let mut gpus = Vec::new();
|
||||
let re = regex::Regex::new(r"^card(\d+)$")?;
|
||||
for entry in fs::read_dir("/sys/class/drm")? {
|
||||
let entry = entry?;
|
||||
let file_name = entry.file_name();
|
||||
let file_name_str = file_name.to_string_lossy();
|
||||
|
||||
str::from_utf8(&output.stdout)
|
||||
.unwrap()
|
||||
.lines()
|
||||
.filter_map(|line| parse_pci_device(line))
|
||||
.filter(|(class_id, _, _, _)| matches!(class_id.as_str(), "0300" | "0302" | "0380"))
|
||||
.filter_map(|(_, vendor_id, device_name, pci_addr)| {
|
||||
get_dri_device_path(&pci_addr)
|
||||
.map(|(card, render)| (vendor_id, card, render, device_name, pci_addr))
|
||||
})
|
||||
.map(
|
||||
|(vid, card_path, render_path, device_name, pci_bus_id)| GPUInfo {
|
||||
vendor: get_gpu_vendor(&vid),
|
||||
// We are only interested in entries that match "cardN", and getting the minor number
|
||||
let caps = match re.captures(&file_name_str) {
|
||||
Some(caps) => caps,
|
||||
None => continue,
|
||||
};
|
||||
let minor = &caps[1];
|
||||
|
||||
// Read vendor and device ID
|
||||
let vendor_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/vendor", minor))?;
|
||||
let vendor_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n');
|
||||
let vendor = u16::from_str_radix(vendor_str, 16)?;
|
||||
|
||||
let device_str = fs::read_to_string(format!("/sys/class/drm/card{}/device/device", minor))?;
|
||||
let device_str = device_str.trim_start_matches("0x").trim_end_matches('\n');
|
||||
|
||||
// Look up in hwdata PCI database
|
||||
let device_name = match fs::read_to_string("/usr/share/hwdata/pci.ids") {
|
||||
Ok(pci_ids) => parse_pci_ids(&pci_ids, vendor_str, device_str).unwrap_or("".to_owned()),
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to read /usr/share/hwdata/pci.ids: {}", e);
|
||||
"".to_owned()
|
||||
}
|
||||
};
|
||||
|
||||
// Read PCI bus ID
|
||||
let pci_bus_id = fs::read_to_string(format!("/sys/class/drm/card{}/device/uevent", minor))?;
|
||||
let pci_bus_id = pci_bus_id
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
if line.starts_with("PCI_SLOT_NAME=") {
|
||||
Some(line.trim_start_matches("PCI_SLOT_NAME=").to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or("PCI_SLOT_NAME not found")?;
|
||||
|
||||
// Get DRI device paths
|
||||
if let Some((card_path, render_path)) = get_dri_device_path(pci_bus_id.as_str()) {
|
||||
gpus.push(GPUInfo {
|
||||
vendor: vendor.into(),
|
||||
card_path,
|
||||
render_path,
|
||||
device_name,
|
||||
pci_bus_id,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(gpus)
|
||||
}
|
||||
|
||||
fn parse_pci_device(line: &str) -> Option<(String, String, String, String)> {
|
||||
let re = Regex::new(
|
||||
r#"^(?P<pci_addr>\S+)\s+"[^\[]*\[(?P<class_id>[0-9a-f]{4})\].*?"\s+"[^"]*?\[(?P<vendor_id>[0-9a-f]{4})\][^"]*?"\s+"(?P<device_name>[^"]+?)""#,
|
||||
).unwrap();
|
||||
fn parse_pci_ids(pci_data: &str, vendor_id: &str, device_id: &str) -> Option<String> {
|
||||
let mut current_vendor = String::new();
|
||||
let vendor_id = vendor_id.to_lowercase();
|
||||
let device_id = device_id.to_lowercase();
|
||||
|
||||
let caps = re.captures(line)?;
|
||||
for line in pci_data.lines() {
|
||||
// Skip comments and empty lines
|
||||
if line.starts_with('#') || line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clean device name by removing only the trailing device ID
|
||||
let device_name = caps.name("device_name")?.as_str().trim();
|
||||
let clean_re = Regex::new(r"\s+\[[0-9a-f]{4}\]$").unwrap();
|
||||
let cleaned_name = clean_re.replace(device_name, "").trim().to_string();
|
||||
// Check for vendor lines (no leading whitespace)
|
||||
if !line.starts_with(['\t', ' ']) {
|
||||
let mut parts = line.splitn(2, ' ');
|
||||
if let (Some(vendor), Some(_)) = (parts.next(), parts.next()) {
|
||||
current_vendor = vendor.to_lowercase();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
Some((
|
||||
caps.name("class_id")?.as_str().to_lowercase(),
|
||||
caps.name("vendor_id")?.as_str().to_lowercase(),
|
||||
cleaned_name,
|
||||
caps.name("pci_addr")?.as_str().to_string(),
|
||||
))
|
||||
// Check for device lines (leading whitespace)
|
||||
let line = line.trim_start();
|
||||
let mut parts = line.splitn(2, ' ');
|
||||
if let (Some(dev_id), Some(desc)) = (parts.next(), parts.next()) {
|
||||
if dev_id.to_lowercase() == device_id && current_vendor == vendor_id {
|
||||
return Some(desc.trim().to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
|
||||
let target_dir = format!("0000:{}", pci_addr);
|
||||
let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if !entry.path().to_string_lossy().contains(&target_dir) {
|
||||
if !entry.path().to_string_lossy().contains(&pci_addr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -145,10 +223,9 @@ fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: &str) -> Vec<GPUInfo> {
|
||||
let target = vendor.to_lowercase();
|
||||
pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: GPUVendor) -> Vec<GPUInfo> {
|
||||
gpus.iter()
|
||||
.filter(|gpu| gpu.vendor_string().to_lowercase() == target)
|
||||
.filter(|gpu| *gpu.vendor() == vendor)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
@@ -169,42 +246,22 @@ pub fn get_gpu_by_card_path(gpus: &[GPUInfo], path: &str) -> Option<GPUInfo> {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn get_nvidia_gpu_by_cuda_id(gpus: &[GPUInfo], cuda_device_id: usize) -> Option<GPUInfo> {
|
||||
// Check if nvidia-smi is available
|
||||
if Command::new("nvidia-smi").arg("--help").output().is_err() {
|
||||
tracing::warn!("nvidia-smi is not available");
|
||||
return None;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires access to /sys/class/drm and a GPU; not suitable for default CI"]
|
||||
fn test_get_gpus() {
|
||||
let gpus = get_gpus().unwrap();
|
||||
// Environment-dependent; just print for manual runs.
|
||||
if gpus.is_empty() {
|
||||
eprintln!("No GPUs found; skipping assertions");
|
||||
return;
|
||||
}
|
||||
// Print the GPUs found for manual verification
|
||||
for gpu in &gpus {
|
||||
println!("{}", gpu);
|
||||
}
|
||||
}
|
||||
|
||||
// Run nvidia-smi to get information about the CUDA device
|
||||
let output = Command::new("nvidia-smi")
|
||||
.args([
|
||||
"--query-gpu=pci.bus_id",
|
||||
"--format=csv,noheader",
|
||||
"-i",
|
||||
&cuda_device_id.to_string(),
|
||||
])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Parse the output to get the PCI bus ID
|
||||
let pci_bus_id = str::from_utf8(&output.stdout).ok()?.trim().to_uppercase(); // nvidia-smi returns uppercase PCI IDs
|
||||
|
||||
// Convert from 00000000:05:00.0 to 05:00.0 if needed
|
||||
let pci_bus_id = if pci_bus_id.starts_with("00000000:") {
|
||||
pci_bus_id[9..].to_string() // Skip the domain part
|
||||
} else if pci_bus_id.starts_with("0000:") {
|
||||
pci_bus_id[5..].to_string() // Alternate check for older nvidia-smi versions
|
||||
} else {
|
||||
pci_bus_id
|
||||
};
|
||||
|
||||
// Find the GPU with the matching PCI bus ID
|
||||
gpus.iter()
|
||||
.find(|gpu| gpu.vendor == GPUVendor::NVIDIA && gpu.pci_bus_id.to_uppercase() == pci_bus_id)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
@@ -25,43 +25,42 @@ use tracing_subscriber::filter::LevelFilter;
|
||||
// Handles gathering GPU information and selecting the most suitable GPU
|
||||
fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
||||
tracing::info!("Gathering GPU information..");
|
||||
let mut gpus = gpu::get_gpus();
|
||||
let mut gpus = gpu::get_gpus()?;
|
||||
if gpus.is_empty() {
|
||||
return Err("No GPUs found".into());
|
||||
}
|
||||
for (i, gpu) in gpus.iter().enumerate() {
|
||||
tracing::info!(
|
||||
"> [GPU:{}] Vendor: '{}', Card Path: '{}', Render Path: '{}', Device Name: '{}'",
|
||||
i,
|
||||
gpu.vendor_string(),
|
||||
gpu.card_path(),
|
||||
gpu.render_path(),
|
||||
gpu.device_name()
|
||||
);
|
||||
tracing::info!("> [GPU:{}] {}", i, gpu);
|
||||
}
|
||||
|
||||
// Additional GPU filtering
|
||||
if !args.device.gpu_card_path.is_empty() {
|
||||
if let Some(gpu) = gpu::get_gpu_by_card_path(&gpus, &args.device.gpu_card_path) {
|
||||
return Ok(Vec::from([gpu]));
|
||||
}
|
||||
if let Some(gpu_card_path) = &args.device.gpu_card_path {
|
||||
return match gpu::get_gpu_by_card_path(&gpus, gpu_card_path.as_str()) {
|
||||
Some(gpu) => Ok(Vec::from([gpu])),
|
||||
None => Err(format!(
|
||||
"No GPU found with the specified card path: '{}'",
|
||||
gpu_card_path
|
||||
)
|
||||
.into()),
|
||||
};
|
||||
} else {
|
||||
// Run all filters that are not empty
|
||||
let mut filtered_gpus = gpus.clone();
|
||||
if !args.device.gpu_vendor.is_empty() {
|
||||
filtered_gpus = gpu::get_gpus_by_vendor(&filtered_gpus, &args.device.gpu_vendor);
|
||||
if let Some(gpu_vendor) = &args.device.gpu_vendor {
|
||||
filtered_gpus =
|
||||
gpu::get_gpus_by_vendor(&filtered_gpus, GPUVendor::from(gpu_vendor.clone()));
|
||||
}
|
||||
if !args.device.gpu_name.is_empty() {
|
||||
filtered_gpus = gpu::get_gpus_by_device_name(&filtered_gpus, &args.device.gpu_name);
|
||||
if let Some(gpu_name) = &args.device.gpu_name {
|
||||
filtered_gpus = gpu::get_gpus_by_device_name(&filtered_gpus, gpu_name.as_str());
|
||||
}
|
||||
if args.device.gpu_index > -1 {
|
||||
if let Some(gpu_index) = &args.device.gpu_index {
|
||||
// get single GPU by index
|
||||
let gpu_index = args.device.gpu_index as usize;
|
||||
let gpu_index = *gpu_index as usize;
|
||||
if gpu_index >= filtered_gpus.len() {
|
||||
return Err(format!(
|
||||
"GPU index {} is out of bounds for available GPUs (0-{})",
|
||||
gpu_index,
|
||||
filtered_gpus.len() - 1
|
||||
filtered_gpus.len().saturating_sub(1)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
@@ -77,10 +76,10 @@ fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
||||
if gpus.is_empty() {
|
||||
return Err(format!(
|
||||
"No GPU(s) found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
||||
args.device.gpu_vendor,
|
||||
args.device.gpu_name,
|
||||
args.device.gpu_index,
|
||||
args.device.gpu_card_path
|
||||
args.device.gpu_vendor.as_deref().unwrap_or("auto"),
|
||||
args.device.gpu_name.as_deref().unwrap_or("auto"),
|
||||
args.device.gpu_index.map_or("auto".to_string(), |i| i.to_string()),
|
||||
args.device.gpu_card_path.as_deref().unwrap_or("auto")
|
||||
).into());
|
||||
}
|
||||
Ok(gpus)
|
||||
@@ -112,9 +111,8 @@ fn handle_encoder_video(
|
||||
}
|
||||
// Pick most suitable video encoder based on given arguments
|
||||
let video_encoder;
|
||||
if !args.encoding.video.encoder.is_empty() {
|
||||
video_encoder =
|
||||
enc_helper::get_encoder_by_name(&video_encoders, &args.encoding.video.encoder)?;
|
||||
if let Some(wanted_encoder) = &args.encoding.video.encoder {
|
||||
video_encoder = enc_helper::get_encoder_by_name(&video_encoders, wanted_encoder.as_str())?;
|
||||
} else {
|
||||
video_encoder = enc_helper::get_best_working_encoder(
|
||||
&video_encoders,
|
||||
@@ -164,11 +162,12 @@ fn handle_encoder_video_settings(
|
||||
// Handles picking audio encoder
|
||||
// TODO: Expand enc_helper with audio types, for now just opus
|
||||
fn handle_encoder_audio(args: &args::Args) -> String {
|
||||
let audio_encoder = if args.encoding.audio.encoder.is_empty() {
|
||||
"opusenc".to_string()
|
||||
} else {
|
||||
args.encoding.audio.encoder.clone()
|
||||
};
|
||||
let audio_encoder = args
|
||||
.encoding
|
||||
.audio
|
||||
.encoder
|
||||
.clone()
|
||||
.unwrap_or_else(|| "opusenc".to_string());
|
||||
tracing::info!("Selected audio encoder: '{}'", audio_encoder);
|
||||
audio_encoder
|
||||
}
|
||||
@@ -197,10 +196,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Get relay URL from arguments
|
||||
let relay_url = args.app.relay_url.trim();
|
||||
|
||||
// Initialize libp2p (logically the sink should handle the connection to be independent)
|
||||
let nestri_p2p = Arc::new(NestriP2P::new().await?);
|
||||
let p2p_conn = nestri_p2p.connect(relay_url).await?;
|
||||
|
||||
gstreamer::init()?;
|
||||
let _ = gstrswebrtc::plugin_register_static(); // Might be already registered, so we'll pass..
|
||||
|
||||
@@ -239,6 +234,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Handle audio encoder selection
|
||||
let audio_encoder = handle_encoder_audio(&args);
|
||||
|
||||
// Initialize libp2p (logically the sink should handle the connection to be independent)
|
||||
let nestri_p2p = Arc::new(NestriP2P::new().await?);
|
||||
let p2p_conn = nestri_p2p.connect(relay_url).await?;
|
||||
|
||||
/*** PIPELINE CREATION ***/
|
||||
// Create the pipeline
|
||||
let pipeline = Arc::new(gstreamer::Pipeline::new());
|
||||
@@ -250,7 +249,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
gstreamer::ElementFactory::make("pulsesrc").build()?
|
||||
}
|
||||
encoding_args::AudioCaptureMethod::PIPEWIRE => {
|
||||
gstreamer::ElementFactory::make("pipewiresrc").build()?
|
||||
let pw_element = gstreamer::ElementFactory::make("pipewiresrc").build()?;
|
||||
pw_element.set_property("use-bufferpool", &false); // false for audio
|
||||
pw_element
|
||||
}
|
||||
encoding_args::AudioCaptureMethod::ALSA => {
|
||||
gstreamer::ElementFactory::make("alsasrc").build()?
|
||||
@@ -279,7 +280,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
},
|
||||
);
|
||||
// If has "frame-size" (opus), set to 10 for lower latency (below 10 seems to be too low?)
|
||||
if audio_encoder.has_property("frame-size", None) {
|
||||
if audio_encoder.has_property("frame-size") {
|
||||
audio_encoder.set_property_from_str("frame-size", "10");
|
||||
}
|
||||
|
||||
@@ -313,6 +314,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
))?;
|
||||
caps_filter.set_property("caps", &caps);
|
||||
|
||||
// Get bit-depth and choose appropriate format (NV12 or P010_10LE)
|
||||
// H.264 does not support above 8-bit. Also we require DMA-BUF.
|
||||
let video_format = if args.encoding.video.bit_depth == 10
|
||||
&& args.app.dma_buf
|
||||
&& video_encoder_info.codec != enc_helper::VideoCodec::H264
|
||||
{
|
||||
"P010_10LE"
|
||||
} else {
|
||||
"NV12"
|
||||
};
|
||||
|
||||
// GL and CUDA elements (NVIDIA only..)
|
||||
let mut glupload = None;
|
||||
let mut glconvert = None;
|
||||
@@ -325,7 +337,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
glconvert = Some(gstreamer::ElementFactory::make("glcolorconvert").build()?);
|
||||
// GL color convert caps
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let gl_caps = gstreamer::Caps::from_str("video/x-raw(memory:GLMemory),format=NV12")?;
|
||||
let gl_caps = gstreamer::Caps::from_str(
|
||||
format!("video/x-raw(memory:GLMemory),format={video_format}").as_str(),
|
||||
)?;
|
||||
caps_filter.set_property("caps", &gl_caps);
|
||||
gl_caps_filter = Some(caps_filter);
|
||||
// CUDA upload element
|
||||
@@ -341,7 +355,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
vapostproc = Some(gstreamer::ElementFactory::make("vapostproc").build()?);
|
||||
// VA caps filter
|
||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
||||
let va_caps = gstreamer::Caps::from_str("video/x-raw(memory:VAMemory),format=NV12")?;
|
||||
let va_caps = gstreamer::Caps::from_str(
|
||||
format!("video/x-raw(memory:VAMemory),format={video_format}").as_str(),
|
||||
)?;
|
||||
caps_filter.set_property("caps", &va_caps);
|
||||
va_caps_filter = Some(caps_filter);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use atomic_refcell::AtomicRefCell;
|
||||
use glib::subclass::prelude::*;
|
||||
use gstreamer::glib;
|
||||
use gstreamer::prelude::*;
|
||||
use gstreamer_webrtc::{gst_sdp, WebRTCSDPType, WebRTCSessionDescription};
|
||||
use gstreamer_webrtc::{WebRTCSDPType, WebRTCSessionDescription, gst_sdp};
|
||||
use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
||||
use parking_lot::RwLock as PLRwLock;
|
||||
use prost::Message;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod p2p;
|
||||
pub mod p2p_protocol_stream;
|
||||
pub mod p2p_safestream;
|
||||
pub mod p2p_protocol_stream;
|
||||
@@ -1,10 +1,16 @@
|
||||
use libp2p::futures::StreamExt;
|
||||
use libp2p::multiaddr::Protocol;
|
||||
use libp2p::{
|
||||
Multiaddr, PeerId, Swarm, identify, noise, ping,
|
||||
Multiaddr, PeerId, Swarm, identity,
|
||||
swarm::{NetworkBehaviour, SwarmEvent},
|
||||
tcp, yamux,
|
||||
};
|
||||
use libp2p_autonat as autonat;
|
||||
use libp2p_identify as identify;
|
||||
use libp2p_noise as noise;
|
||||
use libp2p_ping as ping;
|
||||
use libp2p_stream as stream;
|
||||
use libp2p_tcp as tcp;
|
||||
use libp2p_yamux as yamux;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -12,15 +18,28 @@ use tokio::sync::Mutex;
|
||||
#[derive(Clone)]
|
||||
pub struct NestriConnection {
|
||||
pub peer_id: PeerId,
|
||||
pub control: libp2p_stream::Control,
|
||||
pub control: stream::Control,
|
||||
}
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
struct NestriBehaviour {
|
||||
identify: identify::Behaviour,
|
||||
ping: ping::Behaviour,
|
||||
stream: libp2p_stream::Behaviour,
|
||||
autonatv2: libp2p::autonat::v2::client::Behaviour,
|
||||
stream: stream::Behaviour,
|
||||
autonatv2: autonat::v2::client::Behaviour,
|
||||
}
|
||||
impl NestriBehaviour {
|
||||
fn new(key: identity::PublicKey) -> Self {
|
||||
Self {
|
||||
identify: identify::Behaviour::new(identify::Config::new(
|
||||
"/ipfs/id/1.0.0".to_string(),
|
||||
key,
|
||||
)),
|
||||
ping: ping::Behaviour::default(),
|
||||
stream: stream::Behaviour::default(),
|
||||
autonatv2: autonat::v2::client::Behaviour::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NestriP2P {
|
||||
@@ -39,22 +58,7 @@ impl NestriP2P {
|
||||
.with_dns()?
|
||||
.with_websocket(noise::Config::new, yamux::Config::default)
|
||||
.await?
|
||||
.with_behaviour(|key| {
|
||||
let identify_behaviour = identify::Behaviour::new(identify::Config::new(
|
||||
"/ipfs/id/1.0.0".to_string(),
|
||||
key.public(),
|
||||
));
|
||||
let ping_behaviour = ping::Behaviour::default();
|
||||
let stream_behaviour = libp2p_stream::Behaviour::default();
|
||||
let autonatv2_behaviour = libp2p::autonat::v2::client::Behaviour::default();
|
||||
|
||||
Ok(NestriBehaviour {
|
||||
identify: identify_behaviour,
|
||||
ping: ping_behaviour,
|
||||
stream: stream_behaviour,
|
||||
autonatv2: autonatv2_behaviour,
|
||||
})
|
||||
})?
|
||||
.with_behaviour(|key| NestriBehaviour::new(key.public()))?
|
||||
.build(),
|
||||
));
|
||||
|
||||
@@ -62,12 +66,6 @@ impl NestriP2P {
|
||||
let swarm_clone = swarm.clone();
|
||||
tokio::spawn(swarm_loop(swarm_clone));
|
||||
|
||||
{
|
||||
let mut swarm_lock = swarm.lock().await;
|
||||
swarm_lock.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; // IPv4 - TCP Raw
|
||||
swarm_lock.listen_on("/ip6/::/tcp/0".parse()?)?; // IPv6 - TCP Raw
|
||||
}
|
||||
|
||||
Ok(NestriP2P { swarm })
|
||||
}
|
||||
|
||||
@@ -95,6 +93,55 @@ async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
|
||||
swarm_lock.select_next_some().await
|
||||
};
|
||||
match event {
|
||||
/* Ping Events */
|
||||
SwarmEvent::Behaviour(NestriBehaviourEvent::Ping(ping::Event {
|
||||
peer,
|
||||
connection,
|
||||
result,
|
||||
})) => {
|
||||
if let Ok(latency) = result {
|
||||
tracing::debug!(
|
||||
"Ping event - peer: {}, connection: {:?}, latency: {} us",
|
||||
peer,
|
||||
connection,
|
||||
latency.as_micros()
|
||||
);
|
||||
} else if let Err(err) = result {
|
||||
tracing::warn!(
|
||||
"Ping event - peer: {}, connection: {:?}, error: {:?}",
|
||||
peer,
|
||||
connection,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
/* Autonat (v2) Events */
|
||||
SwarmEvent::Behaviour(NestriBehaviourEvent::Autonatv2(
|
||||
autonat::v2::client::Event {
|
||||
server,
|
||||
tested_addr,
|
||||
bytes_sent,
|
||||
result,
|
||||
},
|
||||
)) => {
|
||||
if let Ok(()) = result {
|
||||
tracing::debug!(
|
||||
"AutonatV2 event - test server '{}' verified address '{}' with {} bytes sent",
|
||||
server,
|
||||
tested_addr,
|
||||
bytes_sent
|
||||
);
|
||||
} else if let Err(err) = result {
|
||||
tracing::warn!(
|
||||
"AutonatV2 event - test server '{}' failed to verify address '{}' with {} bytes sent: {:?}",
|
||||
server,
|
||||
tested_addr,
|
||||
bytes_sent,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
/* Swarm Events */
|
||||
SwarmEvent::NewListenAddr { address, .. } => {
|
||||
tracing::info!("Listening on: '{}'", address);
|
||||
}
|
||||
@@ -130,6 +177,13 @@ async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
|
||||
tracing::error!("Failed to connect: {}", error);
|
||||
}
|
||||
}
|
||||
SwarmEvent::ExternalAddrConfirmed { address } => {
|
||||
tracing::info!("Confirmed external address: {}", address);
|
||||
}
|
||||
/* Unhandled Events */
|
||||
SwarmEvent::Behaviour(event) => {
|
||||
tracing::warn!("Unhandled Behaviour event: {:?}", event);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use libp2p::futures::io::{ReadHalf, WriteHalf};
|
||||
use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -22,37 +19,6 @@ impl SafeStream {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_json<T: Serialize>(
|
||||
&self,
|
||||
data: &T,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let json_data = serde_json::to_vec(data)?;
|
||||
tracing::info!("Sending JSON");
|
||||
let e = self.send_with_length_prefix(&json_data).await;
|
||||
tracing::info!("Sent JSON");
|
||||
e
|
||||
}
|
||||
|
||||
pub async fn receive_json<T: DeserializeOwned>(&self) -> Result<T, Box<dyn std::error::Error>> {
|
||||
let data = self.receive_with_length_prefix().await?;
|
||||
let msg = serde_json::from_slice(&data)?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
pub async fn send_proto<M: Message>(&self, msg: &M) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut proto_data = Vec::new();
|
||||
msg.encode(&mut proto_data)?;
|
||||
self.send_with_length_prefix(&proto_data).await
|
||||
}
|
||||
|
||||
pub async fn receive_proto<M: Message + Default>(
|
||||
&self,
|
||||
) -> Result<M, Box<dyn std::error::Error>> {
|
||||
let data = self.receive_with_length_prefix().await?;
|
||||
let msg = M::decode(&*data)?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
pub async fn send_raw(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.send_with_length_prefix(data).await
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoTimestampEntry {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub stage: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub time: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoLatencyTracker {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub sequence_id: ::prost::alloc::string::String,
|
||||
#[prost(message, repeated, tag="2")]
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
|
||||
}
|
||||
/// MouseMove message
|
||||
@@ -21,11 +21,11 @@ pub struct ProtoLatencyTracker {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMove {
|
||||
/// Fixed value "MouseMove"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseMoveAbs message
|
||||
@@ -33,11 +33,11 @@ pub struct ProtoMouseMove {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseMoveAbs {
|
||||
/// Fixed value "MouseMoveAbs"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseWheel message
|
||||
@@ -45,11 +45,11 @@ pub struct ProtoMouseMoveAbs {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseWheel {
|
||||
/// Fixed value "MouseWheel"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub x: i32,
|
||||
#[prost(int32, tag="3")]
|
||||
#[prost(int32, tag = "3")]
|
||||
pub y: i32,
|
||||
}
|
||||
/// MouseKeyDown message
|
||||
@@ -57,9 +57,9 @@ pub struct ProtoMouseWheel {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyDown {
|
||||
/// Fixed value "MouseKeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// MouseKeyUp message
|
||||
@@ -67,9 +67,9 @@ pub struct ProtoMouseKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMouseKeyUp {
|
||||
/// Fixed value "MouseKeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyDown message
|
||||
@@ -77,9 +77,9 @@ pub struct ProtoMouseKeyUp {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyDown {
|
||||
/// Fixed value "KeyDown"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// KeyUp message
|
||||
@@ -87,53 +87,53 @@ pub struct ProtoKeyDown {
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoKeyUp {
|
||||
/// Fixed value "KeyUp"
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub r#type: ::prost::alloc::string::String,
|
||||
#[prost(int32, tag="2")]
|
||||
#[prost(int32, tag = "2")]
|
||||
pub key: i32,
|
||||
}
|
||||
/// Union of all Input types
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoInput {
|
||||
#[prost(oneof="proto_input::InputType", tags="1, 2, 3, 4, 5, 6, 7")]
|
||||
#[prost(oneof = "proto_input::InputType", tags = "1, 2, 3, 4, 5, 6, 7")]
|
||||
pub input_type: ::core::option::Option<proto_input::InputType>,
|
||||
}
|
||||
/// Nested message and enum types in `ProtoInput`.
|
||||
pub mod proto_input {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum InputType {
|
||||
#[prost(message, tag="1")]
|
||||
#[prost(message, tag = "1")]
|
||||
MouseMove(super::ProtoMouseMove),
|
||||
#[prost(message, tag="2")]
|
||||
#[prost(message, tag = "2")]
|
||||
MouseMoveAbs(super::ProtoMouseMoveAbs),
|
||||
#[prost(message, tag="3")]
|
||||
#[prost(message, tag = "3")]
|
||||
MouseWheel(super::ProtoMouseWheel),
|
||||
#[prost(message, tag="4")]
|
||||
#[prost(message, tag = "4")]
|
||||
MouseKeyDown(super::ProtoMouseKeyDown),
|
||||
#[prost(message, tag="5")]
|
||||
#[prost(message, tag = "5")]
|
||||
MouseKeyUp(super::ProtoMouseKeyUp),
|
||||
#[prost(message, tag="6")]
|
||||
#[prost(message, tag = "6")]
|
||||
KeyDown(super::ProtoKeyDown),
|
||||
#[prost(message, tag="7")]
|
||||
#[prost(message, tag = "7")]
|
||||
KeyUp(super::ProtoKeyUp),
|
||||
}
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageBase {
|
||||
#[prost(string, tag="1")]
|
||||
#[prost(string, tag = "1")]
|
||||
pub payload_type: ::prost::alloc::string::String,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub latency: ::core::option::Option<ProtoLatencyTracker>,
|
||||
}
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProtoMessageInput {
|
||||
#[prost(message, optional, tag="1")]
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub message_base: ::core::option::Option<ProtoMessageBase>,
|
||||
#[prost(message, optional, tag="2")]
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub data: ::core::option::Option<ProtoInput>,
|
||||
}
|
||||
// @@protoc_insertion_point(module)
|
||||
|
||||
Reference in New Issue
Block a user