Перейти к содержимому

Protocol reference

Это содержимое пока не доступно на вашем языке.

This page is VXP as it appears on the pipe, for anyone writing a client in any language. The Rust SDK’s types serialize to exactly these bytes. Read Build an extension first for the model behind it.

The current version is VXP 1.7.

vrOS creates one endpoint per extension before it launches the process, and puts three variables in its environment:

Variable Holds
VROS_EXT_ENDPOINT The endpoint to connect to
VROS_EXT_TOKEN A token for this launch only. Send it back in hello; never log it
VROS_EXT_ID Your extension id

On Windows the endpoint is the named pipe \\.\pipe\vros.ext.<id>. On Linux it is the Unix socket ext-<id>.sock in $XDG_RUNTIME_DIR/vros/. Read the variable rather than rebuilding the name. The pipe can appear a moment after your process starts, so retry the connect for a few seconds.

Every frame is one JSON-RPC 2.0 object on one line, terminated by \n.

  • Batch arrays are refused.
  • A frame over 3 MiB is corruption, not traffic.
  • Whitespace, NUL, and a byte-order mark around a line are trimmed, and empty lines are skipped.
  • Request ids are unsigned integers.
  • Unknown fields are ignored everywhere. That is what lets minors add them.

A request names a namespace as its method. Its params is that namespace’s request object, tagged by op:

{"jsonrpc":"2.0","id":7,"method":"kv","params":{"op":"get","key":"count"}}

The reply’s result is the namespace’s reply object, tagged by result:

{"jsonrpc":"2.0","id":7,"result":{"result":"value","value":3,"revision":12}}

Events from the host are notifications — no id — with method set to "event":

{"jsonrpc":"2.0","method":"event","params":{"event":"panel_shown","panel":"overlay"}}

There are two kinds, and they never overlap.

Domain errors ride inside a successful reply as that namespace’s err variant, with a code and usually a message:

{"result":"err","code":"value_too_large","message":"…"}

Protocol errors are the JSON-RPC error object. They mean the request never reached a namespace:

Code Meaning
-32700 The line is not JSON
-32600 Not a valid request: a batch, a wrong jsonrpc, a frame with no method
-32601 Unknown namespace
-32602 params does not match the namespace’s request shape
-32603 Internal
-32001 capability_denied — the namespace exists, but it was not granted to you
-32002 A request arrived before a valid hello. The host disconnects

Replies, events, and codes can all grow in later minors. Treat an op, a result, an event, or a code you do not recognize as something to skip, not as a reason to stop.

The first frame on every connection, within five seconds of launch.

{"jsonrpc":"2.0","id":1,"method":"hello","params":{"id":"com.example.hello","vxp_version":"1.7","token":"…"}}

A welcome carries the version the host speaks, what the user granted — the manifest’s requests narrowed by the user’s consent, in the manifest’s own shape — the user’s locale, and, when vrOS can resolve it, the user’s vrOS content folder:

{"result":"welcome","vxp_version":"1.7","granted":{"bus":["vr.lifecycle"],"overlays":false,"capture":false,"audio":false,"media":false,"osc":false,"network":false,"secrets":false},"locale":"en-US","content_root":"C:\\Users\\you\\Documents\\vrOS"}

face_provide and face_read appear in granted only when they are true. A missing grant stays missing for the whole connection.

A rejection is followed by a disconnect:

{"result":"rejected","code":"version_unsupported","message":"host speaks 1.7 (N and N-1 majors)"}
Code Why
bad_token The token does not match this launch
wrong_id The id is not the one this endpoint was created for
version_unsupported Your major is older than the host’s previous major, or newer than its current one
already_connected This extension already has a live connection

The minor you send matters: some events go only to clients that said a new enough minor. Send the version your code actually understands.

Your private key/value store. Last write wins, and every key has a revision that only goes up. No capability.

op Fields Reply
get key value with value and revision. An absent key is value: null, revision: 0
set key, value (any JSON) written with the new revision and over_quota
delete key deleted, even if the key was already gone
list — keys with keys

Keys are at most 256 bytes and values at most 64 KiB. The store has a soft quota of 1 MiB per extension. Going over it sets over_quota and badges the extension in the pane; nothing is ever deleted to enforce it.

A set on a key your manifest declares in [[settings]] must pass that setting’s kind and bounds.

Errors: key_too_large, value_too_large, internal.

Named slots in the OS credential store. Needs secrets.

op Fields Reply
set slot, value ok
get slot value with value, or null for an empty slot
delete slot ok
exists slot exists with exists

A value is at most 2048 bytes, and a longer one is refused, never truncated. Secrets never appear in KV, config, logs, or anything vrOS displays.

Errors: not_persistable (Linux, where there is no credential backend yet — ask per session instead), too_large, internal.

op Fields Reply
publish topic, payload (any JSON) ok
subscribe pattern ok
unsubscribe pattern ok

You may always publish to and subscribe to your own prefix, ext.<id>.. Anything else, including vr.lifecycle and other extensions’ topics, needs a matching pattern in your manifest’s bus list. A pattern is a literal topic or a prefix ending in .*: ext.cat.kittyn.twitch.* covers ext.cat.kittyn.twitch.chat.message.

Messages arrive as bus events. Subscribing to vr.lifecycle delivers vr_lifecycle events instead.

Errors: not_allowed, internal.

Your manifest’s panels need no define: the host reads and compiles them when your extension starts, and their names are the role keys, overlay and dash-tab.

op Fields Reply
props panel, props applied
define panel, source, optional width, height, anchor defined with revision
remove panel removed

props is a list of [name, value] pairs, merged latest-wins. The host coalesces them to about 30 updates a second per panel. A value is tagged by type:

t v Slint type
bool true bool
i64 21 int (exact up to 2⁵³)
f64 0.5 float
str "text" string
str_list ["a","b"] [string]
rows [[{"t":"str","v":"row"},{"t":"i64","v":-7}]] a model of rows
assets ["e:icon:1"] [image], from keys you uploaded with assets
{"op":"props","panel":"overlay","props":[["count",{"t":"i64","v":21}],["title",{"t":"str","v":"Hello"}]]}

define creates or replaces a runtime overlay panel from a whole .slint source. It needs overlays. The source is compiled before the reply, so a compile error comes back as compile_failed with the compiler’s message. width and height are logical pixels, defaulted and clamped by the host. anchor (world or hmd) is a hint for where the panel first appears; once the user moves it, their placement wins. Re-defining bumps the revision. Runtime panels are overlays only, and remove cannot remove a manifest panel.

Panel names are [a-z0-9-], at most 64 bytes. A source is at most 256 KiB.

Errors: invalid_name, source_too_large, compile_failed, no_such_panel, not_allowed, internal.

A panel receives VR-keyboard and hardware keys only if its root component declares both of these:

  • callback field-focus(bool) — fire it on every text field’s has-focus edge. The host consumes it; it never reaches your extension.
  • in property <int> field-blur — the host bumps it when focus moves elsewhere. Answer with a changed field-blur handler that clears your fields’ focus.

Several fields can share the one callback; the host nets each frame’s edges, so moving between two of your fields reads as still focused. When you remove a focused field or change pages, report the loss yourself: a destroyed item cannot be relied on to run its handler.

For precise click-away, put an ExtFocusRegion from @vros/theme.slint inside each text box, with width: parent.width; height: parent.height; focused: input.has-focus;. Panels without regions keep working; their whole panel counts as the field.

import { ExtTheme, ExtFocusRegion } from "@vros/theme.slint"; is the only import a panel may use. The host fills it with the user’s live settings.

  • ExtTheme.c is the current palette: bg0, bg1, bg2, line, tx-hi, tx-mid, tx-low, accent, accent-text, ok, warn, err, focus, live, live-text.
  • ExtTheme.current is the theme id: signal, graphite, aura, meow, ember, cobalt, mono, volt, crimson, daylight, hc-dark, or hc-light. OLED mode is a separate flag, ExtTheme.oled, and does not change current. Prefer the palette to checking ids.
  • Sizes follow the density: space-1 to space-6, control-h, hit-min, radius-card, radius-control, border-default, and the type sizes type-title, type-body, type-data, type-label, type-numeral. is-desktop is true on the desktop app.
  • reduced-motion is the user’s setting; motion-press and motion-expand already drop to zero under it.

Images your panels display. You fetch and decode them; vrOS never loads a URL for you. No capability.

op Fields Reply
put key, width, height, frames, loop ok
remove key ok
clear — ok

Each frame is {"rgba_base64": "…", "delay_ms": 100}: straight RGBA8, exactly width × height × 4 bytes once decoded. loop is the total play count, and 0 repeats forever.

{"op":"put","key":"e:icon:1","width":1,"height":1,"frames":[{"rgba_base64":"/////w==","delay_ms":100}],"loop":0}
Limit Value
Size up to 128 × 128
Frames up to 60, each 20–1000 ms
One asset 2 MiB decoded
Your table 1024 assets or 32 MiB; the least recently used are evicted to make room
Animating at once 24 per panel

