diff --git a/README.md b/README.md index 2b07a96..7798db4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ overlay /mnt/merged-cstrike/models overlay ro,lowerdir=/home/gameservers/css_ overlay /mnt/merged-cstrike/sound overlay ro,lowerdir=/home/gameservers/css_ze/cstrike/sound:/home/gameservers/css_mg/cstrike/sound:/home/gameservers/css_zr/cstrike/sound,nofail 0 0 ## third run -docker run -d --name css-test -e SIG_URL=ws://172.17.0.1:3000 -e CONTAINER_NAME=css-test -e CF_APP_ID= -e CF_APP_TOKEN= -e CAP_FPS=60 -e CAP_W=800 -e CAP_H=600 --cpus="8.0" \ +docker run -d --name css-test -e SIG_URL=ws://172.17.0.1:3000 -e CONTAINER_NAME=css-test -e CF_APP_ID= -e CF_APP_TOKEN= -e CAP_FPS=60 -e CAP_W=800 -e CAP_H=600 --cpus="10.0" \ -v /mnt/merged-cstrike/maps:/home/ubuntu/Counter-Strike\ Source/cstrike/maps:ro \ -v /mnt/merged-cstrike/materials:/home/ubuntu/Counter-Strike\ Source/cstrike/materials:ro \ -v /mnt/merged-cstrike/models:/home/ubuntu/Counter-Strike\ Source/cstrike/models:ro \ @@ -64,3 +64,5 @@ css-client ## CSS DirectX9 → built-in DXVK → Vulkan → Wine Wayland driver → Weston (lavapipe/llvmpipe) → Xwayland → X11 → ximagesrc capture for gst_agent ## using DXVK cache and MESA_SHADER_CACHE which are mounted volumes + +## the inbuilt DXVK version 2.3 is replaced with DXVK 3.0 downloaded from https://github.com/doitsujin/dxvk/releases/download/v3.0/dxvk-3.0.tar.gz diff --git a/streaming-agent/gst_agent.py b/streaming-agent/gst_agent.py index f386665..1075328 100755 --- a/streaming-agent/gst_agent.py +++ b/streaming-agent/gst_agent.py @@ -79,6 +79,20 @@ def xdotool(*args): stderr=subprocess.DEVNULL, ) +# Track held keys so we can release them all on controller change +_held_keys = set() + +def release_all_keys(): + # Release all currently held keys and reset X11 modifier state + for key in list(_held_keys): + try: + xdotool('keyup', key) + except Exception: + pass + _held_keys.clear() + # Reset X11 modifier state (clears stuck shift/ctrl/alt etc.) + xdotool('key', '--clearmodifiers', 'Escape') + def handle_input(event): t = event.get('type') if t == 'mouse_move': @@ -91,12 +105,18 @@ def handle_input(event): elif t == 'mouse_up': xdotool('mouseup', str(event.get('button', 1))) elif t == 'key_down': - xdotool('keydown', event.get('key', '')) + key = event.get('key', '') + _held_keys.add(key) + xdotool('keydown', key) elif t == 'key_up': - xdotool('keyup', event.get('key', '')) + key = event.get('key', '') + _held_keys.discard(key) + xdotool('keyup', key) elif t == 'mouse_wheel': button = '4' if event.get('direction') == 'up' else '5' xdotool('click', button) + elif t == 'release_keys': + release_all_keys() # ── GStreamer Pipeline ──────────────────────────────────────────────────────── class CSSStreamAgent: @@ -125,18 +145,10 @@ class CSSStreamAgent: f'webrtcbin. ' # Game audio: pipewiresrc → Opus - # pipewiresrc with no target follows PipeWire's default *capture* - # node (the microphone, virtual_mic) — not the default sink's - # monitor — so without explicit targeting it silently captured - # the mic into this track instead of game audio. - # game_output is a single PipeWire node with both playback ports - # (what CSS writes to) and monitor ports (the loopback read side) - # on the same node — there's no separate "game_output.monitor" - # node. stream.capture.sink=true tells PipeWire's autoconnect to - # link to that node's monitor/capture-sink ports rather than - # treating it as a regular source. - f'pipewiresrc target-object=game_output ' - f'stream-properties="props,stream.capture.sink=(boolean)true" ! ' + # pipewiresrc is the native PipeWire GStreamer source (Ubuntu 24+/26). + # It connects to the default audio output monitor via PipeWire directly, + # avoiding the PulseAudio compatibility layer entirely. + f'pipewiresrc ! ' f'audio/x-raw,format=F32LE,rate=48000,channels=2 ! ' f'audioconvert ! ' f'opusenc ! ' diff --git a/streaming-agent/signaling-server.js b/streaming-agent/signaling-server.js index 1f28ec1..7b4b26c 100644 --- a/streaming-agent/signaling-server.js +++ b/streaming-agent/signaling-server.js @@ -182,6 +182,15 @@ function releaseControl(entry, reason) { entry.ws.send(JSON.stringify({ type: 'control_released', reason })); } + // Tell gst_agent to release all held keys and reset X11 modifier state. + // Without this, stuck keys (shift/ctrl/w/etc.) persist across controller + // handoffs and eventually corrupt xdotool's input state, requiring a + // full container restart to recover. + const agent = [...agentSessions.values()][0]; + if (agent && agent.agentWs && agent.agentWs.readyState === WebSocket.OPEN) { + agent.agentWs.send(JSON.stringify({ type: 'release_keys' })); + } + if (controlQueue.length > 0) { const next = controlQueue.shift(); applyController(next);