Rusty XR Companion

Windows and Android companion utilities for public Quest development workflows.

Streaming

The first stream lane is display casting through scrcpy. Install the managed tool cache if scrcpy is not already on PATH:


dotnet run --project src/RustyXr.Companion.Cli -- tooling install-official

dotnet run --project src/RustyXr.Companion.Cli -- cast --serial <serial> --max-size 1280

The WPF app exposes this as Start Display Cast.

Broker H.264 Preview Decode

The broker app-camera H.264 path is a bounded diagnostic lane for validating encoded camera payload transport before a target OpenXR app owns decode-to-texture and layer submission. The companion can start the broker stream, save the elementary H.264 payload, summarize the RXYRVID1 packet framing, and decode a single PNG preview frame when FFmpeg is available.

Install the optional media runtime when ffmpeg.exe is not already on PATH:


dotnet run --project src\RustyXr.Companion.Cli -- tooling install-media
dotnet run --project src\RustyXr.Companion.Cli -- tooling media-status --latest

The installer downloads a Windows x64 LGPL shared FFmpeg build into the managed LocalAppData cache, verifies SHA-256, records metadata, and classifies ffmpeg -version output for GPL/nonfree flags. The app and CLI release zips do not bundle FFmpeg binaries.

Capture and decode a preview:


dotnet run --project src\RustyXr.Companion.Cli -- broker app-camera-h264-probe --serial <serial> --camera-id <id> --capture-ms 900 --max-packets 12 --payload-out .\artifacts\broker-app-camera\camera.h264 --json
dotnet run --project src\RustyXr.Companion.Cli -- media decode-h264-preview --payload .\artifacts\broker-app-camera\camera.h264 --out .\artifacts\broker-app-camera\camera-preview.png --json

For broker-to-broker payload relay work, keep the proxy path separate from the camera capture and preview decode path. The synthetic proxy probe validates the RXYRVID1 TCP relay without camera permission or a second headset. Starting a real proxy subscribes to a remote broker H.264 TCP stream and republishes it on the local broker endpoint; LAN binds remain explicit opt-ins.


dotnet run --project src\RustyXr.Companion.Cli -- broker h264-proxy-probe --serial <serial> --json
dotnet run --project src\RustyXr.Companion.Cli -- broker h264-proxy-start --serial <serial> --remote-host <source-broker-lan-ip> --remote-port 8879 --local-port 8879 --json

The WPF Streams tab exposes the same flow through Capture H.264 Preview and Decode H.264 Preview. Leave the FFmpeg path field blank for managed runtime or PATH discovery, or set it to an explicit user-supplied ffmpeg.exe.

Fast Stereo Projection Checks

For headset-side renderer parity checks, use the sibling Rusty XR composite catalog profiles rather than hand-writing launch extras. The direct profile keeps in-app Camera2 capture and stereo mapping, while the broker profile keeps the H.264 encode/decode/import path and switches to the fast public raw-projection shader at render scale 0.75.


dotnet run --project .\src\RustyXr.Companion.Cli -- catalog verify --path ..\Rusty-XR\examples\quest-composite-layer-apk\catalog\rusty-xr-quest-composite-layer.catalog.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --device-profile xr-composite-comparison-level-4 --runtime-profile camera-stereo-gpu-composite-fast075 --settle-ms 9000 --logcat-lines 1600 --out .\artifacts\verify
dotnet run --project .\src\RustyXr.Companion.Cli -- catalog verify --path ..\Rusty-XR\examples\quest-composite-layer-apk\catalog\rusty-xr-quest-composite-layer.catalog.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --device-profile xr-composite-comparison-level-4 --runtime-profile broker-h264-stereo-live-openxr-projection-fast075-probe --settle-ms 35000 --logcat-lines 2400 --out .\artifacts\verify

For the Quest custom stereo broker profiles, the public catalog pins Camera2 IDs 50 and 51 for the outside front camera pair, uses square 1280x1280 broker frames, requests hardware-buffer decode, and reports pair/import/render timing for scorecards. Motion artifacts during head movement are tracked as a future compensation problem, not as a projection-orientation failure.

MediaProjection Frame Receiver

Examples that stream final display-composite frames can use the built-in receiver. It implements the same length-prefixed JSON header plus payload protocol as the Rusty XR core media-pipeline tool. For camera-driven custom layers, this stream is for Windows/operator inspection of the final headset screen; it is not the layer's camera source.


dotnet run --project src\RustyXr.Companion.Cli -- media reverse --serial <serial> --device-port 8787 --host-port 8787
dotnet run --project src\RustyXr.Companion.Cli -- media receive --port 8787 --out .\artifacts\media-stream --once

The Quest app connects to 127.0.0.1:8787; adb reverse forwards that socket to the Windows receiver. MediaProjection still requires the headset popup to be accepted by the user in normal app flows.

When using --once, the receiver exits after the first frame. The target app may then log a broken pipe because the proof receiver closed the socket; use the saved frame and matching receiver metadata as the success signal for that short validation mode.

Some Quest builds show a second MediaProjection panel titled `Select view you want to share. The user must select Entire view` in the headset and then press Share. ADB shell taps and UIAutomator inspection are not reliable for that panel because the VR selector focus is owned by system UI, so the companion records diagnostics but does not claim it can clear that prompt.

Catalog verification can arm the receiver automatically:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --runtime-profile media-projection-stream --media-receiver --settle-ms 7000 --logcat-lines 1000 --out .\artifacts\verify

OSC UDP Probe

The companion CLI can send and receive generic OSC messages for lightweight control/sensor adapter tests. OSC uses UDP, so test it over the Quest LAN IP; the adb reverse helper used by MediaProjection is a TCP path and is not used for this probe.

Launch a Rusty XR app profile that enables an OSC listener, then send a probe message from the companion:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --runtime-profile osc-udp-listener --settle-ms 5000 --logcat-lines 1000 --out .\artifacts\verify
dotnet run --project src\RustyXr.Companion.Cli -- osc send --host <quest-lan-ip> --port 9000 --address /rusty-xr/probe --arg string:hello

The headset log should include Rusty XR OSC packet received. To test the Windows receive side instead, run:


dotnet run --project src\RustyXr.Companion.Cli -- osc receive --host 0.0.0.0 --port 9000 --count 1

The osc-udp-listener profile enables the headset diagnostic HUD. Use osc-udp-listener-no-overlay when you want to measure UDP ingress without drawing the HUD.

Broker OSC Drive

The Rusty XR broker proof-of-concept exposes a sidecar OSC ingress profile. After building examples\quest-broker-apk in the sibling Rusty XR checkout, launch the broker and send a drive value to the Quest LAN IP:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path ..\Rusty-XR\examples\quest-broker-apk\catalog\rusty-xr-quest-broker.catalog.json --app rusty-xr-quest-broker --serial <serial> --stop-catalog-apps --install --launch --runtime-profile broker-osc-drive-ingress --settle-ms 5000 --logcat-lines 1000 --out .\artifacts\verify
dotnet run --project src\RustyXr.Companion.Cli -- broker forward --serial <serial>
dotnet run --project src\RustyXr.Companion.Cli -- broker status --json
dotnet run --project src\RustyXr.Companion.Cli -- broker host-manifest --json
dotnet run --project src\RustyXr.Companion.Cli -- broker lease-request --scope runtime.bio --expected-revision 7 --operator-confirmed --json
dotnet run --project src\RustyXr.Companion.Cli -- broker lease-release --lease control-lease-1 --scope runtime.bio --reason operator_done --json
dotnet run --project src\RustyXr.Companion.Cli -- broker streams --json
dotnet run --project src\RustyXr.Companion.Cli -- broker sample --subscribe --json
dotnet run --project src\RustyXr.Companion.Cli -- broker verify --serial <serial> --osc-host <quest-lan-ip> --out .\artifacts\verify --json
dotnet run --project src\RustyXr.Companion.Cli -- osc send --host <quest-lan-ip> --port 9000 --address /rusty-xr/drive/radius --arg float:0.75

The broker turns accepted OSC packets into localhost WebSocket osc_drive events. The public Unity Quest target for this comparison is The Big Red Button Institute, which consumes the broker stream events and exposes the direct Unity OSC acknowledgement route used by broker compare.

broker host-manifest reads /broker/host_manifest and reports the broker's declared host role, endpoint visibility, security policy, clock domain, and capabilities. Use it before selecting a UI/control endpoint; broker verify now includes the same read-only manifest check so stale broker APKs fail before stream or mutation diagnostics depend on implicit endpoint assumptions. broker lease-request and broker lease-release exercise the same lease-aware control contracts used by broker-described UI surfaces. They mutate only broker lease state; the broker still validates holder, scope, revision, expiry, and conflicts before granting or releasing authority.

Broker Comparison Runner

Use broker compare when you need one bundle that compares a direct target-app OSC route with the broker-routed OSC/WebSocket route:


dotnet run --project src\RustyXr.Companion.Cli -- broker compare --quest-host <quest-lan-ip> --serial <serial> --count 16 --interval-ms 250 --out .\artifacts\broker-compare --json

The direct route sends /rusty-xr/drive/radius to UDP 9001 with value01, sequence, host_send_unix_ns, and reply_port arguments. A target app that implements the acknowledgement profile replies on /rusty-xr/drive/ack with sequence, host send time, target receive time, target acknowledgement send time, value, and accepted-pulse state.

The comparison runner estimates target-minus-host clock offset with the same four-timestamp shape used by NTP-style round-trip probes, then reports aligned host-to-target and target-to-host timing. The recommended offset is the median offset from the lowest-RTT quartile, which avoids letting a single delayed UDP packet dominate the assessment.

