> ## Documentation Index
> Fetch the complete documentation index at: https://docs.streampixel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# VR on the web

> Deliver a VR Unreal experience to any WebXR-capable browser headset — Quest, Vision Pro, and others.

<Warning>
  **VR streaming is experimental.** Pixel Streaming for VR is still under active development by Epic Games and is **not production-ready**. Expect rough edges around input mapping, head-tracking jitter, and codec / framerate stability. Use it for prototypes, demos, and internal testing — not for shipping consumer experiences.
</Warning>

The outcome: a viewer puts on a Quest, opens your URL in the headset's browser, taps "Enter VR," and is inside your Unreal experience — no APK install, no app store, no native binary.

## What works, what doesn't

Streampixel streams pixels, and WebXR puts those pixels onto a stereo display. Combined, they let any WebXR-capable browser become a VR client.

|                                                        | Status                                      |
| ------------------------------------------------------ | ------------------------------------------- |
| Meta Quest 2 / 3 / Pro (built-in browser)              | Supported                                   |
| Apple Vision Pro (Safari)                              | Supported                                   |
| Pico 4 / Neo 3 (built-in browser)                      | Supported                                   |
| Desktop VR via Chrome/Edge + WebXR (Index, Vive, Rift) | Supported                                   |
| Native APK / sideloaded app                            | Out of scope — Streampixel is browser-first |
| iOS Safari (non-Vision Pro)                            | No WebXR; falls back to flat 2D stream      |

<Note>
  Streampixel does not ship a native VR runtime. WebXR is the bridge — if a device's browser exposes a WebXR session, the SDK can hand the stream to it.
</Note>

## Step 1 — Prepare the Unreal project

VR is an **in-engine** choice, not a different build target. You still package for Windows like any other Streampixel project — the VR-specific work is inside the editor:

* Start from the VR template (or add VR Pawn + WebXR-friendly setup to an existing project).
* Disable OpenXR (it conflicts with Pixel Streaming). See [Prepare a VR experience](/resources/quick-start-guide/prepare-your-unreal-engine-project-for-vr).
* Render in stereo (single-pass instanced recommended).
* Target a stable framerate matching the headset (72, 90, or 120 Hz).
* Use a forward renderer for best perf on standalone headsets.

## Step 2 — Enable XR input in the SDK

The SDK has an `xrInput` config flag. Enable it on the page that will request the WebXR session:

```javascript theme={"dark"}
import { StreamPixelApplication } from 'streampixelsdk';

const { appStream, pixelStreaming, UIControl } = await StreamPixelApplication({
  appId: PROJECT_ID,
  AutoConnect: true,
  xrInput: true,           // route XR controller / hand input to UE
  forceTurn: true,
});
```

`xrInput: true` tells the SDK to listen for WebXR controller, hand, and pose events and forward them to Unreal as input. Without it, the headset will display the stream but controllers will not function inside the UE world.

## Step 3 — Trigger the WebXR session

Browsers require a user gesture (a tap or click) to enter immersive mode. Wire up a button:

```html theme={"dark"}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Streampixel VR</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000; color: #fff;
        font-family: ui-sans-serif, system-ui; }
      #stream { width: 100vw; height: 100vh; }
      #vr-button {
        position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
        padding: 14px 28px; font-size: 16px; font-weight: 600;
        background: #4e9cff; color: #000; border: 0; border-radius: 8px;
        cursor: pointer; z-index: 10;
      }
      #vr-button:disabled { opacity: 0.4; cursor: not-allowed; }
    </style>
  </head>
  <body>
    <div id="stream"></div>
    <button id="vr-button" disabled>Enter VR</button>

    <script type="module">
      import { StreamPixelApplication } from 'streampixelsdk';

      const PROJECT_ID = 'YOUR_PROJECT_ID';
      const button = document.getElementById('vr-button');

      const { appStream, pixelStreaming } = await StreamPixelApplication({
        appId: PROJECT_ID,
        AutoConnect: true,
        xrInput: true,
        forceTurn: true,
      });

      appStream.onVideoInitialized = async () => {
        document.getElementById('stream').append(appStream.rootElement);

        // Only enable the button if WebXR immersive-vr is supported.
        if (!('xr' in navigator)) {
          button.textContent = 'WebXR not available';
          return;
        }
        const supported = await navigator.xr.isSessionSupported('immersive-vr');
        if (supported) {
          button.disabled = false;
        } else {
          button.textContent = 'VR not supported on this device';
        }
      };

      button.addEventListener('click', async () => {
        try {
          const session = await navigator.xr.requestSession('immersive-vr', {
            requiredFeatures: ['local-floor'],
            optionalFeatures: ['hand-tracking', 'bounded-floor'],
          });

          // Hand the session to the SDK so it can drive xrInput.
          if (typeof pixelStreaming.startWebXR === 'function') {
            pixelStreaming.startWebXR(session);
          }

          session.addEventListener('end', () => {
            button.disabled = false;
            button.textContent = 'Enter VR';
          });

          button.disabled = true;
          button.textContent = 'In VR';
        } catch (err) {
          console.error('Failed to enter VR:', err);
        }
      });
    </script>
  </body>
</html>
```

