Files
docker-dosbox-novnc/TODO.md
T
jensandClaude Sonnet 5 3332553cb5 Give each running game its own screen, capped at 10 concurrent
Previously every game shared one always-on Xvnc/fluxbox/websockify
desktop, so starting two games at once meant they fought over focus on
the same screen and the same ALSA device. Now:

- server.sh no longer starts a shared desktop at all - it just runs
  setup_server.py. There's no default display anymore.
- start_game() allocates a free display from GAME_DISPLAY_NUMS
  (:90-:99, one per MAX_CONCURRENT_GAMES=10 slot), spins up a fresh
  Xvnc+fluxbox+websockify for it, and launches the game with DISPLAY set
  to that display. All four processes are tracked together per game.
- stop_game() tears down all four; is_running() does the same lazily if
  the game exited on its own (crash/quit), so a slot doesn't stay stuck
  just because nobody clicked Stop.
- Starting past the 10-slot cap is refused with an error shown on that
  game's row instead of silently failing.
- Each running game's row gets its own "Open Screen" link (client-side
  JS, since the port is only known once the game is actually started)
  instead of one global noVNC link.
- run.sh publishes the whole 8090-8099 noVNC port range up front, since
  Docker can't add port mappings to an already-running container.

Verified end-to-end: two different games running concurrently get fully
independent Xvnc/fluxbox/websockify/game process sets and noVNC
endpoints; stopping one leaves the other untouched; the capacity guard
correctly refuses a start at the limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 15:25:00 +02:00

6.8 KiB

TODO

  • ~Reduce image size of jayfield/dosbox-novnc (currently 2.3GB). Done in two steps:

    1. Merged the game download/extract steps and the unzip/smbclient install into a single layer (so the downloaded zips and the packages only needed to extract them no longer persist in the final image). 2.31GB -> 1.6GB.
    2. Stopped baking the games into the image at all (927MB of that was just t7g's two CD ISOs). run.sh mounts a named volume (dosbox-games) so installed games persist across --rm restarts. 1.6GB -> ~690MB. Container now needs network access to vlda-01 at runtime (not just build time) to install games.
    3. Dropped the auto-download at container startup entirely. scripts/setup_server.py is a small stdlib-only HTTP server that serves an HTML page on ${SETUP_PORT} (70${DISPLAY_NUM}, published as 7099 by run.sh) listing every *.zip on the SMB share with an Installed/Install status per game; the user clicks "Install" to fetch+extract a specific game into ${GAMES_HOME} on demand.
  • Manifest-driven games + Start/Stop/Uninstall: games are no longer discovered from raw *.zip files on the share. setup_server.py lists *.json manifests instead (one per game, in the Games\dosbox\ catalog directory), each declaring name/title/zip_path/start_cmd. The web page grew Start/Stop/Uninstall buttons alongside Install, driven entirely by that manifest — nothing about a game's identity or how to run it is hardcoded in the image anymore. Started as a stuntcar-only pilot, now extended to 6 games (see the SCUMM item below). t7g still has no manifest and won't show up in the setup page until one is added (t7g.json with a start_cmd for its dosbox-0.74-3.conf). game.sh/scripts/start_game.sh (the old docker exec-based launch path, with its hardcoded DOSBOX_VERSION/run.bat convention) were intentionally left untouched during the pilot and still overlap with the new Start button — worth reconciling (or removing) once every remaining game has a manifest.

  • Create manifests for SCUMM engine games Done: scummvm added to the Dockerfile (/usr/games/scummvm, not on $PATH by default under a non-login shell — manifests use the absolute path). Manifests live in the usual Games\dosbox\*.json catalog directory on the share, but their zip_path now points at wherever the game's zip actually already lived (Games\Monkey Island\..., Games\Indiana Jones\..., etc.) — this required generalizing the manifest schema from a bare zip filename (implicitly under Games\dosbox\) to a full zip_path relative to the share root, since these games weren't colocated with dosbox's. Added and verified end-to-end (install/start/stop/uninstall) via monkey2:

    • monkey — The Secret of Monkey Island
    • monkey2 — Monkey Island 2: LeChuck's Revenge
    • atlantis — Indiana Jones and the Fate of Atlantis
    • indy3 — Indiana Jones and the Last Crusade
    • tentacle — Day of the Tentacle

    Skipped the German CD release of Day of the Tentacle on the share (Day Of The Tentacle (CD DOS, German).zip) — unlike every other zip here, it doesn't wrap its contents in a single top-level folder (loose files at the zip root plus a stray MANIAC/ dir for the embedded Maniac Mansion easter egg), so it doesn't fit the current "zip's top-level folder == install name" extraction convention. Also skipped Curse of Monkey Island (much larger, untested) — could be added the same way if wanted.

    ScummVM's own game-target IDs (monkey, monkey2, atlantis, indy3, tentacle) happened to exactly match each zip's existing top-level folder name, so no change was needed to the install/extraction logic itself, only to where zips are looked up from.

  • Handle multiple games running at once Done: each running game now gets its own ephemeral X session (Xvnc+fluxbox+websockify, its own display :90-:99 and noVNC port 8090-8099) spun up by start_game() and torn down by stop_game() (or lazily on the next page load, if the game exited/crashed on its own) — instead of every game sharing one screen and fighting over focus/audio. Capped at MAX_CONCURRENT_GAMES = 10; starting an 11th game while all 10 slots are in use is refused with an error shown on its row rather than silently doing nothing. There is no more a single shared "default" desktop or global noVNC link — server.sh no longer starts Xvnc/fluxbox/websockify at container startup at all, only setup_server.py itself; each game's own "Open Screen" link appears on its row only while it's running, built with a little client-side JS (the noVNC port differs from the setup port and can't be known server-side without knowing which hostname the browser used to reach the container). run.sh publishes the whole 8090-8099 range up front, since Docker can't add port mappings to an already-running container. Verified end-to-end with two different games running concurrently (separate processes, separate displays, separate noVNC ports, independent stop/teardown) and the 10-slot cap logic.

  • Get game sound actually audible during play. --device /dev/snd is passed through and alsa-utils is installed, so DOSBox can write to an ALSA device inside the container, but VNC/noVNC only ever streams video, not audio — nothing currently carries that sound out to the browser.

    Target latency is a few tens of ms, not seconds — that rules out the "obvious" approach of a PulseAudio null sink piped through ffmpeg into a compressed (mp3/ogg) HTTP/Icecast-style stream consumed by an <audio> tag: codec frame buffering plus the <audio> element's own jitter buffer realistically puts that in the 1-3s range, no matter how it's tuned.

    Concrete approach instead: PulseAudio null sink → capture raw/lightly-buffered PCM (small frames, e.g. parec with a short --latency) → push those frames to the browser over a plain WebSocket (new endpoint alongside setup_server.py, or a small dedicated process) → browser side, feed them straight into the Web Audio API via an AudioWorkletNode (not <audio>, not ScriptProcessorNode — that's deprecated and has worse latency) for near-real-time scheduled playback. No container/codec framing in the path at all.

    Build this to be reusable across the other noVNC-family projects (docker-xserver-novnc, docker-sdr-novnc), not dosbox-specific — it's a "browser audio + noVNC" concern, nothing about it is really about DOSBox. Follow the docker-common/scripts/signals.sh precedent (see the docker-common repo): keep the generic PulseAudio-sink/WebSocket/AudioWorklet piece project-agnostic there, then copy it (not symlink) into each project's own scripts/ the same way signal handling is shared, so a fix in one place gets propagated by hand to the others.