A key is 1–128 bytes of letters, digits, and - _ . : /. It names an image in your own table and nowhere else. The table lives as long as your connection. Animations run on the renderer’s clock while the panel is visible; do not drive them with props.

Errors: too_large, too_many_frames, budget_full (the host is busy — retry), bad_key, bad_pixels.

Named commands the user can bind. No capability.

op Fields Reply
register name ok, also for a name already registered
unregister name ok, also for a name that was not registered

A registered command does nothing until the user assigns it to one of the extension action slots and binds that slot in SteamVR. Then each press arrives as a command_invoked event. Registrations end with the connection, so register again after every hello. Names follow the panel-name rule.

Errors: invalid_name, internal.

[[settings]] in your manifest declares a form. vrOS renders it in the Extensions pane, even while your extension is stopped, and the values are ordinary KV rows under the declared keys.

[[settings]]
key = "send_target"
kind = "text"
label = "Send target"
default = "127.0.0.1:9000"
placeholder = "host:port"
[[settings]]
key = "rate"
kind = "number"
label = "Updates per second"
default = 10
min = 1
max = 60
[[settings]]
key = "mode"
kind = "select"
label = "Mode"
default = "auto"
choices = [
{ value = "auto", label = "Automatic" },
{ value = "manual", label = "Manual" },
]
kind default Rules
text a string of at most 1 KiB placeholder allowed
toggle true or false —
number an integer within bounds min and max required, min < max, both within ±1,000,000
select one of the choice values 2–8 choices, each with a unique value

At most 16 settings. Keys are [a-z0-9_-], at most 64 bytes; labels at most 200 bytes. A manifest that breaks these fails to load, with the reason on the pane.

op Fields Reply
get key value with key, value, is_default
list — values with entries, in manifest order

The value is what is stored, or the default if nothing is. There is no settings write on the wire: when the user edits a field you get a settings_changed event, and your own writes are ordinary kv sets to the same keys.

Errors: no_such_key, no_schema, internal.

Locale-resolved text from the catalogs built into vrOS.

op Fields Reply
get prefix, optional locale strings with the locale actually served and entries

Only keys under ext. are served, and today that means the keys vrOS’s own bundled extensions use. Your own keys are not in those catalogs, so expect empty results for your prefix and ship your own translations. An unknown locale serves en-US and says so. Re-fetch on locale_changed.

Errors: prefix_not_allowed, internal.

Needs audio. vrOS decodes a sound once, keeps it in memory, and plays it on its named channels:

Channel Plays into
soundboard The soundboard output
mic The device that feeds the user’s microphone, such as a virtual cable. Nothing is captured
ui Interface feedback; follows the system default output
op Fields Reply
load path, normalize loading with the new sound id, then an audio_loaded event
unload sound ok
pre_arm channel ok — opens the channel’s output ahead of the first play
play sound, targets, looping ok
stop sound ok
stop_all — ok
get_status — status with playing
list_channels — channels
list_devices — devices with endpoints and default_endpoint

path must be absolute, at most 256 MiB, and decode in under 30 seconds; the welcome’s content_root is the user’s vrOS folder. load answers at once and the decode finishes in the background. Play a sound only after its audio_loaded event says loaded — playback never decodes.

{"op":"play","sound":"s-00000001","targets":[{"channel":"soundboard","gain_pct":80},{"channel":"mic","gain_pct":40}],"looping":false}

Each target has its own gain, 0–100. Targets that cannot play are skipped, and only if none of them could does play fail. Status rows list the targets that actually accepted the sound. Playing a sound again replaces its earlier playback.

Errors: not_found, undecodable, too_long, cache_full, unknown_sound, unknown_channel, sample_not_cached, no_valid_targets, no_endpoint, internal.

Needs media. Plays a video or audio stream in a vrOS player. Windows only today; elsewhere every request answers unsupported.

op Fields Reply
open key, url, title, and the options below opened with session and source_generation
set session and any of the options except native_controls applied with source_generation
reload session, url, optional title applied with the new source_generation
close session closed
status session status

The options, with their defaults: max_fps (30), volume_pct (100), muted (false), paused (false), show_video (true), native_controls (false), and controls_on_player (false).

  • key is your own name for the slot, [a-z0-9-], at most 64 bytes; one open session per key.
  • url is at most 8 KiB and title at most 256 bytes. Treat the URL as a credential: never log or store it.
  • max_fps is 1–120, volume_pct 0–100.
  • vrOS runs four sessions at a time across all extensions; a fifth gets too_many.

Hiding the video or changing a value keeps the stream and its audio running. reload is the only thing that changes the source, and it bumps source_generation.