<Tip>
  Always feature-detect with `navigator.xr.isSessionSupported('immersive-vr')` before showing the button. Without that check, desktop visitors will see a button that throws on click.
</Tip>

## Step 4 — Pick the right codec

Standalone headsets do most decoding in fixed-function silicon. The decoder's codec support is what determines whether your stream plays smoothly or stutters.

| Codec | Quest 2 / 3 / Pro               | Vision Pro       | Pico             | Desktop VR          |
| ----- | ------------------------------- | ---------------- | ---------------- | ------------------- |
| H264  | Hardware-decoded, recommended   | Hardware-decoded | Hardware-decoded | Yes                 |
| VP8   | Software fallback, drops frames | Limited          | Limited          | Yes                 |
| VP9   | Software fallback               | Software         | Software         | Yes                 |
| AV1   | No (not in browser)             | No               | No               | Chrome desktop only |

Set H264 as your default in the dashboard ([Codec settings](/resources/quick-start-guide/codec-settings)) for any project that targets headsets. Other codecs may negotiate but will burn battery and miss frame deadlines.

## Step 5 — Tune for the device

| Headset    | Native refresh | Recommended target | Per-eye resolution |
| ---------- | -------------- | ------------------ | ------------------ |
| Quest 2    | 72 / 90 Hz     | 72 Hz              | 1832×1920          |
| Quest 3    | 90 / 120 Hz    | 90 Hz              | 2064×2208          |
| Quest Pro  | 90 Hz          | 90 Hz              | 1800×1920          |
| Vision Pro | 90 Hz (varies) | 90 Hz              | High DPI per eye   |
| Pico 4     | 72 / 90 Hz     | 90 Hz              | 2160×2160          |

Streampixel can deliver up to \~1080p per eye comfortably; pushing higher requires more bitrate and more decoder headroom. Start at 1920×1080 per eye, 25–30 Mbps, H264, and tune from there. See [Performance tuning](/resources/recipes/performance-tuning) for bitrate guidance.

## Latency

VR is unforgiving of lag. A delay between head movement and rendered frame that you would never notice on a 2D stream becomes nausea-inducing in stereo.

Things that move latency in the right direction:

* **Closest [region](/resources/concepts/regions).** A 30 ms RTT improvement is more valuable than any encoder tweak.
* **Wired or 6 GHz Wi-Fi.** Standalone headsets on 2.4 GHz Wi-Fi are the most common cause of "this feels laggy" complaints. Quest 3 and Vision Pro on Wi-Fi 6E typically work well.
* **H264 over VP9.** H264 hardware decode shaves several ms compared to software-decoded VP9.
* **Lower `minQP`** in the dashboard's adaptive settings, only if your bitrate budget allows it. Quality dips at the same bitrate, but frames arrive faster.

## Gotchas

<AccordionGroup>
  <Accordion title="Vision Pro Safari requires HTTPS, no exceptions">
    `localhost` over HTTP works during development on Quest browsers, but Safari on Vision Pro will not surface the WebXR API on insecure origins. Use a local HTTPS dev server (mkcert, Caddy, or `vite --https`) to test there.
  </Accordion>

  <Accordion title="The Enter VR button must be inside a user gesture">
    `navigator.xr.requestSession` only resolves if it is called synchronously from a click or tap event. Wrapping it in a Promise chain that awaits something else first (like the SDK's `onVideoInitialized`) breaks the gesture link. Always trigger from the button click handler directly.
  </Accordion>

  <Accordion title="Controllers and hand tracking need explicit features">
    Pass `optionalFeatures: ['hand-tracking']` if your UE project expects hand input. If you require it, use `requiredFeatures` instead — the session will fail to start on devices without hand tracking, which is the right behavior for hand-only experiences.
  </Accordion>

  <Accordion title="Audio output device on standalone headsets">
    Quest and Pico route audio to the headset speakers by default. Some users with paired Bluetooth headphones experience added audio latency that does not match the visual stream. Document this for your testers; there is no fix on the streaming side.
  </Accordion>

  <Accordion title="Battery drain">
    A standalone headset decoding a 30 Mbps H264 stream while running its display at 90 Hz drains the battery in roughly 90–120 minutes. For longer experiences, recommend tethered power or build in checkpoints.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Prepare a VR experience" icon="cube" href="/resources/quick-start-guide/prepare-your-unreal-engine-project-for-vr">
    In-engine setup: VR Pawn, OpenXR, rendering paths.
  </Card>

  <Card title="Codec settings" icon="film" href="/resources/quick-start-guide/codec-settings">
    Lock H264 as the default for headset audiences.
  </Card>

  <Card title="Performance tuning" icon="gauge-high" href="/resources/recipes/performance-tuning">
    Bitrate and resolution recipes that ship without dropped frames.
  </Card>

  <Card title="Regions" icon="globe" href="/resources/concepts/regions">
    Pick the closest region — VR latency is dominated by RTT.
  </Card>
</CardGroup>
