Commit Graph
20 Commits
Author SHA1 Message Date
jensandClaude Sonnet 5 588086e82c Audio on by default, per-game volume, and fix real navigation + latency bugs
- Removed the "Enable Sound" button: AudioContext/worklet/WebSocket now
  connect eagerly on page load; only ctx.resume() still needs a user
  gesture, piggybacking on the page's first click/keypress (e.g.
  clicking Start) instead of a dedicated audio-only button.

- Added a per-game volume slider (POST /volume). Despite audio being
  one shared PulseAudio mix, this is a real independent control: every
  game is still its own distinct sink-input, found by matching
  `pactl -f json list sink-inputs`'s application.process.id against
  the game's own PID, then `pactl set-sink-input-volume`.

- Real bug: audio still never played after removing the button, because
  Install/Start/Stop/Uninstall were still <form method="post"> submits.
  Every click caused a full page navigation (303 redirect), tearing
  down whatever AudioContext had just connected; the fresh page after
  reload creates a new suspended context with no further gesture to
  unlock it. Tell: no speaker icon ever appeared on the Chrome tab.
  Fixed by removing <form>s entirely - every button is now
  onclick="doAction(...)", do_POST returns a plain 204, and client-side
  doAction()/refresh() fetch() the action and the updated page, then
  swap only #content's innerHTML. The page itself never navigates, so
  the audio connection survives every action. setInterval(refresh,
  3000) replaces the old <meta refresh> for keeping status current
  without that risk.

- Real bug: once audio worked, ~2s of latency that got worse over time
  plus multi-second delay before volume changes were audible - fixed
  upstream in docker-common's pcm-worklet.js (uncapped playback queue),
  propagated here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 19:15:51 +02:00
jensandClaude Sonnet 5 eb63f46978 Drop the iframe wrapper page and fix two real certificate bugs
Video and audio no longer share one iframe-based /screen/<name> page.
Firefox refuses to load iframe content signed by a certificate whose
warning hasn't been accepted at the top level, with no way to click
through inside the iframe (Chrome is more lenient, which is why this
briefly looked fine there). "Open Screen" is now a plain top-level link
straight to noVNC's own URL (new tab, normal Accept-the-Risk applies),
and the "Enable Sound" toggle moved to the main setup page - meant to
be left open in its own tab while playing. Because that page now hosts
a persistent AudioContext/WebSocket, its auto-refresh no longer fires
just because a game is running (only during active installs), since a
full-page refresh would have killed that connection every 3s.

Two real certificate bugs found and fixed along the way:
- CN=NY (a meaningless placeholder) -> CN=localhost + SAN
  (DNS:localhost, IP:127.0.0.1). Firefox validates the WebSocket-Secure
  connection's cert against the hostname independently of the page
  load and rejected the CN mismatch there even after the page-level
  warning was accepted.
- Worse: adding -addext without also pinning basicConstraints=CA:FALSE
  left the cert defaulting to CA:TRUE - i.e. flagged as a Certificate
  Authority, not a server cert. Firefox hard-refuses that with no
  override option at all (not a normal clickable warning), which is
  why "the warning appears but won't let me proceed" even after the CN
  fix. Fixed by explicitly setting basicConstraints=critical,CA:FALSE
  plus keyUsage/extendedKeyUsage=serverAuth. Confirmed fixed: Firefox
  now shows the same clickable self-signed warning Chrome always did,
  and the noVNC connection completes successfully after accepting it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 18:21:35 +02:00
jensandClaude Sonnet 5 301503a276 Get game audio into the browser via PulseAudio + a WebSocket bridge
VNC/noVNC only ever streams video, so game sound needed a completely
separate path. Adds pulseaudio/pulseaudio-utils/libasound2-plugins and
routes ALSA's default device through Pulse (/etc/asound.conf), so
dosbox/scummvm need zero special config. server.sh starts one
PulseAudio daemon and one pcm_ws_bridge.py (from docker-common) for the
container's whole lifetime, capturing Pulse's single default sink via
parec.

Audio is a single shared mix, not per-game - considered and dropped a
per-slot-isolated design (mirroring the video architecture) as
unnecessary complexity per direction. Every running game's audio just
mixes into the one default sink; every /screen/<name> page connects to
the same AUDIO_PORT.

