Automation
Run a wasm app with:
/Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm
In this mode:
- stdin is a JSONL command stream
- stdout is a JSONL result stream for
screenshotcommands only - stderr is for logs and fatal protocol errors
- commands are processed in input order
One JSON object per line.
Fatal Errors
The process terminates with an error on stderr when:
- a line is not valid JSON
commandis missing or invalid- a non-
screenshotcommand has invalid fields - a non-
screenshotcommand cannot be enqueued or synchronized - a nonzero
objectIddoes not match the main window
screenshot is the only recoverable command. It writes either a success result or an error result
to stdout.
Window Readiness
There is no window_opened event.
Commands that require a window are held until the main window is:
- created
- visible
- has presented at least one frame
Window-dependent commands:
fencescreenshotpointer_enterpointer_leavepointer_downpointer_uppointer_movepointer_cancelscrollgesture_pangesture_pinchgesture_rotatekey_downkey_up
wait does not require a window.
Queue Semantics
wait
- delays later commands by
ms - uses a timer; it does not block a thread while waiting
fence
- waits until all prior inserted input has been consumed by the guest
- if prior commands caused a frame request, waits for the resulting frame to present
- produces no stdout output
- is not needed immediately before
screenshot, sincescreenshotalready implies the same wait
Event commands
- enqueue the requested runtime event
- do not wait for guest consumption or frame presentation
- produce no stdout output
screenshot
- first waits until all prior inserted input has been consumed by the guest
- for
format:"png", waits for any resulting frame to present - for
format:"json", waits for the guest barrier after prior input so the latest submitted renderer package reflects the effects of those commands - then captures a PNG or writes a JSON renderer-package description
- writes one JSON result line to stdout
- this means
{"command":"key_down",...}followed by{"command":"screenshot",...}captures state after the key event has been applied; PNG additionally waits for any resulting frame to present before capture - an explicit
fenceimmediately beforescreenshotis redundant
When stdin is closed, the app exits after the last queued command completes.
Command Shape
All input lines have this base shape:
interface InputMessage {
command: string
id?: string
}
id is opaque. It is only echoed back by screenshot results.
Wait
{"command":"wait","ms":250}
msis a non-negative integer number of milliseconds
Fence
{"command":"fence"}
Screenshot
{"command":"screenshot","id":"s1","format":"png","rect":{"x":0,"y":0,"width":640,"height":480}}
Fields:
output_fileoptional output pathformatoptional,"png"or"json", default"png"rectoptional capture rect in window content coordinates, top-left origin
Rules:
- If
output_fileis omitted or"", a temporary file is created underTMPDIR. - For
format:"png",rectis clipped against the window content area. - For
format:"json",rectis ignored. format:"json"writes a JSON file describing the most recently submitted renderer package.- JSON screenshot files also include
windowObjectId, the main window's object id. This is useful when testing explicit nonzeroobjectIdinput.
JSON screenshot file format:
interface RendererPackageScreenshot {
windowObjectId: number
shapes: ShapeItem[]
texts: TextItem[]
instructions: InstructionItem[]
clips: ClipItem[]
}
interface Vec4 {
x: number
y: number
z: number
w: number
}
interface U16x4 {
x: number
y: number
z: number
w: number
}
interface ShapeItem {
bounds: Vec4
cornerRadius: U16x4
fillColor: string
fillColors: [string, string, string, string]
strokeColor: string
strokeWidth: number
flags: number
blur: number
opacity: number
}
interface TextItem {
bounds: Vec4
textPlan: number
}
interface InstructionItem {
kind: "shape" | "text" | "clip_push" | "clip_pop"
runLength?: number
}
interface ClipItem {
bounds: Vec4
}
Notes:
boundsand clip rectangles are encoded as{x,y,z,w}matching the runtime vector fields. For rectangles this means(x, y, width, height).cornerRadiusis encoded as four unsigned 16-bit values{x,y,z,w}.- Color values are hex strings like
"0xFF18120C00000000". runLengthis present only for"shape"and"text"instructions.textPlanis the runtime text-plan handle captured when the renderer package was submitted.- The JSON file is intended for testing and automation. It reflects the latest submitted renderer package, not a rasterized image.
Example:
{
"windowObjectId": 123,
"shapes": [
{
"bounds": { "x": 24, "y": 24, "z": 16, "w": 16 },
"cornerRadius": { "x": 0, "y": 0, "z": 0, "w": 0 },
"fillColor": "0xFFC4E87000000000",
"fillColors": [
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"strokeColor": "0x0000000000000000",
"strokeWidth": 0,
"flags": 0,
"blur": 0,
"opacity": 1
}
],
"texts": [],
"instructions": [
{ "kind": "shape", "runLength": 1 }
],
"clips": []
}
Stdout result:
interface ScreenshotSuccessResult {
id?: string
path: string
}
interface ScreenshotErrorResult {
id?: string
error: string
}
Examples:
{"id":"s1","path":"/var/folders/.../shot-1.png"}
{"id":"s1","error":"no renderer package available"}
Event Commands
Common fields for all event commands:
interface EventCommand extends InputMessage {
objectId?: number
timestamp?: number
clientId?: number
deviceId?: number
modifiers?: string
}
Defaults:
objectId:0timestamp: currentPBSysTimeclientId:0deviceId:0modifiers:""
objectId handling:
0targets the main window and leaves the inserted eventobjectIdas0- nonzero
objectIdmust equal the main window object id
modifiers is a space-separated list of:
shiftctrlaltmetacapslockfn
Pointer Events
Commands:
pointer_enterpointer_leavepointer_downpointer_uppointer_movepointer_cancel
Shape:
interface PointerEventCommand extends EventCommand {
command:
| "pointer_enter" | "pointer_leave" | "pointer_down"
| "pointer_up" | "pointer_move" | "pointer_cancel"
pointerId?: number
flags?: string
buttons?: number[]
button?: number
kind?: "mouse" | "touch" | "trackpad" | "pen"
clickCount?: number
x: number
y: number
dx?: number
dy?: number
}
Defaults:
pointerId:0flags:""buttons:[]button: omitted, encoded as0kind:"mouse"clickCount:0dx:0dy:0
flags is a space-separated list of:
primaryin_contacteraserinvertedcoalescedpredicted
buttons and button use 1-based numbering in JSON:
1primary2secondary3middle4+additional buttons
Example:
{"command":"pointer_move","x":120,"y":80,"dx":4,"dy":-2,"flags":"primary"}
Scroll
interface ScrollCommand extends EventCommand {
command: "scroll"
kind?: "mouse" | "touch" | "trackpad" | "pen"
phase?: "changed" | "began" | "ended" | "momentum"
flags?: string
x?: number
y?: number
dx: number
dy: number
wheelZ?: number
}
Defaults:
kind:"trackpad"phase:"changed"flags:""x:0y:0wheelZ:0
flags is a space-separated list of:
preciseinvertedunit_linesunit_pages
Example:
{"command":"scroll","x":120,"y":220,"dx":0,"dy":-48,"flags":"precise"}
Gestures
gesture_pan
{"command":"gesture_pan","phase":"changed","x":120,"y":220,"dx":16,"dy":0}
gesture_pinch
{"command":"gesture_pinch","phase":"changed","x":120,"y":220,"scale":1.05}
gesture_rotate
{"command":"gesture_rotate","phase":"changed","x":120,"y":220,"rotation":0.1}
Shared fields:
phaseoptional:"changed","began","ended", default"changed"x,yoptional, default0,0
gesture_pan defaults dx=0, dy=0.
gesture_pinch defaults scale=1.0.
gesture_rotate defaults rotation=0.
Keyboard
Commands:
key_downkey_up
Two payload forms are supported.
Keyed form:
{"command":"key_down","key":"Tab","deviceKey":"Tab"}
IME/text form:
{"command":"key_down","text":"a"}
Shape:
interface KeyboardEventCommand extends EventCommand {
command: "key_down" | "key_up"
flags?: string
}
interface KeyedKeyboardEventCommand extends KeyboardEventCommand {
key: string
deviceKey: string
}
interface TextKeyboardEventCommand extends KeyboardEventCommand {
command: "key_down"
text: string
}
Rules:
flagsis a space-separated list containingrepeat- keyed and text forms are mutually exclusive
- text form is only valid for
key_down textmay contain up to 8 Unicode codepoints- in text form,
keyCodeanddeviceCodeare derived from the first codepoint
Keyboard key names are case-insensitive and use the PBSysKeyboardKey variant suffix name, for
example:
"LeftBracket""Enter""g""G"
Examples
Pipe a whole command stream:
{
printf '%s\n' '{"command":"pointer_move","x":120,"y":80}'
printf '%s\n' '{"command":"pointer_down","x":120,"y":80,"buttons":[1],"button":1}'
printf '%s\n' '{"command":"pointer_up","x":120,"y":80,"buttons":[],"button":1}'
printf '%s\n' '{"command":"fence"}'
printf '%s\n' '{"command":"screenshot","id":"shot1"}'
} | /Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm
Delay before a screenshot:
{
printf '%s\n' '{"command":"wait","ms":250}'
printf '%s\n' '{"command":"screenshot","id":"shot2","format":"png"}'
} | /Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm
Write the latest render commands as JSON:
printf '%s\n' \
'{"command":"screenshot","id":"pkg1","format":"json","output_file":"/tmp/render-package.json"}' |
/Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm