Runwave is a reusable Playwright CLI for browser games and canvas apps.
This package is the task-neutral version of the PR145 browser runner. It only provides browser control and artifact capture. It does not include a VLM playtester, frame picker, or judge.
Runwave's recording pipeline is gstreamer-only. There is no silent-video
fallback. Any run that sets record: true requires all of:
- Linux. gstreamer's
ximagesrcandpulsesrcelements only work on Linux. - gstreamer 1.x with
ximagesrc,pulsesrc,vp8enc,opusenc,webmmux, andfilesinkavailable onPATHasgst-launch-1.0(override via theRUNWAVE_GSTREAMERenv var or thegstreamerPathstart option). - An X server or Xvfb.
DISPLAYmust be set to a display that Chromium can render into and thatximagesrccan read. - PulseAudio running.
pactl infomust succeed. Chromium's audio must be routed to a sink whose.monitorsource is captured bypulsesrc. On headless servers, load a null-sink (e.g.runwave_sink) and setPULSE_SINKbefore starting Chromium; passaudioSource: "runwave_sink.monitor"(orRUNWAVE_AUDIO_SOURCE) to the start action.
The harness checks these prerequisites before spawning gstreamer and fails fast with a message naming the missing piece.
Non-recording usage (screenshots, state, keyboard/mouse) has no such requirements and runs on macOS/Windows/Linux.
From a local checkout:
npm install
npm testFrom a private GitHub repo in a task Dockerfile:
RUN apt-get update && apt-get install -y \
gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly gstreamer1.0-x pulseaudio xvfb
RUN npm install -g https://github.com/parsewave/runwave.git
RUN npx playwright install --with-deps chromiumThe installed CLI is:
runwave '<json>'Use verbose mode to create profiling logs:
runwave -v '{"action":"start","action_name":"run-start","file":"game/index.html"}'
runwave -v '{"action":"state","action_name":"turn-001-state"}'Verbose mode writes newline-delimited JSON timing events to
<sessionDir>/runwave-verbose.ndjson and includes that path as verboseLog in
verbose operation responses. The log records CLI, daemon, browser, output writing,
state, screenshot, navigation, step timeline, input-event, capture, and cleanup
timings.
Relative file, outputRoot, and outDir paths resolve from:
RUNWAVE_WORKSPACE, when set.- The current working directory.
Useful environment variables:
RUNWAVE_WORKSPACE: base directory for relative paths.RUNWAVE_SESSION_DIR: directory for session JSON files. Defaults to<workspace>/.runwave-sessions.RUNWAVE_SESSION_WAIT_MS: startup wait for the daemon session file. Defaults to60000.
Each browser operation must include action_name and session_id. Use the same
session_id for start, subsequent actions, and stop. By default, operation
artifacts are written under:
state/output/<action_name>/
Start from a local file:
runwave '{
"action": "start",
"action_name": "run-start",
"session_id": "playtest-001",
"file": "game/index.html",
"record": true,
"viewport": { "width": 1024, "height": 620 },
"videoSize": { "width": 1024, "height": 620 },
"keyAliases": {
"left": "a",
"right": "d",
"jump": "w"
}
}'Start from a URL:
runwave '{
"action": "start",
"action_name": "run-start",
"session_id": "playtest-001",
"url": "http://localhost:3000",
"record": true
}'Useful start options:
urlorfile: required target.session_id: required session identifier. Reuse it for all actions targeting the same browser session.record: enable gstreamer audio+video WebM recording. Requires all the prerequisites in the Requirements section. Chromium is launched headed with kiosk/fullscreen flags so gstreamer'sximagesrccan capture the rendered viewport. Override capture sources withvideoSourceandaudioSource. (recordAudiois accepted as a legacy alias forrecord; they mean the same thing — audio is always captured when recording.)headless: defaults totrue; setfalseto watch the browser.channel: optional Playwright browser channel, such aschromeormsedge.executablePath: optional explicit browser executable path.viewport: defaults to{ "width": 1024, "height": 620 }.videoSize: defaults to the viewport.keyAliases: maps semantic names used in steps to real Playwright keys.stateExpression: optional JavaScript expression evaluated in the page.outputRoot: defaults tostate/output.outDir: defaults torecordings/runwave-run-<timestamp>.sessionWaitMs: overrides daemon startup wait for this start operation.
Inspect state:
runwave '{"action":"state","action_name":"turn-001-state","session_id":"playtest-001"}'Capture a screenshot:
runwave '{"action":"screenshot","action_name":"turn-001-screen","session_id":"playtest-001","name":"screen"}'Execute timed keyboard controls:
runwave '{
"action": "step",
"action_name": "turn-002-jump-right",
"session_id": "playtest-001",
"actions": [
{ "type": "key", "start": 0, "end": 900, "key": "right" },
{ "type": "key", "start": 150, "end": 230, "key": "jump" }
],
"captures": [900],
"autoCaptures": false
}'For step, the payload is a sequence. The sequence duration is inferred from
the latest action end, or from start for instant actions.
Click:
runwave '{
"action": "step",
"action_name": "turn-003-click-start",
"session_id": "playtest-001",
"actions": [
{ "type": "click", "start": 100, "end": 500, "x": 512, "y": 310 }
]
}'Screenshots include an 8x8 red mark grid by default. Pointer actions may use
up to 4 grid cell IDs instead of exact pixels. Cell IDs run row-major from 0
at the top-left to 63 at the bottom-right.
Single grid-cell click:
runwave '{
"action": "step",
"action_name": "turn-003-click-start-cell",
"session_id": "playtest-001",
"actions": [
{ "type": "click", "start": 100, "end": 500, "cells": [27] }
]
}'Multi-click sends quick clicks at random points inside the selected cells:
runwave '{
"action": "step",
"action_name": "turn-003-multi-click",
"session_id": "playtest-001",
"actions": [
{ "type": "multi_click", "start": 100, "cells": [27, 28], "count": 10 }
]
}'Drag:
runwave '{
"action": "step",
"action_name": "turn-004-drag",
"session_id": "playtest-001",
"actions": [
{ "type": "drag", "start": 100, "end": 700, "from": { "x": 420, "y": 300 }, "to": { "x": 500, "y": 300 }, "mode": "mouse", "steps": 12 }
]
}'Use mode: "mouse" for canvas and pointer-based games. Use mode: "html5"
for browser-native draggable/drop elements.
Drag endpoints can also use grid cells:
{ "type": "drag", "start": 100, "from_cells": [34], "to_cells": [35], "mode": "mouse" }Move the cursor without clicking:
{ "action": "step", "action_name": "turn-005-hover", "session_id": "playtest-001", "actions": [{ "type": "cursor_move", "start": 100, "end": 500, "cells": [27] }] }Move the mouse without clicking for camera control:
runwave '{
"action": "step",
"action_name": "turn-005-look-around",
"session_id": "playtest-001",
"actions": [
{ "type": "view_move", "start": 200, "end": 900, "dx": 260, "dy": -40, "steps": 16 }
]
}'view_move actions use relative CSS-pixel deltas. Positive dx moves right,
negative dx moves left, positive dy moves down, and negative dy moves up.
Navigate or reset:
runwave '{"action":"reset","action_name":"reset-001","session_id":"playtest-001"}'Stop and finalize the recording:
runwave '{"action":"stop","action_name":"run-stop","session_id":"playtest-001"}'List known sessions:
runwave '{"action":"sessions"}'When record: true is set, stop returns video and audioVideo pointing at
the same recorded audio/video WebM. All recording goes through gstreamer — see
the Requirements section for the mandatory environment
(Linux, gstreamer, X server/Xvfb, PulseAudio).
Every response includes generic browser state:
- URL and title.
- Viewport dimensions.
- Active element summary.
- Pointer-lock element summary.
- Canvas positions and sizes.
For game-specific state, pass a stateExpression on start or an individual
operation:
{
"stateExpression": "() => ({ score: window.score, lives: window.lives })"
}If the expression throws, the response still includes generic state plus
customError.
Each turn writes:
response.json: the main CLI response.*.png: screenshots captured during that operation.NNN-<action_name>.json: detailed sequence log forstepoperations.video/000-runwave-with-audio.webm: final gstreamer audio+video recording.
Active sessions are tracked as JSON files in .runwave-sessions/ by default.
The matching session file is removed by stop.