nextdesk

Tuning — Audio, Storage, Video, Performance

Most desktop apps (app_type=kasm) now run on Selkies, not KasmVNC — see architecture.md for how audio/volume actually works there (native, no separate stream). The Audio, Video and Clipboard sections below describe the legacy lwp-kasm-base path (currently only the vpn app), kept for reference until it’s fully retired.

Audio (legacy KasmVNC only)

How it works

Each KasmVNC container runs a PulseAudio daemon with a null sink (virtual output). KasmVNC 1.4’s web client cannot play session audio standalone (it delegates to a Kasm Workspaces parent frame), so LWP streams audio itself over an independent channel:

PulseAudio sink monitor
  → ffmpeg  (supervisord `lwp-audio`)  →  Opus/Ogg over HTTP on container :8081
    → backend  GET /api/sessions/{id}/audio  (relay)
      → browser hidden <audio> element  (per-window mute + volume slider)

Startup sequence (lwp-start-kasm.sh):

  1. PulseAudio starts with --daemon, loads module-native-protocol-unix at the socket path
  2. Script polls for /run/user/1000/pulse/native
  3. Only after the socket exists does vncserver :1 start; lwp-audio waits for the same socket

PulseAudio config (/home/lwp/.config/pulse/default.pa):

.fail
load-module module-null-sink sink_name=default_sink
set-default-sink default_sink
load-module module-null-source source_name=default_source
set-default-source default_source
load-module module-native-protocol-unix socket=/run/user/1000/pulse/native

The .fail directive means PulseAudio exits if any module fails to load, which causes supervisord to restart the whole stack cleanly rather than running silently without audio.

Troubleshooting audio

No sound in browser:

Audio latency: The independent Opus/Ogg HTTP stream buffers ~1–2 s — fine for notifications, music, and video sound, but not lip-synced or real-time voice.


Storage — Nextcloud WebDAV (rclone)

How it works

When a user has Nextcloud configured, lwp-rclone (supervisord service) mounts their Nextcloud at $LWP_NC_MOUNT (default /home/lwp/Files) using rclone FUSE.

Mount command (containers/kasm-base/lwp-nc-mount.sh):

rclone mount \
  --vfs-cache-mode full \
  --vfs-cache-max-size 1G \
  --vfs-cache-max-age 24h \
  --dir-cache-time 10m \
  --exclude ".cache/**" \
  --no-modtime \
  nc: /home/lwp/Files

VFS cache mode: full

--vfs-cache-mode full means:

This is the right mode for any use case that involves small-file access patterns or databases. The previous writes mode caused slow first-reads for uncached files.

Cache limits

Flag Value Effect
--vfs-cache-max-size 1G Evict oldest cached files when cache exceeds 1 GB
--vfs-cache-max-age 24h Evict cache entries not accessed for 24 h
--dir-cache-time 10m Re-read directory listings every 10 min

What to store on Nextcloud vs. home volume

Data Where Reason
Documents, downloads Nextcloud (/home/lwp/Files) Persists across containers, accessible from other devices
Browser profile (~/.mozilla, ~/.config/chromium) Home volume (/home/lwp) Frequent SQLite writes; local disk speed essential
Browser cache (~/.cache) Excluded from rclone Thousands of tiny files; catastrophic on WebDAV

