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:
Kristian Ollikainen
2025-09-24 20:08:04 +03:00
committed by GitHub
parent aba0bc3be1
commit 590fe5e196
26 changed files with 1508 additions and 1804 deletions

View File

@@ -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"]

View 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"
}
}
}
]

View 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
}

View File

@@ -0,0 +1,21 @@
"InstallConfigStore"
{
"Software"
{
"Valve"
{
"Steam"
{
"CompatToolMapping"
{
"0"
{
"name" "proton-cachyos"
"config" ""
"priority" "75"
}
}
}
}
}
}

View 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
}
}
]
}

View 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; }

View 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}"

View File

@@ -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

View File

@@ -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
} }

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "1.89"

View File

@@ -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")

View File

@@ -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,

View File

@@ -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")
);
} }
} }

View File

@@ -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)

View File

@@ -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,
} }

View File

@@ -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() }
Ok(gpus)
} }
fn parse_pci_device(line: &str) -> Option<(String, String, String, String)> { fn parse_pci_ids(pci_data: &str, vendor_id: &str, device_id: &str) -> Option<String> {
let re = Regex::new( let mut current_vendor = String::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>[^"]+?)""#, let vendor_id = vendor_id.to_lowercase();
).unwrap(); 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 // Check for vendor lines (no leading whitespace)
let device_name = caps.name("device_name")?.as_str().trim(); if !line.starts_with(['\t', ' ']) {
let clean_re = Regex::new(r"\s+\[[0-9a-f]{4}\]$").unwrap(); let mut parts = line.splitn(2, ' ');
let cleaned_name = clean_re.replace(device_name, "").trim().to_string(); if let (Some(vendor), Some(_)) = (parts.next(), parts.next()) {
current_vendor = vendor.to_lowercase();
}
continue;
}
Some(( // Check for device lines (leading whitespace)
caps.name("class_id")?.as_str().to_lowercase(), let line = line.trim_start();
caps.name("vendor_id")?.as_str().to_lowercase(), let mut parts = line.splitn(2, ' ');
cleaned_name, if let (Some(dev_id), Some(desc)) = (parts.next(), parts.next()) {
caps.name("pci_addr")?.as_str().to_string(), 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()
} }

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
_ => {} _ => {}
} }
} }

View File

@@ -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
} }

View File

