Files
netris-nestri/build/usr/bin/nestri-entry
2026-04-03 00:33:36 +03:00

577 lines
20 KiB
Bash

#!/bin/bash
set -e
# ============================================================
# nestri-entry
#
# Entry point that runs inside the bwrap sandbox.
# Launched by nestri-runner.sh as the top-level process.
#
# Responsibilities:
# 1. Start PipeWire (sandbox-local audio server)
# 2. Start GStreamer pipeline (Wayland compositor + encoding + streaming)
# 3. Launch muvm (microVM that runs the actual games)
#
# The GStreamer pipeline IS the Wayland compositor:
# - waylanddisplaysrc creates $XDG_RUNTIME_DIR/wayland-1
# - VM guest connects via wl-cross-domain-proxy (virtio-gpu)
# - Video frames captured as DMA-BUF, encoded, streamed via QUIC
# - Audio captured from PipeWire, encoded as Opus, muxed with video
#
# Environment variables (set by nestri-runner.sh):
# NESTRI_WIDTH Stream width (default: 1920)
# NESTRI_HEIGHT Stream height (default: 1080)
# NESTRI_FPS Stream framerate (default: 60)
# NESTRI_BITRATE Encoder bitrate in kbps (default: 8000)
# NESTRI_RENDER_NODE GPU render node (default: /dev/dri/renderD128)
# NESTRI_BROADCAST Stream broadcast name (default: live)
# NESTRI_GPU GPU type: amd, intel, nvidia (default: amd)
# NESTRI_CODEC Video codec: h264, h265, av1 (default: h264)
# MICROVM_UID User ID inside sandbox/VM
# MICROVM_GID Group ID inside sandbox/VM
#
# muvm-guest binary symlinks (in /opt/bin/):
# muvm-remote — session runner (runs user command)
# muvm-configure-network — network setup for VM
# muvm-pwbridge — PipeWire bridge (host ↔ guest audio)
# ============================================================
# ============================================================
# Configuration (from environment or defaults)
# ============================================================
NESTRI_WIDTH="${NESTRI_WIDTH:-1920}"
NESTRI_HEIGHT="${NESTRI_HEIGHT:-1080}"
NESTRI_FPS="${NESTRI_FPS:-60}"
NESTRI_BITRATE="${NESTRI_BITRATE:-8000}"
NESTRI_RENDER_NODE="${NESTRI_RENDER_NODE:-/dev/dri/renderD128}"
NESTRI_BROADCAST="${NESTRI_BROADCAST:-live}"
MICROVM_UID="${MICROVM_UID:-1000}"
# GPU and codec selection
# GPU options: amd, intel, nvidia
# Codec options: h264, h265, av1
NESTRI_GPU="${NESTRI_GPU:-amd}"
NESTRI_CODEC="${NESTRI_CODEC:-h264}"
# Setup XDG_RUNTIME_DIR
mkdir -p "$XDG_RUNTIME_DIR"
# Track background PIDs for cleanup
PIDS=()
cleanup() {
echo "nestri-entry: shutting down..." >&2
# Kill all tracked background processes
for pid in "${PIDS[@]}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null
fi
done
# Wait briefly for graceful shutdown
sleep 0.5
# Force kill anything still alive
for pid in "${PIDS[@]}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid" 2>/dev/null
fi
done
# dbus-launch sets this separately
if [ -n "${DBUS_SESSION_BUS_PID:-}" ]; then
kill -TERM "$DBUS_SESSION_BUS_PID" 2>/dev/null
fi
exit
}
trap cleanup EXIT INT TERM
# ============================================================
# 1. D-Bus Session Bus + PipeWire (sandbox-local audio)
#
# WirePlumber requires a D-Bus session bus — it's a hard
# dependency. PipeWire itself can start without D-Bus, but
# WirePlumber (the session manager that handles routing,
# device enumeration, policy) will abort without one.
#
# Inside the VM, systemd provides the session bus via
# nestri-session-bus.service. Here in bwrap, we have no
# systemd, so we launch dbus-daemon manually.
#
# Audio flow:
# Game (in VM) → PipeWire (guest) → muvm-pwbridge → PipeWire (here)
# PipeWire (here) → pipewiresrc → GStreamer → Opus encode → QUIC
# ============================================================
# --- D-Bus session bus ---
echo "nestri-entry: starting dbus session bus..." >&2
# Create dbus directory
mkdir -p "$XDG_RUNTIME_DIR/dbus"
# Launch a session bus and capture its address
eval "$(dbus-launch --sh-syntax)"
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
echo "nestri-entry: WARNING — dbus-launch failed, trying manual start..." >&2
DBUS_SOCKET="$XDG_RUNTIME_DIR/dbus/session-bus"
dbus-daemon \
--session \
--address="unix:path=$DBUS_SOCKET" \
--nofork \
--print-address \
&>/dev/null &
DBUS_PID=$!
PIDS+=($DBUS_PID)
# Wait for socket
for i in $(seq 1 30); do
[ -S "$DBUS_SOCKET" ] && break
sleep 0.1
done
if [ -S "$DBUS_SOCKET" ]; then
export DBUS_SESSION_BUS_ADDRESS="unix:path=$DBUS_SOCKET"
echo "nestri-entry: dbus ready at $DBUS_SOCKET" >&2
else
echo "nestri-entry: WARNING — dbus-daemon failed to start" >&2
echo "nestri-entry: wireplumber will likely fail" >&2
fi
else
# dbus-launch succeeded, track its PID for cleanup
PIDS+=("${DBUS_SESSION_BUS_PID:-}")
echo "nestri-entry: dbus ready (via dbus-launch) PID=${DBUS_SESSION_BUS_PID:-}" >&2
fi
export DBUS_SESSION_BUS_ADDRESS
# --- PipeWire ---
echo "nestri-entry: starting pipewire..." >&2
pipewire &
PIDS+=($!)
# Wait for PipeWire socket
for i in $(seq 1 50); do
[ -S "$XDG_RUNTIME_DIR/pipewire-0" ] && break
sleep 0.1
done
if [ -S "$XDG_RUNTIME_DIR/pipewire-0" ]; then
echo "nestri-entry: pipewire ready" >&2
# WirePlumber needs both PipeWire AND D-Bus
if [ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
wireplumber &
PIDS+=($!)
sleep 0.5
echo "nestri-entry: wireplumber ready" >&2
else
echo "nestri-entry: WARNING — skipping wireplumber (no dbus)" >&2
echo "nestri-entry: audio routing may not work correctly" >&2
fi
else
echo "nestri-entry: WARNING — pipewire not ready, continuing without audio" >&2
fi
# Wait for WirePlumber to register the loopback node
for i in $(seq 1 50); do
if pw-cli ls Node 2>/dev/null | grep -q "nestri-source"; then
break
fi
sleep 0.1
done
if pw-cli ls Node 2>/dev/null | grep -q "nestri-source"; then
echo "nestri-entry: wireplumber ready, loopback node available" >&2
else
echo "nestri-entry: WARNING — loopback node not found after 5s" >&2
fi
# ============================================================
# 2. GStreamer Pipeline — Encoder Detection & Configuration
#
# VAAPI encoders come in two variants:
# - vaXenc — standard encoder
# - vaXlpenc — low-power variant (sometimes the only option)
#
# We probe for available encoders and use what's present.
# ============================================================
# Check if a GStreamer element exists
gst_element_exists() {
gst-inspect-1.0 "$1" &>/dev/null
}
# Find the best available VAAPI encoder for a codec
find_vaapi_encoder() {
local codec="$1"
local standard_enc="va${codec}enc"
local lowpower_enc="va${codec}lpenc"
# Prefer standard encoder, fall back to low-power
if gst_element_exists "$standard_enc"; then
echo "$standard_enc"
elif gst_element_exists "$lowpower_enc"; then
echo "$lowpower_enc"
else
echo ""
fi
}
# ============================================================
# 2. GStreamer Pipeline — Encoding Configuration
#
# GPU-specific encoding paths:
#
# NVIDIA (NVENC):
# Memory: video/x-raw(memory:CUDAMemory)
# H.264: nvh264enc (p1 preset, ultra-low-latency tune)
# H.265: nvh265enc (p1 preset, ultra-low-latency tune)
# AV1: nvav1enc (p1 preset, ultra-low-latency tune)
#
# Modern NVENC presets (p1-p7):
# p1 = fastest (lowest quality)
# p7 = slowest (highest quality)
# Tunes: ultra-low-latency, low-latency, high-quality
#
#
# Intel (QSV):
# Memory: video/x-raw(memory:DMABuf) → vapostproc → video/x-raw(memory:VAMemory)
# H.264: qsvh264enc
# H.265: qsvh265enc
# AV1: qsvav1enc
#
# target-usage is 7 for all (speed) with low-latency set to true
#
#
# AMD/Intel/.. (VAAPI):
# Memory: video/x-raw(memory:DMABuf) → vapostproc → video/x-raw(memory:VAMemory)
# H.264: vah264enc or vah264lpenc (low-power variant)
# H.265: vah265enc or vah265lpenc (low-power variant)
# AV1: vaav1enc or vaav1lpenc (low-power variant)
#
# target-usage: 1 (quality) to 7 (speed)
# For streaming, we use 7 (maximum speed/minimum latency)
#
# The vapostproc element converts DMA-BUF to VA-API memory and
# transforms to NV12 format required by the VA-API encoders.
# ============================================================
# Calculate GOP size (keyframe interval)
# For low latency, we use 1-2 seconds worth of frames
GOP_SIZE=$((NESTRI_FPS * 2))
setup_vaapi_encoder() {
# AMD/Intel VAAPI path — uses DMA-BUF → VA-API memory
# vapostproc handles colorspace conversion to NV12
GST_MEM_CAPS="video/x-raw(memory:DMABuf),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1 ! vapostproc ! video/x-raw(memory:VAMemory),format=NV12"
case "$NESTRI_CODEC" in
av1)
VAAPI_ENC=$(find_vaapi_encoder "av1")
if [ -z "$VAAPI_ENC" ]; then
echo "nestri-entry: FATAL — no VAAPI AV1 encoder found" >&2
echo "nestri-entry: tried: vaav1enc, vaav1penc" >&2
exit 1
fi
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
GST_ENC="$VAAPI_ENC"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
;;
h265)
VAAPI_ENC=$(find_vaapi_encoder "h265")
if [ -z "$VAAPI_ENC" ]; then
echo "nestri-entry: FATAL — no VAAPI H.265 encoder found" >&2
echo "nestri-entry: tried: vah265enc, vah265lpenc" >&2
exit 1
fi
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
GST_ENC="$VAAPI_ENC"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
;;
h264|*)
VAAPI_ENC=$(find_vaapi_encoder "h264")
if [ -z "$VAAPI_ENC" ]; then
echo "nestri-entry: FATAL — no VAAPI H.264 encoder found" >&2
echo "nestri-entry: tried: vah264enc, vah264lpenc" >&2
exit 1
fi
echo "nestri-entry: using VAAPI encoder: $VAAPI_ENC" >&2
GST_ENC="$VAAPI_ENC"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC key-int-max=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
;;
esac
}
# Select memory caps and encoder based on GPU type
case "$NESTRI_GPU" in
nvidia)
# NVIDIA NVENC path — uses CUDA memory
# Modern presets: p1 (fastest) to p7 (highest quality)
# Tunes: ultra-low-latency, low-latency, high-quality
GST_MEM_CAPS="video/x-raw(memory:CUDAMemory),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1"
case "$NESTRI_CODEC" in
av1)
GST_ENC="nvav1enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC preset=p1"
GST_ENC="$GST_ENC tune=ultra-low-latency"
GST_ENC="$GST_ENC multi-pass=disabled"
GST_ENC="$GST_ENC zerolatency=true"
;;
h265)
GST_ENC="nvh265enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC preset=p1"
GST_ENC="$GST_ENC tune=ultra-low-latency"
GST_ENC="$GST_ENC multi-pass=disabled"
GST_ENC="$GST_ENC zerolatency=true"
;;
h264|*)
GST_ENC="nvh264enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC preset=p1"
GST_ENC="$GST_ENC tune=ultra-low-latency"
GST_ENC="$GST_ENC multi-pass=disabled"
GST_ENC="$GST_ENC zerolatency=true"
;;
esac
;;
intel)
# Check for QSV element plugin
if gst_element_exists "qsv"; then
# Intel QSV path — uses DMA-BUF → VA-API memory
# vapostproc handles colorspace conversion to NV12
GST_MEM_CAPS="video/x-raw(memory:DMABuf),width=${NESTRI_WIDTH},height=${NESTRI_HEIGHT},framerate=${NESTRI_FPS}/1 ! vapostproc ! video/x-raw(memory:VAMemory),format=NV12"
case "$NESTRI_CODEC" in
av1)
GST_ENC="qsvav1enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
GST_ENC="$GST_ENC low-latency=true"
;;
h265)
GST_ENC="qsvh265enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
GST_ENC="$GST_ENC low-latency=true"
;;
h264|*)
GST_ENC="qsvh264enc"
GST_ENC="$GST_ENC bitrate=${NESTRI_BITRATE}"
GST_ENC="$GST_ENC gop-size=${GOP_SIZE}"
GST_ENC="$GST_ENC target-usage=7"
GST_ENC="$GST_ENC low-latency=true"
;;
esac
else
# Fallback to VAAPI if no QSV available
setup_vaapi_encoder
fi
;;
amd|*)
setup_vaapi_encoder
;;
esac
# ============================================================
# 2. GStreamer Pipeline — Launch
#
# Pipeline structure:
#
# Video path:
# waylanddisplaysrc (compositor + capture)
# → queue (buffer management)
# → [GPU-specific caps + encoder]
# → [codec parser]
#
# Audio path:
# pipewiresrc (capture from PipeWire)
# → audioconvert/audiorate/audioresample
# → opusenc (Opus encoding @ 128kbps)
#
# Output:
# nestrisink → QUIC/iroh-moq streaming
#
# waylanddisplaysrc IS the Wayland compositor:
# - Creates socket at $XDG_RUNTIME_DIR/wayland-1
# - Captures client buffers as DMA-BUF (zero-copy)
# - Input events arrive via nestrisink (bidirectional)
# ============================================================
echo "nestri-entry: starting gstreamer pipeline..." >&2
echo "nestri-entry: resolution: ${NESTRI_WIDTH}x${NESTRI_HEIGHT}@${NESTRI_FPS}fps" >&2
echo "nestri-entry: bitrate: ${NESTRI_BITRATE}kbps" >&2
echo "nestri-entry: gop-size: ${GOP_SIZE} frames" >&2
echo "nestri-entry: render: ${NESTRI_RENDER_NODE}" >&2
echo "nestri-entry: gpu: ${NESTRI_GPU}" >&2
echo "nestri-entry: codec: ${NESTRI_CODEC}" >&2
echo "nestri-entry: encoder: ${GST_ENC%% *}" >&2
echo "nestri-entry: broadcast: ${NESTRI_BROADCAST}" >&2
# Note: GST_MEM_CAPS and GST_ENC are intentionally unquoted
# to allow bash word splitting for GStreamer's '!' element separators.
gst-launch-1.0 --gst-debug-no-color -e \
nestrisink name=sink broadcast="$NESTRI_BROADCAST" \
waylanddisplaysrc render-node="$NESTRI_RENDER_NODE" ! \
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 ! \
$GST_MEM_CAPS ! \
$GST_ENC ! \
sink. \
pipewiresrc target-object="nestri-source" use-bufferpool=false do-timestamp=true ! \
queue max-size-buffers=2 max-size-time=0 max-size-bytes=0 ! \
"audio/x-raw,format=S16LE,channels=2,rate=48000" ! \
audioconvert ! \
audiorate ! \
audioresample ! \
opusenc bitrate=128000 frame-size=10 ! \
sink. \
>> /tmp/gstreamer.log 2>&1 &
GST_PID=$!
PIDS+=($GST_PID)
# Wait for compositor socket
for i in $(seq 1 100); do
[ -S "$XDG_RUNTIME_DIR/wayland-1" ] && break
sleep 0.1
done
if [ ! -S "$XDG_RUNTIME_DIR/wayland-1" ]; then
echo "nestri-entry: FATAL — compositor socket not created" >&2
echo "nestri-entry: check waylanddisplaysrc plugin and GPU access" >&2
exit 1
fi
echo "nestri-entry: wayland compositor ready at $XDG_RUNTIME_DIR/wayland-1" >&2
# X_DISPLAY_NUM="12"
# X_SOCKET="/tmp/.X11-unix/X$X_DISPLAY_NUM"
# # Launch the XWayland satellite process to handle X11 clients in the VM.
# xwayland-satellite ":$X_DISPLAY_NUM" >> /tmp/x11.log 2>&1 &
# PIDS+=($!)
# # Wait for X11 socket
# for i in $(seq 1 100); do
# [ -S "$X_SOCKET" ] && break
# sleep 0.1
# done
# if [ ! -S "$X_SOCKET" ]; then
# echo "nestri-entry: FATAL — X11 socket $X_SOCKET not created" >&2
# echo "nestri-entry: check xwayland-satellite logs for startup errors" >&2
# exit 1
# fi
# echo "nestri-entry: X11 server ready at $X_SOCKET" >&2
# export DISPLAY=":$X_DISPLAY_NUM"
# ============================================================
# DNS Setup
#
# passt (muvm's network backend) reads /etc/resolv.conf from
# the bwrap namespace to get DNS servers for the VM's DHCP.
# The rootfs may have a stale or symlinked resolv.conf.
# We overwrite it here with the real DNS from the host.
#
# nestri-init also tries to copy /run/nestri/resolv.conf into
# the VM's /etc/resolv.conf, but that often fails because the
# VM filesystem may be read-only. Fixing it here in the bwrap
# layer ensures passt gets the right servers.
# ============================================================
if [ -f /run/nestri/resolv.conf ]; then
# Remove symlink if present, then write directly
rm -f /etc/resolv.conf 2>/dev/null || true
cp /run/nestri/resolv.conf /etc/resolv.conf 2>/dev/null || {
# If /etc is somehow read-only, try writing in-place
cat /run/nestri/resolv.conf > /etc/resolv.conf 2>/dev/null || true
}
echo "nestri-entry: DNS configured:" >&2
cat /etc/resolv.conf >&2
fi
# ============================================================
# 3. muvm — MicroVM Launch
#
# The VM boots from this same filesystem via virtiofs.
# nestri-init wraps systemd, setting up machine-id, dns, etc.
#
# Guest systemd services (started automatically):
# nestri-network.service — muvm-configure-network (networking)
# nestri-wayland-proxy.* — wl-cross-domain-proxy (socket-activated)
# nestri-pwbridge.* — muvm-pwbridge (socket-activated audio)
# nestri-session-bus.* — dbus-daemon (socket-activated)
# nestri-remote.service — muvm-remote (runs user command)
#
# Cross-domain Wayland proxy:
# The VM guest runs wl-cross-domain-proxy which connects to
# the host's wayland-1 socket via virtio-gpu cross-domain context.
# This allows the guest gamescope to render to the host compositor.
#
# Environment passed to guest:
# WAYLAND_DISPLAY=wayland-1 — compositor socket name
# XDG_RUNTIME_DIR=/run/vm-user — guest runtime directory
# XDG_SESSION_TYPE=wayland — session type hint
# SDL_VIDEODRIVER=wayland — force SDL wayland backend
# SDL_AUDIO_DRIVER=pipewire — force SDL pipewire audio
# NESTRI_* — stream configuration
# ============================================================
echo "nestri-entry: starting muvm..." >&2
# -e "SDL_VIDEODRIVER=wayland" \
# -e "ELECTRON_OZONE_PLATFORM_HINT=wayland" \
# -e "_JAVA_AWT_WM_NONREPARENTING=1" \
# -e "QT_QPA_PLATFORM=wayland" \
muvm \
--custom-init-cmdline "nestri-init /sbin/init --echo-target=console" \
-e "container=muvm" \
-e "XDG_SESSION_TYPE=wayland" \
-e "XDG_RUNTIME_DIR=/run/vm-user" \
-e "SDL_AUDIO_DRIVER=pipewire" \
-e "WAYLAND_DISPLAY=wayland-1" \
-e "VK_DRIVER_FILES=$VK_DRIVER_FILES" \
-e "NESTRI_WIDTH=$NESTRI_WIDTH" \
-e "NESTRI_HEIGHT=$NESTRI_HEIGHT" \
-e "NESTRI_FPS=$NESTRI_FPS" \
-e "MICROVM_UID=$MICROVM_UID" \
-e "MICROVM_GID=${MICROVM_GID:-$MICROVM_UID}" \
-e "BOOT_TIME_OFFSET=${BOOT_TIME_OFFSET:-}" \
-i -t \
"$@"
MUVM_EXIT=$?
echo "nestri-entry: muvm exited with code $MUVM_EXIT" >&2
exit $MUVM_EXIT
# /usr/bin/steam -gamepadui -cef-force-gpu &
# STEAM_PID=$!
# PIDS+=($STEAM_PID)
# wait $STEAM_PID