setup_server.py gains GET /screen/<name> (an iframe onto the game's
noVNC screen plus an Enable Sound button - browsers require a user
gesture before audio can start) and GET /pcm-worklet.js. The "Open
Screen" link simplifies from a client-side-JS-built cross-port link to
a plain same-origin relative link, since /screen/<name> now reads the
real host server-side from the request's own Host header.

Found and fixed a real bug along the way: parec --device=@DEFAULT_SINK@.monitor
looks correct but fails with "Stream error: Invalid argument" - the
actual PulseAudio macro is the single token @DEFAULT_MONITOR@.

Verified end-to-end with real audio, not just plumbing: confirmed via
`pactl list sink-inputs` that dosbox connects to Pulse correctly
(unmuted, uncorked), then used xdotool to advance stuntcar past its
silent title screen and captured real audible game audio (RMS ~9292)
through the WebSocket bridge with a raw Python client.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 16:27:10 +02:00
jensandClaude Sonnet 5 c1942f5ec4 Move the Open Screen link into its own column after Actions
Was embedded in the Status cell next to the Running badge; now a
dedicated column to the right of Actions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 15:29:01 +02:00
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
jensandClaude Sonnet 5 60b8f63ad0 Add release date/publisher/size info and a noVNC link to the setup page
Manifests gain release_date/publisher fields (static historical facts).
Install size deliberately isn't a manifest field - get_zip_sizes() looks
it up live via a batched smbclient ls (one call per game, single
connection, split on each command's trailer line) so it can't go stale
if a zip is replaced.

Add an "Open noVNC Screen" link at the top of the page. Its href is set
by a few lines of client-side JS reading window.location.hostname at
render time, since the noVNC port (NOVNC_PORT, now passed into
setup_server.py by server.sh alongside SETUP_PORT) differs from the
setup port and the container may be reached via different
hostnames/IPs - a fixed server-rendered URL would be wrong.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 15:09:19 +02:00
jensandClaude Sonnet 5 afb18f1b38 Add ScummVM support and generalize manifest zip lookup
Add scummvm to the Dockerfile. Its binary lives at /usr/games/scummvm
(Debian's convention for game packages), not reliably on $PATH under a
non-login shell, so manifests reference it by absolute path.

Generalize the manifest schema from a bare "zip" filename (implicitly
under Games\dosbox\ on the SMB share) to "zip_path" (a full path
relative to the share root), since the SCUMM games' zips live in their
own folders (Games\Monkey Island\, Games\Indiana Jones\, etc.) rather
than being colocated with their manifest like stuntcar/t7g were.
Manifests themselves still all live in the Games\dosbox\ catalog
directory regardless of where the actual zip sits.

Added and verified (install/start/stop/uninstall via monkey2) manifests
for: monkey, monkey2, atlantis, indy3, tentacle. Skipped the German CD
release of Day of the Tentacle (loose files at the zip root, no single
top-level folder - incompatible with the current extraction convention)
and Curse of Monkey Island (untested, much larger).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 14:29:11 +02:00
jensandClaude Sonnet 5 17426584cd Sync scripts/signals.sh with docker-common's canonical version
Adopts bash-arithmetic exit-code computation ($((128 + SIGNUM))) instead
of piping to bc, dropping that dependency. Consolidates this project
onto docker-common's shared scripts (see that repo's CLAUDE.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 11:14:36 +02:00
jensandClaude Sonnet 5 dcdf4fa9b3 Update TODO reference from common/ to the new docker-common repo
common/scripts/ was replaced by the docker-common repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 11:00:51 +02:00
jensandClaude Sonnet 5 2c70def189 Note audio streaming should be shared across noVNC-family projects
Follow the common/scripts/signals.sh precedent: build the browser
audio + noVNC piece project-agnostically in common/, then copy it into
each project's own scripts/ rather than making it dosbox-specific.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:49:02 +02:00
jensandClaude Sonnet 5 568c284a8a Add TODO items for SCUMM game manifests and low-latency game audio
Audio note: rules out the obvious compressed-stream + <audio> tag
approach up front, since codec/element buffering can't realistically hit
the tens-of-ms target - raw PCM over WebSocket into an AudioWorkletNode
is the concrete plan instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:43:38 +02:00
jensandClaude Sonnet 5 53f36cca8e Publish the noVNC game-screen port in run.sh
8099 (80${DISPLAY_NUM}) was never published to the host, only the 7099
setup UI was - the game screen was unreachable outside the container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:32:15 +02:00
jensandClaude Sonnet 5 827f03f087 Drive games from per-game JSON manifests instead of raw zip scanning
Games are no longer discovered from *.zip files on the SMB share; each
game now has its own <name>.json manifest (name/title/zip/start_cmd)
living next to its zip, discovered dynamically via smbclient. Nothing
about a game's identity or how to install/start it is hardcoded in the
image anymore. The setup page also grew Start/Stop/Uninstall buttons,
backed by real process tracking (Popen + terminate/kill).

Piloted on stuntcar only (manifest already uploaded to the share); t7g
has no manifest yet so it won't appear until one's added. The old
game.sh/start_game.sh docker-exec launch path is left as-is for now and
overlaps with the new Start button - noted in TODO.md for later cleanup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 10:27:33 +02:00
jensandClaude Sonnet 5 973a12ae65 Add CLAUDE.md documenting image layering, ports, and the setup UI
Mirrors docker-xserver-novnc/CLAUDE.md's structure, adapted for this
project's specifics: no build-time game embedding, the runtime volume,
the setup_server.py install flow, and the game.sh/start_game.sh play path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 08:46:16 +02:00
jensandClaude Sonnet 5 c6ae36ba67 Replace auto-download with an interactive HTML game setup UI
No games are fetched at container start anymore. scripts/setup_server.py
is a small stdlib-only Python HTTP server, started alongside Xvnc/fluxbox/
websockify, serving an HTML page on ${SETUP_PORT} (70${DISPLAY_NUM},
published as 7099 by run.sh) that lists every *.zip found live on the SMB
share with an Installed/Install status per game. Clicking Install
downloads and extracts just that game into ${GAMES_HOME} on demand.
Replaces the old fetch_games.sh, which unconditionally pulled every game
on first start.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 08:37:12 +02:00
jensandClaude Sonnet 5 44bbd00336 Fetch games into a volume at runtime instead of baking them into the image
927MB of the image was just t7g's two CD ISOs, which can't be meaningfully
compressed (already-compressed FMV/audio). Move the smbget/unzip step out
of the Dockerfile and into scripts/fetch_games.sh, run from server.sh on
container start: it populates GAMES_HOME on first run and skips the fetch
if games are already present. run.sh mounts a named volume so the games
survive --rm restarts instead of being re-downloaded every time. Image
drops from 1.6GB to ~690MB; first container start now needs network access
to the vlda-01 SMB share to seed the volume.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 08:25:26 +02:00
jensandClaude Sonnet 5 79cc239dca Shrink image by merging game fetch/extract into one layer
The downloaded zips and their extracted contents lived in separate RUN
layers, so the zips never actually left the image despite a later
rm -rf /tmp/apps layer. unzip/smbclient were also left installed after
only being needed at build time. Merging fetch, extract, cleanup, and
package purge into a single RUN drops the image from 2.31GB to 1.6GB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiNnj78HGx1KWyCCo39HSz
2026-07-28 08:18:18 +02:00
jensandClaude Sonnet 5 c4049a7d79 Add TODO.md
Tracks reducing the image size of jayfield/dosbox-novnc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QuKZRefXR3hVaUiXgeu6i
2026-07-27 23:37:26 +02:00
jensandClaude Sonnet 5 ecd160d542 Repoint to ich777/novnc-baseimage
jayfield/novnc-baseimage (built by docker-novnc-baseimage) has been
retired, so this now builds on the public ich777/novnc-baseimage
instead. That image puts Xvnc/websockify on $PATH and noVNC's web
client at /usr/share/novnc/ rather than /opt/TurboVNC and /tmp, so
server.sh is updated to match. Verified with a full rebuild and
container start: Xvnc, fluxbox, and websockify all come up and the
noVNC web UI responds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018QuKZRefXR3hVaUiXgeu6i
2026-07-27 23:34:15 +02:00
jens 2a3eb9218e Initial commit 2024-05-31 19:13:10 +02:00