@@ -3,17 +3,17 @@
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoTimestampEntry { pub struct ProtoTimestampEntry {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub stage: ::prost::alloc::string::String, pub stage: ::prost::alloc::string::String,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub time: ::core::option::Option<::prost_types::Timestamp>, pub time: ::core::option::Option<::prost_types::Timestamp>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoLatencyTracker { pub struct ProtoLatencyTracker {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub sequence_id: ::prost::alloc::string::String, pub sequence_id: ::prost::alloc::string::String,
#[prost(message, repeated, tag="2")] #[prost(message, repeated, tag = "2")]
pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>, pub timestamps: ::prost::alloc::vec::Vec<ProtoTimestampEntry>,
} }
/// MouseMove message /// MouseMove message
@@ -21,11 +21,11 @@ pub struct ProtoLatencyTracker {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMove { pub struct ProtoMouseMove {
/// Fixed value "MouseMove" /// Fixed value "MouseMove"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseMoveAbs message /// MouseMoveAbs message
@@ -33,11 +33,11 @@ pub struct ProtoMouseMove {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseMoveAbs { pub struct ProtoMouseMoveAbs {
/// Fixed value "MouseMoveAbs" /// Fixed value "MouseMoveAbs"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseWheel message /// MouseWheel message
@@ -45,11 +45,11 @@ pub struct ProtoMouseMoveAbs {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseWheel { pub struct ProtoMouseWheel {
/// Fixed value "MouseWheel" /// Fixed value "MouseWheel"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub x: i32, pub x: i32,
#[prost(int32, tag="3")] #[prost(int32, tag = "3")]
pub y: i32, pub y: i32,
} }
/// MouseKeyDown message /// MouseKeyDown message
@@ -57,9 +57,9 @@ pub struct ProtoMouseWheel {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyDown { pub struct ProtoMouseKeyDown {
/// Fixed value "MouseKeyDown" /// Fixed value "MouseKeyDown"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// MouseKeyUp message /// MouseKeyUp message
@@ -67,9 +67,9 @@ pub struct ProtoMouseKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMouseKeyUp { pub struct ProtoMouseKeyUp {
/// Fixed value "MouseKeyUp" /// Fixed value "MouseKeyUp"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// KeyDown message /// KeyDown message
@@ -77,9 +77,9 @@ pub struct ProtoMouseKeyUp {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyDown { pub struct ProtoKeyDown {
/// Fixed value "KeyDown" /// Fixed value "KeyDown"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// KeyUp message /// KeyUp message
@@ -87,53 +87,53 @@ pub struct ProtoKeyDown {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoKeyUp { pub struct ProtoKeyUp {
/// Fixed value "KeyUp" /// Fixed value "KeyUp"
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub r#type: ::prost::alloc::string::String, pub r#type: ::prost::alloc::string::String,
#[prost(int32, tag="2")] #[prost(int32, tag = "2")]
pub key: i32, pub key: i32,
} }
/// Union of all Input types /// Union of all Input types
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoInput { 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>, pub input_type: ::core::option::Option<proto_input::InputType>,
} }
/// Nested message and enum types in `ProtoInput`. /// Nested message and enum types in `ProtoInput`.
pub mod proto_input { pub mod proto_input {
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)] #[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum InputType { pub enum InputType {
#[prost(message, tag="1")] #[prost(message, tag = "1")]
MouseMove(super::ProtoMouseMove), MouseMove(super::ProtoMouseMove),
#[prost(message, tag="2")] #[prost(message, tag = "2")]
MouseMoveAbs(super::ProtoMouseMoveAbs), MouseMoveAbs(super::ProtoMouseMoveAbs),
#[prost(message, tag="3")] #[prost(message, tag = "3")]
MouseWheel(super::ProtoMouseWheel), MouseWheel(super::ProtoMouseWheel),
#[prost(message, tag="4")] #[prost(message, tag = "4")]
MouseKeyDown(super::ProtoMouseKeyDown), MouseKeyDown(super::ProtoMouseKeyDown),
#[prost(message, tag="5")] #[prost(message, tag = "5")]
MouseKeyUp(super::ProtoMouseKeyUp), MouseKeyUp(super::ProtoMouseKeyUp),
#[prost(message, tag="6")] #[prost(message, tag = "6")]
KeyDown(super::ProtoKeyDown), KeyDown(super::ProtoKeyDown),
#[prost(message, tag="7")] #[prost(message, tag = "7")]
KeyUp(super::ProtoKeyUp), KeyUp(super::ProtoKeyUp),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageBase { pub struct ProtoMessageBase {
#[prost(string, tag="1")] #[prost(string, tag = "1")]
pub payload_type: ::prost::alloc::string::String, pub payload_type: ::prost::alloc::string::String,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub latency: ::core::option::Option<ProtoLatencyTracker>, pub latency: ::core::option::Option<ProtoLatencyTracker>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ProtoMessageInput { pub struct ProtoMessageInput {
#[prost(message, optional, tag="1")] #[prost(message, optional, tag = "1")]
pub message_base: ::core::option::Option<ProtoMessageBase>, pub message_base: ::core::option::Option<ProtoMessageBase>,
#[prost(message, optional, tag="2")] #[prost(message, optional, tag = "2")]
pub data: ::core::option::Option<ProtoInput>, pub data: ::core::option::Option<ProtoInput>,
} }
// @@protoc_insertion_point(module) // @@protoc_insertion_point(module)