The .cache/** exclude prevents rclone from attempting to sync the browser HTTP cache to Nextcloud.

Standard XDG directories on Nextcloud

When the Nextcloud mount is live, lwp-nc-mount.sh automatically:

  1. Creates the standard folders on Nextcloud if they don’t exist: Documents / Downloads / Music / Pictures / Templates / Videos

  2. Replaces ~/Documents, ~/Downloads, etc. inside the container with symlinks into the Nextcloud mount (~/Files/Documents, etc.). Any files that already existed locally are migrated before the symlink is created.

Result: apps save to standard paths (e.g. ~/Downloads/report.pdf) and those paths transparently resolve to Nextcloud. File managers (Thunar, Nautilus) and the XDG sidebar show the correct folders. xdg-user-dirs-update (run at session start) sees the symlinks and doesn’t recreate local copies.

If Nextcloud is not configured or the mount fails, the local dirs remain as real directories — no breakage.

Configure Nextcloud for a user

Admin panel → Users → Edit → Nextcloud tab → enter:

The credentials are decrypted at session launch and passed as env vars to the container. They are never logged and are not returned by any API endpoint after creation.


Persistent home directory

If mount_home is enabled on an app, LWP creates a Docker named volume and mounts it into the container:

This volume:

For multi-node K8s deployments, use a ReadWriteMany StorageClass (NFS, CephFS, Longhorn in RWX mode) so any node can attach the user’s home.


Video & Display (legacy KasmVNC only — Selkies apps use pixelflux, see architecture.md)

YouTube / video playback

KasmVNC re-encodes the X11 framebuffer as a video stream. High-motion video is CPU-intensive on the container node.

Black screen on video:

Choppy playback:

GPU acceleration

Requires a K8s node with GPU + device plugin installed.

NVIDIA:

  1. Install NVIDIA device plugin
  2. Add to app extra env vars: DRINODE=/dev/dri/renderD128
  3. Add resource limit to app definition: nvidia.com/gpu: 1

Intel integrated GPU (no extra plugin):

DRINODE=/dev/dri/renderD128

Available on most bare-metal nodes. Verify: ls /dev/dri/ inside the desktop.

KasmVNC configuration

KasmVNC reads /etc/xdg/kasmvnc/kasmvnc.yaml inside the container:

desktop:
  resolution:
    width: 1920
    height: 1080
  allow_resize: true        # browser auto-resize
network:
  protocol: http            # WebSocket sub-protocol (not HTTP mode)
  interface: "0.0.0.0"
  websocket_port: 8080
  use_ipv4: true
  use_ipv6: false
command_line:
  prompt: false

Note: Do not add an audio: key — KasmVNC 1.4 does not recognise it and will fail to start. Audio is auto-detected via the PulseAudio socket.


Clipboard (legacy KasmVNC only — Selkies has its own native clipboard integration)

Bidirectional clipboard works in Chrome and Edge (secure context / HTTPS required). Firefox may prompt for clipboard permission on first paste.

Clipboard bridge (in-app): every window has a clipboard button (📋) in the titlebar. Use this to manually copy text from the host and paste into the session, or vice versa — useful in Firefox or non-HTTPS dev setups.

KasmVNC ships with its own isInsideKasmVDI() check (window.self !== window.top) that assumes it’s embedded in the real Kasm Workspaces platform whenever it’s loaded inside an iframe — true for every LWP session, since we always embed the client that way. When it fires, KasmVNC disables its own clipboard entirely (clipboard_up/clipboard_down off), assuming the host platform will bridge it instead — which for us is exactly what our own bridge does, so vncQueryString() (frontend/src/lib/vncDisplay.ts) forces clipboard_up=true&clipboard_down=true&clipboard_seamless=false on every session URL to override it. clipboard_seamless=false additionally keeps KasmVNC on the legacy textarea (#noVNC_clipboard_text) + RFB ServerCutText path rather than its native-Clipboard-API “seamless” mode (Chrome/Edge default) — seamless mode writes straight to the OS clipboard from an async websocket callback, which needs user activation Chrome won’t grant there, and it never touches the textarea our bridge reads.

If clipboard API doesn’t work:


Session performance

Idle suspend

Sessions automatically pause after 15 minutes of browser inactivity (no mouse/keyboard events). The container is paused (docker pause in dev, K8s scale-to-zero in prod) and the window shows a suspended overlay. The first mouse move resumes all suspended sessions.

To adjust the timeout, edit IDLE_MS in frontend/src/pages/Desktop.tsx.

Session watchdog

The ARQ background worker runs expire_sessions every 5 minutes. Sessions older than SESSION_TIMEOUT_HOURS (default: 8 h) are stopped automatically regardless of activity.

Override in .env:

SESSION_TIMEOUT_HOURS=12

Max concurrent sessions

Default: 2 sessions per user. Override in .env:

MAX_SESSIONS_PER_USER=5

Or set per-user limits in Admin → Users.