> ## 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.

# Multiplayer: one Unreal app, many viewers

> Stream a single Unreal Engine instance to multiple users in the same scene using SFU mode.

The outcome: one Unreal Engine instance runs on a Streampixel worker; many viewers all see and hear the same scene in real time. One host with input authority, the rest along for the ride.

## Standard streaming vs. SFU

In **standard mode**, Streampixel allocates one worker per user. Two users on the same project means two separate Unreal instances, two separate worlds, two separate save files. Useful for solo experiences (configurators, walkthroughs, tutorials).

In **SFU mode** (Selective Forwarding Unit), Streampixel allocates **one** worker. The worker streams its video and audio to a media server, which fans the same stream out to every connected viewer. There is one Unreal instance, one world state, one set of game logic running.

|                          | Standard                           | SFU                                                  |
| ------------------------ | ---------------------------------- | ---------------------------------------------------- |
| UE instances             | One per user                       | One total                                            |
| Cost per concurrent user | High                               | Low                                                  |
| Input authority          | Each user controls their own       | Host only by default                                 |
| Use case                 | Configurators, single-player demos | Watch parties, classrooms, presentations, co-viewing |
| Scale                    | Limited by worker fleet            | Tens of viewers per host                             |

<Note>
  SFU is best when most viewers are passively watching what the host is doing. If every user needs independent control of their own avatar in a shared world, that is a true multiplayer game and belongs in a different architecture (dedicated UE server, players connect as Pixel Streaming clients with their own workers).
</Note>

## Roles

* **Host** — connects with `sfuHost: true`. Their input drives the Unreal instance. Exactly one host per session.
* **Viewer** — connects with `sfuPlayer: true`. Receives video and audio. Input is ignored by default.

Host and viewer use the same `appId` (project ID). They are distinguished by the SDK init flags and by the `streamerId` query param when present.

## Step 1 — Prerequisites