Use The Big Red Button Institute when you want a complete Unity-side Quest scene that accepts the direct route on UDP 9001 and the broker route through localhost WebSocket stream events.

For route-specific checks:


dotnet run --project src\RustyXr.Companion.Cli -- broker compare --quest-host <quest-lan-ip> --skip-broker-osc --out .\artifacts\broker-compare --json
dotnet run --project src\RustyXr.Companion.Cli -- broker compare --quest-host <quest-lan-ip> --serial <serial> --skip-direct-osc --out .\artifacts\broker-compare --json

Broker LSL Probe

The broker can forward accepted WebSocket latency samples to LSL when the APK was built with a compliant Android liblsl.so. The public source does not vendor that native library. When LSL is packaged, broker status reports lsl.gateway, publisher: native-lsl, stream name rusty_xr_broker_latency, and stream type rusty.xr.latency.

Send one broker sample and inspect the acknowledgement:


dotnet run --project src\RustyXr.Companion.Cli -- broker sample --path lsl_probe --bytes 128 --json

The latency_ack payload should show lsl_forwarded: true. A Windows LSL receiver can independently resolve the stream by name and pull the string sample. If resolution fails, check that the Quest and Windows host are on the same reachable network and that local firewall policy allows LSL multicast and inbound traffic for the receiving process.

LSL Round-Trip Diagnostics

The companion can run LSL diagnostics from the CLI when given a compatible Windows lsl.dll. The public companion does not vendor native LSL binaries; pass the DLL path explicitly, set RUSTY_XR_LSL_DLL, place lsl.dll beside the executable, or put it on PATH.

Check native runtime loading first:


dotnet run --project src\RustyXr.Companion.Cli -- lsl runtime --lsl-dll <path-to-windows-lsl.dll> --json

Use local loopback to validate the Windows receiver path without a Quest. The command creates a temporary double64 LSL outlet and inlet, opens the inlet, primes the stream, sends sequenced samples, estimates LSL clock correction, and writes JSON, CSV, Markdown, and PDF outputs:


dotnet run --project src\RustyXr.Companion.Cli -- lsl loopback --lsl-dll <path-to-windows-lsl.dll> --count 16 --interval-ms 100 --out .\artifacts\lsl-loopback --json

Use broker round-trip after launching an LSL-capable broker APK and forwarding its WebSocket endpoint. This route sends broker latency samples over WebSocket, pulls the matching rusty_xr_broker_latency LSL string samples, and reports host send to broker receive, broker processing, LSL receive delay, and clock correction uncertainty:


dotnet run --project src\RustyXr.Companion.Cli -- broker forward --serial <serial>
dotnet run --project src\RustyXr.Companion.Cli -- lsl broker-roundtrip --serial <serial> --lsl-dll <path-to-windows-lsl.dll> --count 8 --interval-ms 250 --out .\artifacts\lsl-broker --json

Pass --no-pdf when only machine-readable JSON and CSV are needed.

Broker Bio Simulation

Use broker bio-simulate to publish synthetic Polar-compatible payloads through the broker as generic stream events:


dotnet run --project src\RustyXr.Companion.Cli -- broker bio-simulate --serial <serial> --count 8 --interval-ms 250 --out .\artifacts\broker-bio-sim --json

The command publishes:

and RR intervals

frames

Each payload includes the service UUID, characteristic UUID, notification mode, raw payload bytes as base64, decoded summary fields, and the intended public LSL stream schema. This is a protocol-level diagnostic stream through the broker; it does not make the Windows computer advertise as a Bluetooth peripheral.

Subscribe to one lane while publishing targeted samples:


dotnet run --project src\RustyXr.Companion.Cli -- broker subscribe --stream bio:polar_ecg --listen-ms 6000 --json
dotnet run --project src\RustyXr.Companion.Cli -- broker bio-simulate --serial <serial> --count 2 --skip-hr --skip-acc --json

Visual Inspection

For agent and operator verification, capture still frames without routing a downstream app-specific media pipeline:


dotnet run --project src/RustyXr.Companion.Cli -- hzdb screenshot --serial <serial> --out .\artifacts\screenshots

The default screenshot path uses ADB exec-out screencap -p first. Pass --method metacam when you specifically need hzdb's metacam capture path.

Tool Boundary

Rusty XR Companion does not reimplement video streaming. It starts and manages external tools where that is the better open-source base. Keep upstream tool licenses and update paths explicit when packaging or documenting a release.

Display-composite or MediaProjection frame streams should be exposed by the target APK through a public adapter contract before the companion consumes them. The generic companion owns connection, launch extras, casting, screenshots, frame receipt, and diagnostic proof; it does not copy app-specific renderer behavior or app-visible camera acquisition.

Future Stream Lanes

Planned public lanes:

simulation where the host platform supports it