Status comes as media_snapshot events: complete, coalesced, and delivered only to clients that said hello with 1.4 or newer. Each session reports playback (opening, buffering, playing, paused, ended, or failed with a code) and, separately, video (disabled, warming, ready, recovering), so healthy audio can sit beside recovering video.

With native_controls: true the player draws its own pause, volume, and stop controls, and each finished gesture arrives as a media_control event carrying an absolute action — set_paused, set_volume, or stop. Apply it with your own set or close; check its source_generation against the session’s current one first. controls_on_player puts those controls on the video rather than below it.

Errors: invalid_url, invalid_key, invalid_value, unsupported, too_many, no_such_session, busy, endpoint_unavailable, open_failed, playback_failed, dimension_limit.

Device-neutral face tracking. A provider (face_provide) offers a source; the user picks sources for eyes and mouth on the Face Tracking page; a consumer (face_read) reads what was picked. Both grants are off until the user turns them on for your extension, and either can be revoked while you run. Face updates are delivered only to clients that said hello with 1.5 or newer.

op Fields Reply
register source registered with generation
unregister source_id ok
publish source_id, generation, sample ok
subscribe eyes, mouth ok
unsubscribe — ok

A source describes itself honestly:

{"op":"register","source":{"id":"sim","name":"Simulated eyes + mouth","device":"Demo","transport":"VXP","capabilities":{"combined_gaze":true,"eye_lids":true,"expressions":["jaw_open"]},"max_rate_hz":30}}

The id is [a-z0-9-], at most 64 bytes; the three text fields at most 128 bytes; max_rate_hz 1–240. capabilities can claim combined_gaze, per_eye_gaze, eye_lids, and a list of mouth expressions from the fixed set of 37 (jaw_open, mouth_smile_left, tongue_out, …). An extension can register up to 4 sources, and vrOS holds 16 in all.

Publish only when asked. Every face_update event lists demands: which of your sources are wanted, for eyes, mouth, or both. Publish nothing otherwise — the host answers not_demanded — and stop the moment the demand goes away.

A sample carries an increasing sequence, its age_ms since you acquired it (under 250, or it is stale), and eyes, mouth, or both:

  • Gaze directions are unit vectors in headset space: +X right, +Y up, −Z forward.
  • Eyelid closed_left / closed_right run 0 open to 1 closed.
  • convergence_m is metres; confidence is 0–1.
  • Mouth weights are {"expression": "jaw_open", "value": 0.5}, each 0–1.

Send only what you measured. A missing channel is unknown, never zero, and missing gaze never means closed eyes.

A consumer calls subscribe and reads face_update events. Each is a complete snapshot, never a delta: the grants, the selected eyes and mouth with their age, and the demands. Drain them continuously, and add your own delivery delay to age_ms before trusting a sample.

Errors: not_granted, invalid_source, invalid_sample, too_many, no_such_source, stale_generation, out_of_order, not_demanded, unsupported.

event Fields When
shutting_down — vrOS is stopping you. Flush and exit 0
vr_lifecycle state: detached, attached, quitting The VR runtime came or went, if you subscribed to vr.lifecycle
bus topic, payload A message on a topic you subscribed to
panel_shown panel The panel became visible anywhere: overlay, Dash tab, or desktop
panel_hidden panel …and stopped being visible anywhere. Always paired with panel_shown
panel_event panel, callback, args A panel callback fired. args are typed values, as in props
command_invoked command The user triggered one of your commands
settings_changed key, value The user edited a setting. The value already passed validation
locale_changed locale The UI language changed. Re-fetch strings and re-push your words
audio_loaded sound, outcome A load finished: loaded with duration and levels, or failed with a code
media_snapshot granted, sessions Media status changed (1.4+)
media_control key, session, source_generation, action A player gesture on a session with native controls (1.6+)
face_update state Face grants, selections, or demands changed, or a new sample arrived (1.5+)
VXP Added
1.0 The handshake, kv, secrets, bus, panel, commands, and the lifecycle, bus, panel, and command events
1.1 settings and strings, [[settings]] in the manifest, and the settings_changed and locale_changed events
1.2 audio, the audio_loaded event, and content_root in the welcome
1.3 assets and the assets prop type, the anchor hint on define, and number and select settings
1.4 media, and the media_snapshot event
1.5 face, the face_provide and face_read capabilities, and the face_update event
1.6 Native player controls: native_controls, the media_control event, and title on reload
1.7 controls_on_player on open and set

A manifest can also grow keys that an older host refuses. The quickstart lists which.