| Requirement                                             | Notes                                                                                                    |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Streampixel project                                     | Active build distributed. See [Uploading your build](/resources/quick-start-guide/uploading-your-build). |
| Web SDK installed                                       | `npm install git+https://github.com/infinity-void-metaverse/Streampixel-Web-SDK.git#latest`              |
| Unreal project that handles multiple players gracefully | See [UE-side notes](#ue-side-considerations) below.                                                      |

## Step 2 — Host page

The host loads the project and asserts authority over the scene.

```html theme={"dark"}
<!-- host.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Streampixel SFU Host</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000; }
      #stream { width: 100vw; height: 100vh; }
      .badge {
        position: fixed; top: 12px; left: 12px;
        background: #b91c1c; color: white; padding: 6px 10px;
        border-radius: 4px; font-family: ui-sans-serif, system-ui;
        font-size: 12px; z-index: 10;
      }
    </style>
  </head>
  <body>
    <div class="badge">HOST</div>
    <div id="stream"></div>

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

      const PROJECT_ID = 'YOUR_PROJECT_ID';
      const STREAMER_ID = 'host-1'; // viewers must use the same value

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

      appStream.onVideoInitialized = () => {
        document.getElementById('stream').append(appStream.rootElement);
      };
    </script>
  </body>
</html>
```

The host is a normal interactive stream: keyboard, mouse, gamepad input flows through to Unreal as usual.

## Step 3 — Viewer page

Viewers connect with `sfuPlayer: true` and the same `streamerId` as the host.

```html theme={"dark"}
<!-- viewer.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Streampixel SFU Viewer</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000; }
      #stream { width: 100vw; height: 100vh; }
      .badge {
        position: fixed; top: 12px; left: 12px;
        background: #1d4ed8; color: white; padding: 6px 10px;
        border-radius: 4px; font-family: ui-sans-serif, system-ui;
        font-size: 12px; z-index: 10;
      }
    </style>
  </head>
  <body>
    <div class="badge">VIEWER</div>
    <div id="stream"></div>

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

      const PROJECT_ID = 'YOUR_PROJECT_ID';
      const STREAMER_ID = 'host-1'; // must match the host

      const { appStream } = await StreamPixelApplication({
        appId: PROJECT_ID,
        AutoConnect: true,
        sfuPlayer: true,
        streamerId: STREAMER_ID,
        forceTurn: true,
      });

      appStream.onVideoInitialized = () => {
        document.getElementById('stream').append(appStream.rootElement);
      };
    </script>
  </body>
</html>
```

Viewers see what the host sees, frame-for-frame, with sub-second additional latency from the SFU hop.

<Tip>
  In production, serve a single page that reads `?role=host` or `?role=viewer` from the URL and toggles the SDK flags accordingly. Keeping host and viewer logic on different bundles makes it harder for a viewer to flip themselves into host mode by editing localStorage.
</Tip>

## Step 4 — Voice chat (optional)

Video and audio from the Unreal scene are already streamed. If you want viewers to talk to each other, layer in `StreamPixelVoiceChat`, which is backed by LiveKit:

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

const voice = await StreamPixelVoiceChat({
  appId: PROJECT_ID,
  roomName: 'host-1-voice',  // every participant uses the same room
  identity: crypto.randomUUID(),
  enableMicrophone: true,
});

voice.on('participantConnected', (p) => {
  console.log('joined:', p.identity);
});
```

The `roomName` is the join key — give it the same value across host and all viewers in a session. A common pattern is `${STREAMER_ID}-voice`.

<Warning>
  Voice chat needs a microphone permission, which browsers only grant in response to a user gesture. Initialize `StreamPixelVoiceChat` from a button click, not on page load.
</Warning>

For UE-side voice (avatars speaking inside the game world), see [Built-in voice and text chat](/resources/quick-start-guide/built-in-voice-and-text-chat).

## UE-side considerations

The Unreal project does not magically become multiplayer-aware just because Streampixel is fanning out the stream. SFU streams whatever the one running UE instance renders.

What this means in practice:

* **One PlayerController.** The host's input drives a single PlayerController. Viewers do not have their own pawns or cameras — they see exactly what the host's camera is rendering.
* **No replication needed.** Because there is only one UE instance, you don't need network replication, dedicated servers, or any of the multiplayer plumbing. It's single-player UE that happens to have many spectators.
* **Audio is mixed at the source.** Anything played in the UE world is heard by all viewers. Spatial audio still works but the listener is the host's audio listener, not each viewer's.
* **If you want viewer input** — for example, voting, polls, drawing on the screen — send it as JSON via `emitUIInteraction()` from the viewer page and handle it in Unreal Blueprint with a Pixel Streaming Input Component. The host remains in control of the camera and movement; viewers can only influence things you explicitly expose.

## Limits

A single host can serve **tens of viewers** depending on bitrate, codec, and network conditions. Concrete planning numbers:

| Bitrate per viewer          | Approximate viewers per host |
| --------------------------- | ---------------------------- |
| 8 Mbps (1080p high quality) | 10–20                        |
| 4 Mbps (1080p standard)     | 25–40                        |
| 2 Mbps (720p)               | 40–60                        |

These are practical ceilings, not hard limits. For audiences in the hundreds, run multiple host sessions and load-balance.

<Note>
  Latency between the host and the furthest viewer is dominated by the SFU's location and the viewer's RTT. Pick a [region](/resources/concepts/regions) close to the geographic center of your audience.
</Note>

## Gotchas

<AccordionGroup>
  <Accordion title="Viewer input is ignored by default">
    Mouse clicks, key presses, and gamepad input from viewer pages do not reach Unreal. This is intentional — otherwise a hundred viewers could fight over the camera. If you need viewers to interact, route those interactions through your own application code (e.g., a "raise hand" button in your UI), not through raw input.
  </Accordion>

  <Accordion title="Host disconnect ends the session for everyone">
    There is no automatic host migration. If the host's tab closes, the Unreal instance is reclaimed and viewers see a disconnect. Detect this in your viewer code by listening for the disconnect event and showing "host has left" UI.
  </Accordion>

  <Accordion title="streamerId is the join key">
    Host and viewers must use identical `streamerId` values. A typo means viewers connect to a different worker (or none) and never see the host. Generate the value once in your application and pass it down to both pages.
  </Accordion>

  <Accordion title="One host per streamerId">
    Two pages connecting with `sfuHost: true` and the same `streamerId` is undefined behavior — typically the second connection is rejected. Enforce single-host on your side with a session lock.
  </Accordion>

  <Accordion title="Codec compatibility across viewers">
    SFU forwards the same encoded frames to every viewer. If the host encodes in AV1 (Chrome desktop only), viewers on Safari will not be able to decode. For broad audiences, lock the codec to H264. See [Codec settings](/resources/quick-start-guide/codec-settings).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Voice and text chat" icon="microphone" href="/resources/quick-start-guide/built-in-voice-and-text-chat">
    UE-side voice and chat for in-game communication.
  </Card>

  <Card title="Meeting rooms" icon="video" href="/resources/quick-start-guide/meeting-rooms">
    Pre-built room flows on top of LiveKit voice.
  </Card>

  <Card title="Performance tuning" icon="gauge-high" href="/resources/recipes/performance-tuning">
    Pick the right bitrate and codec for your viewer count.
  </Card>

  <Card title="Regions" icon="globe" href="/resources/concepts/regions">
    Choose a region close to your audience.
  </Card>
</CardGroup>
