#!/bin/bash set -euo pipefail # ── Clean up stale sockets from previous run ────────────────────────────────── rm -f /tmp/.X99-lock rm -f /tmp/.X11-unix/X99 rm -rf /tmp/weston-runtime # ── XDG runtime dir (required by Weston and PipeWire) ──────────────────────── export XDG_RUNTIME_DIR=/tmp/weston-runtime mkdir -p "$XDG_RUNTIME_DIR" chmod 700 "$XDG_RUNTIME_DIR" # ── D-Bus session bus (required by PipeWire / wireplumber) ─────────────────── eval $(dbus-launch --sh-syntax) export DBUS_SESSION_BUS_ADDRESS # ── Wine virtual desktop matching Weston size ──────────────────────────────── wine reg add "HKCU\\Software\\Wine\\Explorer" /v "Desktop" /t REG_SZ /d "Default" /f wine reg add "HKCU\\Software\\Wine\\Explorer\\Desktops" /v "Default" /t REG_SZ /d "800x600" /f wine reg add "HKCU\\Software\\Valve\\Source\\cstrike\\Settings" /v ScreenWidth /t REG_DWORD /d 800 /f wine reg add "HKCU\\Software\\Valve\\Source\\cstrike\\Settings" /v ScreenHeight /t REG_DWORD /d 600 /f wine reg add "HKCU\\Software\\Wine\\WineDbg" /v ShowCrashDialog /t REG_DWORD /d 0 /f export DXVK_CUSTOM_DISPLAY_MODES="800x600@60" # ── Start Weston (headless Wayland compositor) ──────────────────────────────── weston --backend=headless-backend.so --width=800 --height=600 --no-config & sleep 2 export WAYLAND_DISPLAY=$(ls "$XDG_RUNTIME_DIR/" | grep -o 'wayland-[0-9]*' | head -1) echo "Using WAYLAND_DISPLAY=$WAYLAND_DISPLAY" # ── Start Xwayland (X11 compatibility layer over Wayland) ──────────────────── # -geometry fixes the virtual framebuffer at exactly 800x600 from the start, # matching Weston's headless output — no xrandr mode-adding needed. Xwayland :99 -glamor off -shm -geometry 800x600 & for i in $(seq 1 20); do DISPLAY=:99 xdpyinfo > /dev/null 2>&1 && echo "Xwayland ready" && break echo "Waiting for Xwayland... attempt $i" sleep 1 done export DISPLAY=:99 echo "xrandr result: $(DISPLAY=:99 xrandr | grep '*')" # ── Start PipeWire stack ────────────────────────────────────────────────────── export PULSE_RUNTIME_PATH="$XDG_RUNTIME_DIR/pulse" pipewire & sleep 1 wireplumber & sleep 1 # Remove the pulse socket directory that pipewire may have pre-created, then # immediately recreate it empty and start pipewire-pulse with no gap in # between — otherwise pipewire can recreate it first and pipewire-pulse # fails with "File exists" on the socket. rm -rf "$PULSE_RUNTIME_PATH" mkdir -p "$PULSE_RUNTIME_PATH" pipewire-pulse & # Wait for the PulseAudio-compatible socket for i in $(seq 1 30); do pactl info > /dev/null 2>&1 && echo "PipeWire-Pulse ready" && break echo "Waiting for PipeWire-Pulse... attempt $i" sleep 1 done export PULSE_SERVER="unix:${PULSE_RUNTIME_PATH}/native" # Raise the PipeWire scheduling quantum (buffer size). This container runs # llvmpipe software rendering + vp8enc + opusenc all competing for CPU at # ~800% on 8 cores. A larger quantum gives the audio graph more buffer # headroom to absorb brief scheduling jitter, trading a few ms of latency # (irrelevant for one-way streaming) for fewer audible underruns/crackling. pw-metadata -n settings 0 clock.force-quantum 4096 || true # ── Audio sinks ─────────────────────────────────────────────────────────────── # game_output: CSS's own audio output (game sounds, music) — set as CSS's # default playback sink. # mic_sink: browser mic input ONLY — pacat writes here, nothing else. # # Without this split, CSS's own audio output was also landing in mic_sink # (the only sink that existed), which fed back into virtual_mic — CSS's own # game audio was drowning out / interfering with the actual browser mic # signal, so other players never heard a clean voice transmission. pactl load-module module-null-sink sink_name=game_output sink_properties=device.description=game_output || true pactl load-module module-null-sink sink_name=mic_sink sink_properties=device.description=mic_sink rate=48000 channels=1 channel_map=mono || true pactl load-module module-remap-source source_name=virtual_mic master=mic_sink.monitor || true pactl set-default-sink game_output || true pactl set-default-source virtual_mic || true # ── Launch CSS ──────────────────────────────────────────────────────────────── wine ~/Counter-Strike\ Source/revLoader.exe & for i in $(seq 1 60); do WIN=$(DISPLAY=:99 xdotool search --name "" 2>/dev/null | head -1) [ -n "$WIN" ] && echo "CSS window found: $WIN" && break echo "Waiting for CSS window... attempt $i" sleep 1 done sleep 3 # gst_agent.py targets game_output explicitly via pipewiresrc target-object, # so no extra env var is needed here for game audio capture. # ── Mic audio: socat → pacat (on-demand, per TCP connection) ───────────────── # signaling-server.js connects a TCP socket to port 12345 only while the user # holds PTT. socat spawns a fresh pacat per connection and kills it when the # connection closes — pacat only runs while actively transmitting, which also # keeps mic_sink idle (not RUNNING) the rest of the time. socat TCP-LISTEN:12345,reuseaddr,fork \ EXEC:"pacat --playback --device=mic_sink --format=s16le --rate=48000 --channels=1 --latency-msec=20" & echo "socat mic listener started on TCP :12345" # ── Start streaming agent (with auto-restart) ───────────────────────────────── # gst_agent exits with code 1 on ICE failure or other fatal errors. # Without a restart loop, the container sits dead until manually restarted. # 3 second delay before each restart to avoid a tight crash loop. while true; do python3 ~/streaming-agent/gst_agent.py echo "[gst_agent] exited with code $?, restarting in 3s..." sleep 3 done