mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 08:45:38 +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
|
libxkbcommon wayland gstreamer gst-plugins-base gst-plugins-good libinput
|
||||||
|
|
||||||
# Clone repository
|
# 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
|
FROM gst-wayland-deps AS gst-wayland-planner
|
||||||
@@ -121,58 +121,50 @@ RUN --mount=type=cache,target=${CARGO_HOME}/registry \
|
|||||||
#******************************************************************************
|
#******************************************************************************
|
||||||
FROM base AS runtime
|
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 ###
|
### Package Installation ###
|
||||||
# Core system components
|
# Core system components
|
||||||
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
RUN --mount=type=cache,target=/var/cache/pacman/pkg \
|
||||||
pacman -Sy --needed --noconfirm \
|
pacman -Sy --needed --noconfirm \
|
||||||
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
vulkan-intel lib32-vulkan-intel vpl-gpu-rt \
|
||||||
vulkan-radeon lib32-vulkan-radeon \
|
vulkan-radeon lib32-vulkan-radeon \
|
||||||
mesa \
|
mesa steam-native-runtime proton-cachyos lib32-mesa \
|
||||||
steam steam-native-runtime proton-cachyos gtk3 lib32-gtk3 \
|
steam gtk3 lib32-gtk3 \
|
||||||
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
sudo xorg-xwayland seatd libinput gamescope mangohud wlr-randr \
|
||||||
libssh2 curl wget \
|
libssh2 curl wget \
|
||||||
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
pipewire pipewire-pulse pipewire-alsa wireplumber \
|
||||||
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
|
noto-fonts-cjk supervisor jq chwd lshw pacman-contrib \
|
||||||
openssh && \
|
hwdata openssh \
|
||||||
# GStreamer stack
|
# GStreamer stack
|
||||||
pacman -Sy --needed --noconfirm \
|
|
||||||
gstreamer gst-plugins-base gst-plugins-good \
|
gstreamer gst-plugins-base gst-plugins-good \
|
||||||
gst-plugins-bad gst-plugin-pipewire \
|
gst-plugins-bad gst-plugin-pipewire \
|
||||||
gst-plugin-webrtchttp gst-plugin-rswebrtc gst-plugin-rsrtp \
|
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
|
# lib32 GStreamer stack to fix some games with videos
|
||||||
pacman -Sy --needed --noconfirm \
|
|
||||||
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
|
lib32-gstreamer lib32-gst-plugins-base lib32-gst-plugins-good && \
|
||||||
# Cleanup
|
# Cleanup
|
||||||
paccache -rk1 && \
|
paccache -rk1 && \
|
||||||
rm -rf /usr/share/{info,man,doc}/*
|
rm -rf /usr/share/{info,man,doc}/*
|
||||||
|
|
||||||
### User Configuration ###
|
### User Configuration ###
|
||||||
ENV USER="nestri" \
|
ARG NESTRI_USER_PWD=""
|
||||||
UID=1000 \
|
ENV NESTRI_USER="nestri" \
|
||||||
GID=1000 \
|
NESTRI_UID=1000 \
|
||||||
USER_PWD="nestri1234" \
|
NESTRI_GID=1000 \
|
||||||
XDG_RUNTIME_DIR=/run/user/1000 \
|
NESTRI_LANG=en_US.UTF-8 \
|
||||||
HOME=/home/nestri \
|
NESTRI_XDG_RUNTIME_DIR=/run/user/1000 \
|
||||||
|
NESTRI_HOME=/home/nestri \
|
||||||
NVIDIA_DRIVER_CAPABILITIES=all
|
NVIDIA_DRIVER_CAPABILITIES=all
|
||||||
|
|
||||||
RUN mkdir -p /home/${USER} && \
|
RUN mkdir -p "/home/${NESTRI_USER}" && \
|
||||||
groupadd -g ${GID} ${USER} && \
|
groupadd -g "${NESTRI_GID}" "${NESTRI_USER}" && \
|
||||||
useradd -d /home/${USER} -u ${UID} -g ${GID} -s /bin/bash ${USER} && \
|
useradd -d "/home/${NESTRI_USER}" -u "${NESTRI_UID}" -g "${NESTRI_GID}" -s /bin/bash "${NESTRI_USER}" && \
|
||||||
chown -R ${USER}:${USER} /home/${USER} && \
|
echo "${NESTRI_USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
||||||
echo "${USER} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
NESTRI_USER_PWD="${NESTRI_USER_PWD:-$(openssl rand -base64 12)}" && \
|
||||||
echo "${USER}:${USER_PWD}" | chpasswd && \
|
echo "Setting password for ${NESTRI_USER} as: ${NESTRI_USER_PWD}" && \
|
||||||
mkdir -p /run/user/${UID} && \
|
echo "${NESTRI_USER}:${NESTRI_USER_PWD}" | chpasswd && \
|
||||||
chown ${USER}:${USER} /run/user/${UID} && \
|
mkdir -p "${NESTRI_XDG_RUNTIME_DIR}" && \
|
||||||
usermod -aG input,video,render,seat root && \
|
chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_XDG_RUNTIME_DIR}" && \
|
||||||
usermod -aG input,video,render,seat ${USER}
|
usermod -aG input,video,render,seat "${NESTRI_USER}"
|
||||||
|
|
||||||
### System Services Configuration ###
|
### System Services Configuration ###
|
||||||
RUN mkdir -p /run/dbus && \
|
RUN mkdir -p /run/dbus && \
|
||||||
@@ -182,29 +174,17 @@ RUN mkdir -p /run/dbus && \
|
|||||||
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
|
-e '/wants = \[/{s/hooks\.node\.suspend\s*//; s/,\s*\]/]/}' \
|
||||||
/usr/share/wireplumber/wireplumber.conf
|
/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 && \
|
RUN mkdir -p /etc/pipewire/pipewire.conf.d && \
|
||||||
echo "[audio]\
|
mkdir -p /etc/wireplumber/wireplumber.conf.d
|
||||||
\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
|
|
||||||
|
|
||||||
|
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 ###
|
### Artifacts and Verification ###
|
||||||
COPY --from=nestri-server-cached-builder /artifacts/nestri-server /usr/bin/
|
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 ###
|
### Scripts and Final Configuration ###
|
||||||
COPY packages/scripts/ /etc/nestri/
|
COPY packages/scripts/ /etc/nestri/
|
||||||
RUN chmod +x /etc/nestri/{envs.sh,entrypoint*.sh} && \
|
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"]
|
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
|
#!/bin/bash
|
||||||
set -euo pipefail
|
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
|
# Configuration
|
||||||
CACHE_DIR="/home/nestri/.cache/nvidia"
|
CACHE_DIR="${NESTRI_HOME}/.cache/nestri"
|
||||||
NVIDIA_INSTALLER_DIR="/tmp"
|
NVIDIA_INSTALLER_DIR="/tmp"
|
||||||
TIMEOUT_SECONDS=10
|
TIMEOUT_SECONDS=10
|
||||||
|
ENTCMD_PREFIX=""
|
||||||
log() {
|
|
||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Ensures user directory ownership
|
# Ensures user directory ownership
|
||||||
chown_user_directory() {
|
chown_user_directory() {
|
||||||
local user_group="${USER}:${GID}"
|
if ! $ENTCMD_PREFIX chown "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}" 2>/dev/null; then
|
||||||
if ! chown -h --no-preserve-root "$user_group" "${HOME}" 2>/dev/null; then
|
echo "Error: Failed to change ownership of ${NESTRI_HOME} to ${NESTRI_USER}:${NESTRI_USER}" >&2
|
||||||
echo "Error: Failed to change ownership of ${HOME} to ${user_group}" >&2
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -38,8 +42,8 @@ wait_for_socket() {
|
|||||||
|
|
||||||
# Prepares environment for namespace-less applications (like Steam)
|
# Prepares environment for namespace-less applications (like Steam)
|
||||||
setup_namespaceless() {
|
setup_namespaceless() {
|
||||||
rm -f /run/systemd/container || true
|
$ENTCMD_PREFIX rm -f /run/systemd/container || true
|
||||||
mkdir -p /run/pressure-vessel || true
|
$ENTCMD_PREFIX mkdir -p /run/pressure-vessel || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensures cache directory exists
|
# Ensures cache directory exists
|
||||||
@@ -49,7 +53,7 @@ setup_cache() {
|
|||||||
log "Warning: Failed to create cache directory, continuing without cache."
|
log "Warning: Failed to create cache directory, continuing without cache."
|
||||||
return 1
|
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..."
|
log "Warning: Failed to set cache directory ownership, continuing..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,7 +98,7 @@ get_nvidia_installer() {
|
|||||||
|
|
||||||
# Cache the downloaded file
|
# Cache the downloaded file
|
||||||
cp "$tmp_file" "$cached_file" 2>/dev/null && \
|
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..."
|
log "Warning: Failed to cache NVIDIA driver, continuing..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -109,7 +113,7 @@ get_nvidia_installer() {
|
|||||||
install_nvidia_driver() {
|
install_nvidia_driver() {
|
||||||
local filename="$1"
|
local filename="$1"
|
||||||
log "Installing NVIDIA driver components from $filename..."
|
log "Installing NVIDIA driver components from $filename..."
|
||||||
bash ./"$filename" \
|
$ENTCMD_PREFIX bash ./"$filename" \
|
||||||
--silent \
|
--silent \
|
||||||
--skip-depmod \
|
--skip-depmod \
|
||||||
--skip-module-unload \
|
--skip-module-unload \
|
||||||
@@ -120,24 +124,32 @@ install_nvidia_driver() {
|
|||||||
--no-systemd \
|
--no-systemd \
|
||||||
--no-rpms \
|
--no-rpms \
|
||||||
--no-backup \
|
--no-backup \
|
||||||
|
--no-distro-scripts \
|
||||||
|
--no-libglx-indirect \
|
||||||
|
--no-install-libglvnd \
|
||||||
--no-check-for-alternate-installs || {
|
--no-check-for-alternate-installs || {
|
||||||
log "Error: NVIDIA driver installation failed."
|
log "Error: NVIDIA driver installation failed."
|
||||||
return 1
|
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."
|
log "NVIDIA driver installation completed."
|
||||||
return 0
|
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() {
|
log_gpu_info() {
|
||||||
if ! declare -p vendor_devices &>/dev/null; then
|
if ! declare -p vendor_devices &>/dev/null; then
|
||||||
log "Warning: vendor_devices array is not defined"
|
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"
|
log "Configuring SSH server on port ${SSH_ENABLE_PORT} with public key authentication"
|
||||||
|
|
||||||
# Ensure SSH host keys exist
|
# 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"
|
log "Error: Failed to generate SSH host keys"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create .ssh directory and authorized_keys file for nestri user
|
# Create .ssh directory and authorized_keys file for nestri user
|
||||||
mkdir -p /home/nestri/.ssh
|
mkdir -p "${NESTRI_HOME}/.ssh"
|
||||||
echo "${SSH_ALLOWED_KEY}" > /home/nestri/.ssh/authorized_keys
|
echo "${SSH_ALLOWED_KEY}" > "${NESTRI_HOME}/.ssh/authorized_keys"
|
||||||
chmod 700 /home/nestri/.ssh
|
chmod 700 "${NESTRI_HOME}/.ssh"
|
||||||
chmod 600 /home/nestri/.ssh/authorized_keys
|
chmod 600 "${NESTRI_HOME}/.ssh/authorized_keys"
|
||||||
chown -R nestri:nestri /home/nestri/.ssh
|
chown -R "${NESTRI_USER}:${NESTRI_USER}" "${NESTRI_HOME}/.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
|
|
||||||
}
|
|
||||||
|
|
||||||
# Configure secure SSH settings
|
# Configure secure SSH settings
|
||||||
{
|
{
|
||||||
@@ -190,12 +196,14 @@ configure_ssh() {
|
|||||||
echo "UsePAM no"
|
echo "UsePAM no"
|
||||||
echo "PubkeyAuthentication yes"
|
echo "PubkeyAuthentication yes"
|
||||||
} | while read -r line; do
|
} | 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
|
done
|
||||||
|
|
||||||
# Start SSH server
|
# Start SSH server
|
||||||
log "Starting SSH server on port ${SSH_ENABLE_PORT}"
|
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=$!
|
SSH_PID=$!
|
||||||
|
|
||||||
# Verify the process started
|
# Verify the process started
|
||||||
@@ -209,6 +217,20 @@ configure_ssh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
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
|
# Configure SSH
|
||||||
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
|
if [ -n "${SSH_ENABLE_PORT+x}" ] && [ "${SSH_ENABLE_PORT:-0}" -ne 0 ] && \
|
||||||
[ -n "${SSH_ALLOWED_KEY+x}" ] && [ -n "${SSH_ALLOWED_KEY}" ]; then
|
[ -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)"
|
log "SSH not configured (missing SSH_ENABLE_PORT or SSH_ALLOWED_KEY)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait for required sockets
|
# Get and detect GPU(s)
|
||||||
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_gpu_info || {
|
get_gpu_info || {
|
||||||
log "Error: Failed to detect GPU information."
|
log "Error: Failed to detect GPU information."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -307,17 +319,23 @@ main() {
|
|||||||
log "Ensuring user directory permissions..."
|
log "Ensuring user directory permissions..."
|
||||||
chown_user_directory || exit 1
|
chown_user_directory || exit 1
|
||||||
|
|
||||||
# Setup namespaceless env
|
# Setup namespaceless env if needed for container runtime
|
||||||
|
if [[ "$container_runtime" != "podman" ]]; then
|
||||||
log "Applying namespace-less configuration"
|
log "Applying namespace-less configuration"
|
||||||
setup_namespaceless
|
setup_namespaceless
|
||||||
|
fi
|
||||||
|
|
||||||
# Switch to nestri user
|
# Switch to nestri runner entrypoint
|
||||||
log "Switching to nestri user for application startup..."
|
log "Switching to application startup entrypoint..."
|
||||||
if [[ ! -x /etc/nestri/entrypoint_nestri.sh ]]; then
|
if [[ ! -f /etc/nestri/entrypoint_nestri.sh ]]; then
|
||||||
log "Error: Entry point script /etc/nestri/entrypoint_nestri.sh not found or not executable."
|
log "Error: Application entrypoint script /etc/nestri/entrypoint_nestri.sh not found"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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 signals for clean exit
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
log() {
|
# Common helpers as requirement
|
||||||
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
|
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
|
# Parses resolution string
|
||||||
parse_resolution() {
|
parse_resolution() {
|
||||||
@@ -23,20 +27,10 @@ parse_resolution() {
|
|||||||
return 0
|
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
|
# Configuration
|
||||||
MAX_RETRIES=3
|
MAX_RETRIES=3
|
||||||
RETRY_COUNT=0
|
RETRY_COUNT=0
|
||||||
|
WAYLAND_READY_DELAY=3
|
||||||
|
|
||||||
# Kills process if running
|
# Kills process if running
|
||||||
kill_if_running() {
|
kill_if_running() {
|
||||||
@@ -125,15 +119,15 @@ start_nestri_server() {
|
|||||||
WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1"
|
WAYLAND_SOCKET="${XDG_RUNTIME_DIR}/wayland-1"
|
||||||
for ((i=1; i<=15; i++)); do
|
for ((i=1; i<=15; i++)); do
|
||||||
if [[ -e "$WAYLAND_SOCKET" ]]; then
|
if [[ -e "$WAYLAND_SOCKET" ]]; then
|
||||||
log "Wayland display 'wayland-1' ready."
|
log "Wayland display 'wayland-1' ready"
|
||||||
sleep 3
|
sleep "${WAYLAND_READY_DELAY:-3}"
|
||||||
start_compositor
|
start_compositor
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
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
|
# Workaround for gstreamer being bit slow at times
|
||||||
log "Clearing gstreamer cache.."
|
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}"
|
NESTRI_LAUNCH_COMPOSITOR="gamescope --backend wayland --force-grab-cursor -g -f --rt --mangoapp -W ${WIDTH} -H ${HEIGHT} -r ${FRAMERATE:-60}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Start Steam patcher only if Steam command is present
|
# Start Steam patcher only if Steam command is present and if needed for container runtime
|
||||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]]; then
|
if [[ -n "${NESTRI_LAUNCH_CMD}" ]] && [[ "$NESTRI_LAUNCH_CMD" == *"steam"* ]] && [[ "${container_runtime:-}" != "podman" ]]; then
|
||||||
start_steam_namespaceless_patcher
|
start_steam_namespaceless_patcher
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -200,17 +194,23 @@ start_compositor() {
|
|||||||
local OUTPUT_NAME
|
local OUTPUT_NAME
|
||||||
OUTPUT_NAME=$(WAYLAND_DISPLAY=wayland-0 wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name' | head -n 1)
|
OUTPUT_NAME=$(WAYLAND_DISPLAY=wayland-0 wlr-randr --json | jq -r '.[] | select(.enabled == true) | .name' | head -n 1)
|
||||||
if [ -z "$OUTPUT_NAME" ]; then
|
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
|
return
|
||||||
fi
|
fi
|
||||||
WAYLAND_DISPLAY=wayland-0 wlr-randr --output "$OUTPUT_NAME" --custom-mode "$WIDTH"x"$HEIGHT"
|
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
|
fi
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)."
|
log "Warning: Compositor socket not found after 15 seconds ($COMPOSITOR_SOCKET)"
|
||||||
else
|
else
|
||||||
# Launch standalone application if no compositor
|
# Launch standalone application if no compositor
|
||||||
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
|
if [[ -n "${NESTRI_LAUNCH_CMD}" ]]; then
|
||||||
@@ -218,7 +218,7 @@ start_compositor() {
|
|||||||
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
WAYLAND_DISPLAY=wayland-1 /bin/bash -c "$NESTRI_LAUNCH_CMD" &
|
||||||
APP_PID=$!
|
APP_PID=$!
|
||||||
else
|
else
|
||||||
log "No compositor or application configured."
|
log "No compositor or application configured"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -228,7 +228,7 @@ increment_retry() {
|
|||||||
local component="$1"
|
local component="$1"
|
||||||
((RETRY_COUNT++))
|
((RETRY_COUNT++))
|
||||||
if [[ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]]; then
|
if [[ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]]; then
|
||||||
log "Error: Max retries reached for $component."
|
log "Error: Max retries reached for $component"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -259,22 +259,22 @@ main_loop() {
|
|||||||
sleep 1
|
sleep 1
|
||||||
# Check nestri-server
|
# Check nestri-server
|
||||||
if [[ -n "${NESTRI_PID:-}" ]] && ! kill -0 "${NESTRI_PID}" 2>/dev/null; then
|
if [[ -n "${NESTRI_PID:-}" ]] && ! kill -0 "${NESTRI_PID}" 2>/dev/null; then
|
||||||
log "nestri-server died."
|
log "nestri-server died"
|
||||||
increment_retry "nestri-server"
|
increment_retry "nestri-server"
|
||||||
restart_chain
|
restart_chain
|
||||||
# Check compositor
|
# Check compositor
|
||||||
elif [[ -n "${COMPOSITOR_PID:-}" ]] && ! kill -0 "${COMPOSITOR_PID}" 2>/dev/null; then
|
elif [[ -n "${COMPOSITOR_PID:-}" ]] && ! kill -0 "${COMPOSITOR_PID}" 2>/dev/null; then
|
||||||
log "compositor died."
|
log "compositor died"
|
||||||
increment_retry "compositor"
|
increment_retry "compositor"
|
||||||
start_compositor
|
start_compositor
|
||||||
# Check application
|
# Check application
|
||||||
elif [[ -n "${APP_PID:-}" ]] && ! kill -0 "${APP_PID}" 2>/dev/null; then
|
elif [[ -n "${APP_PID:-}" ]] && ! kill -0 "${APP_PID}" 2>/dev/null; then
|
||||||
log "application died."
|
log "application died"
|
||||||
increment_retry "application"
|
increment_retry "application"
|
||||||
start_compositor
|
start_compositor
|
||||||
# Check patcher
|
# Check patcher
|
||||||
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
|
elif [[ -n "${PATCHER_PID:-}" ]] && ! kill -0 "${PATCHER_PID}" 2>/dev/null; then
|
||||||
log "steam-patcher died."
|
log "steam-patcher died"
|
||||||
increment_retry "steam-patcher"
|
increment_retry "steam-patcher"
|
||||||
start_steam_namespaceless_patcher
|
start_steam_namespaceless_patcher
|
||||||
fi
|
fi
|
||||||
@@ -282,8 +282,16 @@ main_loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
load_envs
|
|
||||||
parse_resolution "${RESOLUTION:-1920x1080}" || exit 1
|
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
|
restart_chain
|
||||||
main_loop
|
main_loop
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/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 XDG_SESSION_TYPE=x11
|
||||||
export DISPLAY=:0
|
export DISPLAY=:0
|
||||||
export $(dbus-launch)
|
|
||||||
|
|
||||||
# Causes some setups to break
|
# Causes some setups to break
|
||||||
export PROTON_NO_FSYNC=1
|
export PROTON_NO_FSYNC=1
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
[supervisord]
|
[supervisord]
|
||||||
user=root
|
|
||||||
nodaemon=true
|
nodaemon=true
|
||||||
loglevel=info
|
loglevel=info
|
||||||
logfile=/tmp/supervisord.log
|
logfile=/tmp/supervisord.log
|
||||||
|
|
||||||
[program:dbus]
|
[program:dbus]
|
||||||
user=root
|
|
||||||
command=dbus-daemon --system --nofork --nopidfile
|
command=dbus-daemon --system --nofork --nopidfile
|
||||||
autorestart=true
|
autorestart=true
|
||||||
autostart=true
|
autostart=true
|
||||||
startretries=3
|
startretries=3
|
||||||
priority=1
|
priority=1
|
||||||
|
environment=XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||||
[program:seatd]
|
|
||||||
user=root
|
|
||||||
command=seatd
|
|
||||||
autorestart=true
|
|
||||||
autostart=true
|
|
||||||
startretries=3
|
|
||||||
priority=2
|
|
||||||
|
|
||||||
[program:pipewire]
|
[program:pipewire]
|
||||||
user=nestri
|
user=nestri
|
||||||
@@ -28,6 +19,7 @@ autostart=true
|
|||||||
startretries=3
|
startretries=3
|
||||||
priority=3
|
priority=3
|
||||||
nice=-10
|
nice=-10
|
||||||
|
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||||
|
|
||||||
[program:pipewire-pulse]
|
[program:pipewire-pulse]
|
||||||
user=nestri
|
user=nestri
|
||||||
@@ -37,6 +29,7 @@ autostart=true
|
|||||||
startretries=3
|
startretries=3
|
||||||
priority=4
|
priority=4
|
||||||
nice=-10
|
nice=-10
|
||||||
|
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||||
|
|
||||||
[program:wireplumber]
|
[program:wireplumber]
|
||||||
user=nestri
|
user=nestri
|
||||||
@@ -46,9 +39,9 @@ autostart=true
|
|||||||
startretries=3
|
startretries=3
|
||||||
priority=5
|
priority=5
|
||||||
nice=-10
|
nice=-10
|
||||||
|
environment=HOME=%(ENV_NESTRI_HOME)s,XDG_RUNTIME_DIR=%(ENV_NESTRI_XDG_RUNTIME_DIR)s
|
||||||
|
|
||||||
[program:entrypoint]
|
[program:entrypoint]
|
||||||
user=root
|
|
||||||
command=/etc/nestri/entrypoint.sh
|
command=/etc/nestri/entrypoint.sh
|
||||||
autorestart=false
|
autorestart=false
|
||||||
autostart=true
|
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"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gstreamer = { version = "0.23", features = ["v1_26"] }
|
gstreamer = { version = "0.24", features = ["v1_26"] }
|
||||||
gstreamer-webrtc = { version = "0.23", features = ["v1_26"] }
|
gstreamer-webrtc = { version = "0.24", features = ["v1_26"] }
|
||||||
gst-plugin-webrtc = { version = "0.13", features = ["v1_22"] }
|
gst-plugin-webrtc = { version = "0.14" }
|
||||||
serde = {version = "1.0", features = ["derive"] }
|
serde = {version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.45", features = ["full"] }
|
tokio = { version = "1.45", features = ["full"] }
|
||||||
tokio-stream = { version = "0.1", features = ["full"] }
|
tokio-stream = { version = "0.1", features = ["full"] }
|
||||||
@@ -28,6 +28,14 @@ prost-types = "0.14"
|
|||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
atomic_refcell = "0.1"
|
atomic_refcell = "0.1"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
libp2p = { version = "0.55", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
|
libp2p = { version = "0.56", features = ["identify", "dns", "tcp", "noise", "ping", "tokio", "serde", "yamux", "macros", "websocket", "autonat"] }
|
||||||
libp2p-stream = { version = "0.3.0-alpha" }
|
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"
|
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::args::encoding_args::AudioCaptureMethod;
|
||||||
use crate::enc_helper::{AudioCodec, EncoderType, VideoCodec};
|
use crate::enc_helper::{AudioCodec, EncoderType, VideoCodec};
|
||||||
|
use clap::builder::TypedValueParser;
|
||||||
use clap::builder::{BoolishValueParser, NonEmptyStringValueParser};
|
use clap::builder::{BoolishValueParser, NonEmptyStringValueParser};
|
||||||
use clap::{Arg, Command, value_parser};
|
use clap::{Arg, Command, value_parser};
|
||||||
|
|
||||||
@@ -25,15 +26,6 @@ impl Args {
|
|||||||
.value_parser(BoolishValueParser::new())
|
.value_parser(BoolishValueParser::new())
|
||||||
.default_value("false"),
|
.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(
|
||||||
Arg::new("relay-url")
|
Arg::new("relay-url")
|
||||||
.short('u')
|
.short('u')
|
||||||
@@ -88,8 +80,8 @@ impl Args {
|
|||||||
.long("gpu-index")
|
.long("gpu-index")
|
||||||
.env("GPU_INDEX")
|
.env("GPU_INDEX")
|
||||||
.help("GPU to use by index")
|
.help("GPU to use by index")
|
||||||
.value_parser(value_parser!(i32).range(-1..))
|
.value_parser(value_parser!(u32).range(0..))
|
||||||
.default_value("-1"),
|
.required(false),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("gpu-card-path")
|
Arg::new("gpu-card-path")
|
||||||
@@ -154,13 +146,24 @@ impl Args {
|
|||||||
.value_parser(value_parser!(EncoderType))
|
.value_parser(value_parser!(EncoderType))
|
||||||
.default_value("hardware"),
|
.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(
|
||||||
Arg::new("audio-capture-method")
|
Arg::new("audio-capture-method")
|
||||||
.long("audio-capture-method")
|
.long("audio-capture-method")
|
||||||
.env("AUDIO_CAPTURE_METHOD")
|
.env("AUDIO_CAPTURE_METHOD")
|
||||||
.help("Audio capture method")
|
.help("Audio capture method")
|
||||||
.value_parser(value_parser!(AudioCaptureMethod))
|
.value_parser(value_parser!(AudioCaptureMethod))
|
||||||
.default_value("pulseaudio"),
|
.default_value("pipewire"),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("audio-codec")
|
Arg::new("audio-codec")
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
pub struct AppArgs {
|
pub struct AppArgs {
|
||||||
/// Verbose output mode
|
/// Verbose output mode
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
/// Enable additional debug information and features, may affect performance
|
|
||||||
pub debug: bool,
|
|
||||||
|
|
||||||
/// Virtual display resolution
|
/// Virtual display resolution
|
||||||
pub resolution: (u32, u32),
|
pub resolution: (u32, u32),
|
||||||
@@ -15,13 +13,13 @@ pub struct AppArgs {
|
|||||||
pub room: String,
|
pub room: String,
|
||||||
|
|
||||||
/// Experimental DMA-BUF support
|
/// Experimental DMA-BUF support
|
||||||
|
/// TODO: Move to video encoding flags
|
||||||
pub dma_buf: bool,
|
pub dma_buf: bool,
|
||||||
}
|
}
|
||||||
impl AppArgs {
|
impl AppArgs {
|
||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
Self {
|
Self {
|
||||||
verbose: matches.get_one::<bool>("verbose").unwrap_or(&false).clone(),
|
verbose: matches.get_one::<bool>("verbose").unwrap_or(&false).clone(),
|
||||||
debug: matches.get_one::<bool>("debug").unwrap_or(&false).clone(),
|
|
||||||
resolution: {
|
resolution: {
|
||||||
let res = matches
|
let res = matches
|
||||||
.get_one::<String>("resolution")
|
.get_one::<String>("resolution")
|
||||||
@@ -54,7 +52,6 @@ impl AppArgs {
|
|||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
tracing::info!("AppArgs:");
|
tracing::info!("AppArgs:");
|
||||||
tracing::info!("> verbose: {}", self.verbose);
|
tracing::info!("> verbose: {}", self.verbose);
|
||||||
tracing::info!("> debug: {}", self.debug);
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"> resolution: '{}x{}'",
|
"> resolution: '{}x{}'",
|
||||||
self.resolution.0,
|
self.resolution.0,
|
||||||
|
|||||||
@@ -1,37 +1,48 @@
|
|||||||
pub struct DeviceArgs {
|
pub struct DeviceArgs {
|
||||||
/// GPU vendor (e.g. "intel")
|
/// GPU vendor (e.g. "intel")
|
||||||
pub gpu_vendor: String,
|
pub gpu_vendor: Option<String>,
|
||||||
/// GPU name (e.g. "a770")
|
/// GPU name (e.g. "a770")
|
||||||
pub gpu_name: String,
|
pub gpu_name: Option<String>,
|
||||||
/// GPU index, if multiple same GPUs are present, -1 for auto-selection
|
/// GPU index, if multiple same GPUs are present, None for auto-selection
|
||||||
pub gpu_index: i32,
|
pub gpu_index: Option<u32>,
|
||||||
/// GPU card/render path, sets card explicitly from such path
|
/// GPU card/render path, sets card explicitly from such path
|
||||||
pub gpu_card_path: String,
|
pub gpu_card_path: Option<String>,
|
||||||
}
|
}
|
||||||
impl DeviceArgs {
|
impl DeviceArgs {
|
||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
Self {
|
Self {
|
||||||
gpu_vendor: matches
|
gpu_vendor: matches
|
||||||
.get_one::<String>("gpu-vendor")
|
.get_one::<String>("gpu-vendor")
|
||||||
.unwrap_or(&"".to_string())
|
.cloned(),
|
||||||
.clone(),
|
|
||||||
gpu_name: matches
|
gpu_name: matches
|
||||||
.get_one::<String>("gpu-name")
|
.get_one::<String>("gpu-name")
|
||||||
.unwrap_or(&"".to_string())
|
.cloned(),
|
||||||
.clone(),
|
gpu_index: matches
|
||||||
gpu_index: matches.get_one::<i32>("gpu-index").unwrap_or(&-1).clone(),
|
.get_one::<u32>("gpu-index")
|
||||||
|
.cloned(),
|
||||||
gpu_card_path: matches
|
gpu_card_path: matches
|
||||||
.get_one::<String>("gpu-card-path")
|
.get_one::<String>("gpu-card-path")
|
||||||
.unwrap_or(&"".to_string())
|
.cloned(),
|
||||||
.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
tracing::info!("DeviceArgs:");
|
tracing::info!("DeviceArgs:");
|
||||||
tracing::info!("> gpu_vendor: '{}'", self.gpu_vendor);
|
tracing::info!(
|
||||||
tracing::info!("> gpu_name: '{}'", self.gpu_name);
|
"> gpu_vendor: '{}'",
|
||||||
tracing::info!("> gpu_index: {}", self.gpu_index);
|
self.gpu_vendor.as_deref().unwrap_or("auto")
|
||||||
tracing::info!("> gpu_card_path: '{}'", self.gpu_card_path);
|
);
|
||||||
|
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.)
|
/// Codec (e.g. "h264", "opus" etc.)
|
||||||
pub codec: Codec,
|
pub codec: Codec,
|
||||||
/// Overridable encoder (e.g. "vah264lpenc", "opusenc" etc.)
|
/// Overridable encoder (e.g. "vah264lpenc", "opusenc" etc.)
|
||||||
pub encoder: String,
|
pub encoder: Option<String>,
|
||||||
/// Rate control method (e.g. "cqp", "vbr", "cbr")
|
/// Rate control method (e.g. "cqp", "vbr", "cbr")
|
||||||
pub rate_control: RateControl,
|
pub rate_control: RateControl,
|
||||||
}
|
}
|
||||||
impl EncodingOptionsBase {
|
impl EncodingOptionsBase {
|
||||||
pub fn debug_print(&self) {
|
pub fn debug_print(&self) {
|
||||||
tracing::info!("> Codec: '{}'", self.codec.as_str());
|
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 {
|
match &self.rate_control {
|
||||||
RateControl::CQP(cqp) => {
|
RateControl::CQP(cqp) => {
|
||||||
tracing::info!("> Rate Control: CQP");
|
tracing::info!("> Rate Control: CQP");
|
||||||
@@ -93,6 +93,7 @@ impl EncodingOptionsBase {
|
|||||||
pub struct VideoEncodingOptions {
|
pub struct VideoEncodingOptions {
|
||||||
pub base: EncodingOptionsBase,
|
pub base: EncodingOptionsBase,
|
||||||
pub encoder_type: EncoderType,
|
pub encoder_type: EncoderType,
|
||||||
|
pub bit_depth: u32,
|
||||||
}
|
}
|
||||||
impl VideoEncodingOptions {
|
impl VideoEncodingOptions {
|
||||||
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
pub fn from_matches(matches: &clap::ArgMatches) -> Self {
|
||||||
@@ -104,10 +105,7 @@ impl VideoEncodingOptions {
|
|||||||
.unwrap_or(&VideoCodec::H264)
|
.unwrap_or(&VideoCodec::H264)
|
||||||
.clone(),
|
.clone(),
|
||||||
),
|
),
|
||||||
encoder: matches
|
encoder: matches.get_one::<String>("video-encoder").cloned(),
|
||||||
.get_one::<String>("video-encoder")
|
|
||||||
.unwrap_or(&"".to_string())
|
|
||||||
.clone(),
|
|
||||||
rate_control: match matches
|
rate_control: match matches
|
||||||
.get_one::<RateControlMethod>("video-rate-control")
|
.get_one::<RateControlMethod>("video-rate-control")
|
||||||
.unwrap_or(&RateControlMethod::CBR)
|
.unwrap_or(&RateControlMethod::CBR)
|
||||||
@@ -132,6 +130,10 @@ impl VideoEncodingOptions {
|
|||||||
.get_one::<EncoderType>("video-encoder-type")
|
.get_one::<EncoderType>("video-encoder-type")
|
||||||
.unwrap_or(&EncoderType::HARDWARE)
|
.unwrap_or(&EncoderType::HARDWARE)
|
||||||
.clone(),
|
.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:");
|
tracing::info!("Video Encoding Options:");
|
||||||
self.base.debug_print();
|
self.base.debug_print();
|
||||||
tracing::info!("> Encoder Type: {}", self.encoder_type.as_str());
|
tracing::info!("> Encoder Type: {}", self.encoder_type.as_str());
|
||||||
|
tracing::info!("> Bit Depth: {}", self.bit_depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Deref for VideoEncodingOptions {
|
impl Deref for VideoEncodingOptions {
|
||||||
@@ -191,10 +194,7 @@ impl AudioEncodingOptions {
|
|||||||
.unwrap_or(&AudioCodec::OPUS)
|
.unwrap_or(&AudioCodec::OPUS)
|
||||||
.clone(),
|
.clone(),
|
||||||
),
|
),
|
||||||
encoder: matches
|
encoder: matches.get_one::<String>("audio-encoder").cloned(),
|
||||||
.get_one::<String>("audio-encoder")
|
|
||||||
.unwrap_or(&"".to_string())
|
|
||||||
.clone(),
|
|
||||||
rate_control: match matches
|
rate_control: match matches
|
||||||
.get_one::<RateControlMethod>("audio-rate-control")
|
.get_one::<RateControlMethod>("audio-rate-control")
|
||||||
.unwrap_or(&RateControlMethod::CBR)
|
.unwrap_or(&RateControlMethod::CBR)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::args::encoding_args::RateControl;
|
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 clap::ValueEnum;
|
||||||
use gstreamer::prelude::*;
|
use gstreamer::prelude::*;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
@@ -148,7 +148,7 @@ impl VideoEncoderInfo {
|
|||||||
|
|
||||||
pub fn apply_parameters(&self, element: &gstreamer::Element, verbose: bool) {
|
pub fn apply_parameters(&self, element: &gstreamer::Element, verbose: bool) {
|
||||||
for (key, value) in &self.parameters {
|
for (key, value) in &self.parameters {
|
||||||
if element.has_property(key, None) {
|
if element.has_property(key) {
|
||||||
if verbose {
|
if verbose {
|
||||||
tracing::debug!("Setting property {} to {}", key, value);
|
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(
|
pub fn encoder_low_latency_params(
|
||||||
encoder: &VideoEncoderInfo,
|
encoder: &VideoEncoderInfo,
|
||||||
rate_control: &RateControl,
|
_rate_control: &RateControl,
|
||||||
framerate: u32,
|
framerate: u32,
|
||||||
) -> VideoEncoderInfo {
|
) -> VideoEncoderInfo {
|
||||||
// 2 second GOP size, maybe lower to 1 second for fast recovery, if needed?
|
// 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 {
|
match api {
|
||||||
EncoderAPI::QSV | EncoderAPI::VAAPI => {
|
EncoderAPI::QSV | EncoderAPI::VAAPI => {
|
||||||
// Safe property access with panic protection, gstreamer-rs is fun
|
// 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"))
|
Some(element.property::<String>("device-path"))
|
||||||
} else if element.has_property("device", None) {
|
} else if element.has_property("device") {
|
||||||
Some(element.property::<String>("device"))
|
Some(element.property::<String>("device"))
|
||||||
} else {
|
} else {
|
||||||
None
|
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))
|
path.and_then(|p| get_gpu_by_card_path(&gpus, &p))
|
||||||
}
|
}
|
||||||
EncoderAPI::NVENC if element.has_property("cuda-device-id", None) => {
|
EncoderAPI::NVENC => {
|
||||||
let cuda_id = element.property::<u32>("cuda-device-id");
|
if encoder_name.contains("device") {
|
||||||
get_nvidia_gpu_by_cuda_id(&gpus, cuda_id as usize)
|
// 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();
|
||||||
}
|
}
|
||||||
EncoderAPI::AMF if element.has_property("device", None) => {
|
}
|
||||||
let device_id = element.property::<u32>("device");
|
}
|
||||||
get_gpus_by_vendor(&gpus, "amd")
|
None
|
||||||
.get(device_id as usize)
|
} 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()
|
.cloned()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,53 @@
|
|||||||
use regex::Regex;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::process::Command;
|
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
pub enum GPUVendor {
|
pub enum GPUVendor {
|
||||||
UNKNOWN,
|
UNKNOWN = 0x0000,
|
||||||
INTEL,
|
INTEL = 0x8086,
|
||||||
NVIDIA,
|
NVIDIA = 0x10de,
|
||||||
AMD,
|
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)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
@@ -19,21 +58,11 @@ pub struct GPUInfo {
|
|||||||
device_name: String,
|
device_name: String,
|
||||||
pci_bus_id: String,
|
pci_bus_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GPUInfo {
|
impl GPUInfo {
|
||||||
pub fn vendor(&self) -> &GPUVendor {
|
pub fn vendor(&self) -> &GPUVendor {
|
||||||
&self.vendor
|
&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 {
|
pub fn card_path(&self) -> &str {
|
||||||
&self.card_path
|
&self.card_path
|
||||||
}
|
}
|
||||||
@@ -49,73 +78,122 @@ impl GPUInfo {
|
|||||||
pub fn pci_bus_id(&self) -> &str {
|
pub fn pci_bus_id(&self) -> &str {
|
||||||
&self.pci_bus_id
|
&self.pci_bus_id
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn get_gpu_vendor(vendor_id: &str) -> GPUVendor {
|
pub fn as_str(&self) -> String {
|
||||||
match vendor_id {
|
format!(
|
||||||
"8086" => GPUVendor::INTEL,
|
"{} (Vendor: {}, Card Path: {}, Render Path: {}, PCI Bus ID: {})",
|
||||||
"10de" => GPUVendor::NVIDIA,
|
self.device_name, self.vendor, self.card_path, self.render_path, self.pci_bus_id
|
||||||
"1002" => GPUVendor::AMD,
|
)
|
||||||
_ => GPUVendor::UNKNOWN,
|
}
|
||||||
|
}
|
||||||
|
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.
|
/// Retrieves a list of GPUs available on the system.
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `Vec<GPUInfo>` - A vector containing information about each GPU.
|
/// * `Vec<GPUInfo>` - A vector containing information about each GPU.
|
||||||
pub fn get_gpus() -> Vec<GPUInfo> {
|
pub fn get_gpus() -> Result<Vec<GPUInfo>, Box<dyn Error>> {
|
||||||
let output = Command::new("lspci")
|
// Use "/sys/class/drm/card{}" to find all GPU devices
|
||||||
.args(["-mm", "-nn"])
|
let mut gpus = Vec::new();
|
||||||
.output()
|
let re = regex::Regex::new(r"^card(\d+)$")?;
|
||||||
.expect("Failed to execute lspci");
|
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)
|
// We are only interested in entries that match "cardN", and getting the minor number
|
||||||
.unwrap()
|
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()
|
.lines()
|
||||||
.filter_map(|line| parse_pci_device(line))
|
.find_map(|line| {
|
||||||
.filter(|(class_id, _, _, _)| matches!(class_id.as_str(), "0300" | "0302" | "0380"))
|
if line.starts_with("PCI_SLOT_NAME=") {
|
||||||
.filter_map(|(_, vendor_id, device_name, pci_addr)| {
|
Some(line.trim_start_matches("PCI_SLOT_NAME=").to_owned())
|
||||||
get_dri_device_path(&pci_addr)
|
} else {
|
||||||
.map(|(card, render)| (vendor_id, card, render, device_name, pci_addr))
|
None
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.map(
|
.ok_or("PCI_SLOT_NAME not found")?;
|
||||||
|(vid, card_path, render_path, device_name, pci_bus_id)| GPUInfo {
|
|
||||||
vendor: get_gpu_vendor(&vid),
|
// 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,
|
card_path,
|
||||||
render_path,
|
render_path,
|
||||||
device_name,
|
device_name,
|
||||||
pci_bus_id,
|
pci_bus_id,
|
||||||
},
|
});
|
||||||
)
|
}
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_pci_device(line: &str) -> Option<(String, String, String, String)> {
|
Ok(gpus)
|
||||||
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();
|
|
||||||
|
|
||||||
let caps = re.captures(line)?;
|
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();
|
||||||
|
|
||||||
// Clean device name by removing only the trailing device ID
|
for line in pci_data.lines() {
|
||||||
let device_name = caps.name("device_name")?.as_str().trim();
|
// Skip comments and empty lines
|
||||||
let clean_re = Regex::new(r"\s+\[[0-9a-f]{4}\]$").unwrap();
|
if line.starts_with('#') || line.is_empty() {
|
||||||
let cleaned_name = clean_re.replace(device_name, "").trim().to_string();
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Some((
|
// Check for vendor lines (no leading whitespace)
|
||||||
caps.name("class_id")?.as_str().to_lowercase(),
|
if !line.starts_with(['\t', ' ']) {
|
||||||
caps.name("vendor_id")?.as_str().to_lowercase(),
|
let mut parts = line.splitn(2, ' ');
|
||||||
cleaned_name,
|
if let (Some(vendor), Some(_)) = (parts.next(), parts.next()) {
|
||||||
caps.name("pci_addr")?.as_str().to_string(),
|
current_vendor = vendor.to_lowercase();
|
||||||
))
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)> {
|
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()?;
|
let entries = fs::read_dir("/sys/bus/pci/devices").ok()?;
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
if !entry.path().to_string_lossy().contains(&target_dir) {
|
if !entry.path().to_string_lossy().contains(&pci_addr) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +223,9 @@ fn get_dri_device_path(pci_addr: &str) -> Option<(String, String)> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: &str) -> Vec<GPUInfo> {
|
pub fn get_gpus_by_vendor(gpus: &[GPUInfo], vendor: GPUVendor) -> Vec<GPUInfo> {
|
||||||
let target = vendor.to_lowercase();
|
|
||||||
gpus.iter()
|
gpus.iter()
|
||||||
.filter(|gpu| gpu.vendor_string().to_lowercase() == target)
|
.filter(|gpu| *gpu.vendor() == vendor)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -169,42 +246,22 @@ pub fn get_gpu_by_card_path(gpus: &[GPUInfo], path: &str) -> Option<GPUInfo> {
|
|||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_nvidia_gpu_by_cuda_id(gpus: &[GPUInfo], cuda_device_id: usize) -> Option<GPUInfo> {
|
#[cfg(test)]
|
||||||
// Check if nvidia-smi is available
|
mod tests {
|
||||||
if Command::new("nvidia-smi").arg("--help").output().is_err() {
|
use super::*;
|
||||||
tracing::warn!("nvidia-smi is not available");
|
|
||||||
return None;
|
#[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
|
// Handles gathering GPU information and selecting the most suitable GPU
|
||||||
fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
||||||
tracing::info!("Gathering GPU information..");
|
tracing::info!("Gathering GPU information..");
|
||||||
let mut gpus = gpu::get_gpus();
|
let mut gpus = gpu::get_gpus()?;
|
||||||
if gpus.is_empty() {
|
if gpus.is_empty() {
|
||||||
return Err("No GPUs found".into());
|
return Err("No GPUs found".into());
|
||||||
}
|
}
|
||||||
for (i, gpu) in gpus.iter().enumerate() {
|
for (i, gpu) in gpus.iter().enumerate() {
|
||||||
tracing::info!(
|
tracing::info!("> [GPU:{}] {}", i, gpu);
|
||||||
"> [GPU:{}] Vendor: '{}', Card Path: '{}', Render Path: '{}', Device Name: '{}'",
|
|
||||||
i,
|
|
||||||
gpu.vendor_string(),
|
|
||||||
gpu.card_path(),
|
|
||||||
gpu.render_path(),
|
|
||||||
gpu.device_name()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional GPU filtering
|
// Additional GPU filtering
|
||||||
if !args.device.gpu_card_path.is_empty() {
|
if let Some(gpu_card_path) = &args.device.gpu_card_path {
|
||||||
if let Some(gpu) = gpu::get_gpu_by_card_path(&gpus, &args.device.gpu_card_path) {
|
return match gpu::get_gpu_by_card_path(&gpus, gpu_card_path.as_str()) {
|
||||||
return Ok(Vec::from([gpu]));
|
Some(gpu) => Ok(Vec::from([gpu])),
|
||||||
}
|
None => Err(format!(
|
||||||
|
"No GPU found with the specified card path: '{}'",
|
||||||
|
gpu_card_path
|
||||||
|
)
|
||||||
|
.into()),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
// Run all filters that are not empty
|
// Run all filters that are not empty
|
||||||
let mut filtered_gpus = gpus.clone();
|
let mut filtered_gpus = gpus.clone();
|
||||||
if !args.device.gpu_vendor.is_empty() {
|
if let Some(gpu_vendor) = &args.device.gpu_vendor {
|
||||||
filtered_gpus = gpu::get_gpus_by_vendor(&filtered_gpus, &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() {
|
if let Some(gpu_name) = &args.device.gpu_name {
|
||||||
filtered_gpus = gpu::get_gpus_by_device_name(&filtered_gpus, &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
|
// 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() {
|
if gpu_index >= filtered_gpus.len() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"GPU index {} is out of bounds for available GPUs (0-{})",
|
"GPU index {} is out of bounds for available GPUs (0-{})",
|
||||||
gpu_index,
|
gpu_index,
|
||||||
filtered_gpus.len() - 1
|
filtered_gpus.len().saturating_sub(1)
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
@@ -77,10 +76,10 @@ fn handle_gpus(args: &args::Args) -> Result<Vec<gpu::GPUInfo>, Box<dyn Error>> {
|
|||||||
if gpus.is_empty() {
|
if gpus.is_empty() {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"No GPU(s) found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
"No GPU(s) found with the specified parameters: vendor='{}', name='{}', index='{}', card_path='{}'",
|
||||||
args.device.gpu_vendor,
|
args.device.gpu_vendor.as_deref().unwrap_or("auto"),
|
||||||
args.device.gpu_name,
|
args.device.gpu_name.as_deref().unwrap_or("auto"),
|
||||||
args.device.gpu_index,
|
args.device.gpu_index.map_or("auto".to_string(), |i| i.to_string()),
|
||||||
args.device.gpu_card_path
|
args.device.gpu_card_path.as_deref().unwrap_or("auto")
|
||||||
).into());
|
).into());
|
||||||
}
|
}
|
||||||
Ok(gpus)
|
Ok(gpus)
|
||||||
@@ -112,9 +111,8 @@ fn handle_encoder_video(
|
|||||||
}
|
}
|
||||||
// Pick most suitable video encoder based on given arguments
|
// Pick most suitable video encoder based on given arguments
|
||||||
let video_encoder;
|
let video_encoder;
|
||||||
if !args.encoding.video.encoder.is_empty() {
|
if let Some(wanted_encoder) = &args.encoding.video.encoder {
|
||||||
video_encoder =
|
video_encoder = enc_helper::get_encoder_by_name(&video_encoders, wanted_encoder.as_str())?;
|
||||||
enc_helper::get_encoder_by_name(&video_encoders, &args.encoding.video.encoder)?;
|
|
||||||
} else {
|
} else {
|
||||||
video_encoder = enc_helper::get_best_working_encoder(
|
video_encoder = enc_helper::get_best_working_encoder(
|
||||||
&video_encoders,
|
&video_encoders,
|
||||||
@@ -164,11 +162,12 @@ fn handle_encoder_video_settings(
|
|||||||
// Handles picking audio encoder
|
// Handles picking audio encoder
|
||||||
// TODO: Expand enc_helper with audio types, for now just opus
|
// TODO: Expand enc_helper with audio types, for now just opus
|
||||||
fn handle_encoder_audio(args: &args::Args) -> String {
|
fn handle_encoder_audio(args: &args::Args) -> String {
|
||||||
let audio_encoder = if args.encoding.audio.encoder.is_empty() {
|
let audio_encoder = args
|
||||||
"opusenc".to_string()
|
.encoding
|
||||||
} else {
|
.audio
|
||||||
args.encoding.audio.encoder.clone()
|
.encoder
|
||||||
};
|
.clone()
|
||||||
|
.unwrap_or_else(|| "opusenc".to_string());
|
||||||
tracing::info!("Selected audio encoder: '{}'", audio_encoder);
|
tracing::info!("Selected audio encoder: '{}'", audio_encoder);
|
||||||
audio_encoder
|
audio_encoder
|
||||||
}
|
}
|
||||||
@@ -197,10 +196,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
// Get relay URL from arguments
|
// Get relay URL from arguments
|
||||||
let relay_url = args.app.relay_url.trim();
|
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()?;
|
gstreamer::init()?;
|
||||||
let _ = gstrswebrtc::plugin_register_static(); // Might be already registered, so we'll pass..
|
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
|
// Handle audio encoder selection
|
||||||
let audio_encoder = handle_encoder_audio(&args);
|
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 ***/
|
/*** PIPELINE CREATION ***/
|
||||||
// Create the pipeline
|
// Create the pipeline
|
||||||
let pipeline = Arc::new(gstreamer::Pipeline::new());
|
let pipeline = Arc::new(gstreamer::Pipeline::new());
|
||||||
@@ -250,7 +249,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
gstreamer::ElementFactory::make("pulsesrc").build()?
|
gstreamer::ElementFactory::make("pulsesrc").build()?
|
||||||
}
|
}
|
||||||
encoding_args::AudioCaptureMethod::PIPEWIRE => {
|
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 => {
|
encoding_args::AudioCaptureMethod::ALSA => {
|
||||||
gstreamer::ElementFactory::make("alsasrc").build()?
|
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 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");
|
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);
|
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..)
|
// GL and CUDA elements (NVIDIA only..)
|
||||||
let mut glupload = None;
|
let mut glupload = None;
|
||||||
let mut glconvert = None;
|
let mut glconvert = None;
|
||||||
@@ -325,7 +337,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
glconvert = Some(gstreamer::ElementFactory::make("glcolorconvert").build()?);
|
glconvert = Some(gstreamer::ElementFactory::make("glcolorconvert").build()?);
|
||||||
// GL color convert caps
|
// GL color convert caps
|
||||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
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);
|
caps_filter.set_property("caps", &gl_caps);
|
||||||
gl_caps_filter = Some(caps_filter);
|
gl_caps_filter = Some(caps_filter);
|
||||||
// CUDA upload element
|
// CUDA upload element
|
||||||
@@ -341,7 +355,9 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
vapostproc = Some(gstreamer::ElementFactory::make("vapostproc").build()?);
|
vapostproc = Some(gstreamer::ElementFactory::make("vapostproc").build()?);
|
||||||
// VA caps filter
|
// VA caps filter
|
||||||
let caps_filter = gstreamer::ElementFactory::make("capsfilter").build()?;
|
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);
|
caps_filter.set_property("caps", &va_caps);
|
||||||
va_caps_filter = Some(caps_filter);
|
va_caps_filter = Some(caps_filter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use atomic_refcell::AtomicRefCell;
|
|||||||
use glib::subclass::prelude::*;
|
use glib::subclass::prelude::*;
|
||||||
use gstreamer::glib;
|
use gstreamer::glib;
|
||||||
use gstreamer::prelude::*;
|
use gstreamer::prelude::*;
|
||||||
use gstreamer_webrtc::{gst_sdp, WebRTCSDPType, WebRTCSessionDescription};
|
use gstreamer_webrtc::{WebRTCSDPType, WebRTCSessionDescription, gst_sdp};
|
||||||
use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
use gstrswebrtc::signaller::{Signallable, SignallableImpl};
|
||||||
use parking_lot::RwLock as PLRwLock;
|
use parking_lot::RwLock as PLRwLock;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod p2p;
|
pub mod p2p;
|
||||||
pub mod p2p_safestream;
|
|
||||||
pub mod p2p_protocol_stream;
|
pub mod p2p_protocol_stream;
|
||||||
|
pub mod p2p_safestream;
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
use libp2p::futures::StreamExt;
|
use libp2p::futures::StreamExt;
|
||||||
use libp2p::multiaddr::Protocol;
|
use libp2p::multiaddr::Protocol;
|
||||||
use libp2p::{
|
use libp2p::{
|
||||||
Multiaddr, PeerId, Swarm, identify, noise, ping,
|
Multiaddr, PeerId, Swarm, identity,
|
||||||
swarm::{NetworkBehaviour, SwarmEvent},
|
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::error::Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -12,15 +18,28 @@ use tokio::sync::Mutex;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NestriConnection {
|
pub struct NestriConnection {
|
||||||
pub peer_id: PeerId,
|
pub peer_id: PeerId,
|
||||||
pub control: libp2p_stream::Control,
|
pub control: stream::Control,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(NetworkBehaviour)]
|
#[derive(NetworkBehaviour)]
|
||||||
struct NestriBehaviour {
|
struct NestriBehaviour {
|
||||||
identify: identify::Behaviour,
|
identify: identify::Behaviour,
|
||||||
ping: ping::Behaviour,
|
ping: ping::Behaviour,
|
||||||
stream: libp2p_stream::Behaviour,
|
stream: stream::Behaviour,
|
||||||
autonatv2: libp2p::autonat::v2::client::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 {
|
pub struct NestriP2P {
|
||||||
@@ -39,22 +58,7 @@ impl NestriP2P {
|
|||||||
.with_dns()?
|
.with_dns()?
|
||||||
.with_websocket(noise::Config::new, yamux::Config::default)
|
.with_websocket(noise::Config::new, yamux::Config::default)
|
||||||
.await?
|
.await?
|
||||||
.with_behaviour(|key| {
|
.with_behaviour(|key| NestriBehaviour::new(key.public()))?
|
||||||
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,
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.build(),
|
.build(),
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -62,12 +66,6 @@ impl NestriP2P {
|
|||||||
let swarm_clone = swarm.clone();
|
let swarm_clone = swarm.clone();
|
||||||
tokio::spawn(swarm_loop(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 })
|
Ok(NestriP2P { swarm })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +93,55 @@ async fn swarm_loop(swarm: Arc<Mutex<Swarm<NestriBehaviour>>>) {
|
|||||||
swarm_lock.select_next_some().await
|
swarm_lock.select_next_some().await
|
||||||
};
|
};
|
||||||
match event {
|
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, .. } => {
|
SwarmEvent::NewListenAddr { address, .. } => {
|
||||||
tracing::info!("Listening on: '{}'", 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);
|
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 byteorder::{BigEndian, ByteOrder};
|
||||||
use libp2p::futures::io::{ReadHalf, WriteHalf};
|
use libp2p::futures::io::{ReadHalf, WriteHalf};
|
||||||
use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
|
use libp2p::futures::{AsyncReadExt, AsyncWriteExt};
|
||||||
use prost::Message;
|
|
||||||
use serde::Serialize;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
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>> {
|
pub async fn send_raw(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
self.send_with_length_prefix(data).await
|
self.send_with_length_prefix(data).await
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user