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

# Mobile streaming

> Optimize your stream for iOS and Android browsers, including touch input and bandwidth constraints.

The outcome: a clean stream that loads fast on cellular, responds to touch, doesn't bake the device, and survives the iOS Safari quirks that break naive integrations.

Streampixel works on mobile via the device browser — Safari on iOS and Chrome on Android. There is no native mobile SDK; everything is the same Web SDK with a different config profile.

## Step 1 — Detect mobile, don't guess

User-agent sniffing is fragile, but for switching between desktop and mobile config it's good enough:

```javascript theme={"dark"}
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
```

Use this to feed different values into the SDK — not to gate features. A user on a Pixel Tablet wants the desktop experience; a user on an iPhone Mini wants tighter bitrate caps. Detect, then choose.

## Step 2 — Mobile-tuned SDK init

```html theme={"dark"}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport"
      content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no" />
    <title>Streampixel Mobile</title>
    <style>
      html, body { margin: 0; height: 100%; background: #000;
        overscroll-behavior: none; }
      #stream { width: 100vw; height: 100dvh; touch-action: none; }
      #unmute {
        position: fixed; inset: 0; display: flex;
        align-items: center; justify-content: center;
        background: rgba(0,0,0,0.7); color: #fff;
        font-family: ui-sans-serif, system-ui; font-size: 18px;
        z-index: 10;
      }
    </style>
  </head>
  <body>
    <div id="stream"></div>
    <div id="unmute">Tap to start</div>

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

      const PROJECT_ID = 'YOUR_PROJECT_ID';
      const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);

      const { appStream, pixelStreaming, UIControl } =
        await StreamPixelApplication({
          appId: PROJECT_ID,
          AutoConnect: true,
          forceTurn: true,
          touchInput: true,
          startResolutionMobile: '480p',  // mobile-only opener
          preferredCodec: 'H264',          // universal mobile decoder
          maxBitrate: isMobile ? 4_000_000 : 10_000_000,
        });

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

      // iOS requires a user gesture to play audio.
      document.getElementById('unmute').addEventListener('click', () => {
        UIControl.toggleAudio?.();
        document.getElementById('unmute').remove();
      }, { once: true });
    </script>
  </body>
</html>
```

What each option buys you:

* **`touchInput: true`** — translate browser touch events into Pixel Streaming touch input so Unreal sees taps as taps, not as mouse clicks at (0, 0).
* **`startResolutionMobile: '480p'`** — load fast. Adaptive bitrate negotiates upward as the connection allows.
* **`preferredCodec: 'H264'`** — every mobile decoder has H264 silicon. VP9 software-decodes on most phones and burns CPU.
* **`maxBitrate`** — cap the stream so an over-eager adaptive controller doesn't try to push 12 Mbps over LTE.
* **`forceTurn: true`** — most carrier NATs block direct WebRTC; TURN relays just work.

## Step 3 — Touch UI in Unreal

The browser-side `touchInput: true` flag delivers touch events to Unreal. Whether your Unreal project does anything useful with them depends on what touch UI is built into the project itself.

For projects packaged with on-screen joysticks, on-screen buttons, or other touch HUD, see [Hide / unhide touch controls](/resources/additional-unreal-engine-features/hide-unhide-touch-controls) for runtime control of those overlays. Show them when `touchInput` is active and hide them on desktop.

## Step 4 — Bandwidth budget

Plan for the network the user actually has, not the network you wish they had:

| Connection             | Realistic sustained bitrate | Recommended `maxBitrate` |
| ---------------------- | --------------------------- | ------------------------ |
| LTE (good signal)      | 10–20 Mbps                  | 4 Mbps                   |
| LTE (weak / congested) | 1–3 Mbps                    | 2 Mbps                   |
| 5G (sub-6)             | 50–200 Mbps                 | 6 Mbps                   |
| 5G (mmWave)            | 500+ Mbps                   | 8 Mbps                   |
| Hotel / cafe Wi-Fi     | 5–20 Mbps shared            | 4 Mbps                   |

`maxBitrate` is in bits per second. The 4 Mbps cap above is `4_000_000`. Adaptive bitrate will scale down further on bad networks; it will not scale up beyond the cap.

<Tip>
  If your user base skews toward mobile data plans with hard caps, default to 2–3 Mbps and let users opt into higher quality from a settings menu. The bandwidth saved over a 30-minute session adds up.
</Tip>

## Step 5 — Battery and thermals

A long mobile streaming session is a sustained workload: GPU decode, screen at full brightness, radio active. Phones throttle when they get hot, and a throttled phone produces a worse stream.

Things you can do from your application:

* **Set a sensible AFK timeout.** If the user puts the phone down for 30 seconds, Streampixel can disconnect them and free the worker. They can reconnect when they pick it back up.
* **Drop FPS targets if your UE project supports it.** A console command via `emitConsoleCommand('t.MaxFPS 30')` halves render work and saves battery on long-form, low-motion content (configurators, tours).
* **Honour `prefers-reduced-motion`** for any spinner / loader animations on your page.

There's nothing the streaming side can do about a phone's thermal envelope. Cap session length where appropriate or design for short bursts.

## Step 6 — iOS Safari quirks

iOS has the most edge cases. The big ones:

### Audio requires a user gesture

iOS will not start playing audio until a tap, click, or other input event. The "Tap to start" overlay in the example above exists to satisfy that requirement. Without it, the stream connects, video plays muted, and audio never starts even after `audio.play()` is called.

```javascript theme={"dark"}
document.getElementById('unmute').addEventListener('click', () => {
  UIControl.toggleAudio?.();
}, { once: true });
```

### Autoplay policies

Even muted autoplay can fail in some Safari configurations, especially with PiP enabled. The Web SDK handles the muted autoplay attribute correctly, but if you wrap it in your own `<video>` element, set `playsinline muted autoplay` explicitly.

### Tab background

iOS pauses all media playback when the tab backgrounds. Streampixel disconnects after 60 seconds of background; if the user returns sooner, the stream resumes. Beyond 60 seconds, they reconnect (see [Troubleshooting — mobile disconnects](/resources/web-sdk/guides/troubleshooting)).

### Viewport units

`100vh` does not behave the same on iOS Safari as it does in desktop browsers — the URL bar takes a slice and `100vh` doesn't account for it. Use `100dvh` (dynamic viewport height) for full-screen layouts:

```css theme={"dark"}
#stream { height: 100dvh; }
```

## Step 7 — Orientation

Most streamed UE projects are designed for landscape. Lock orientation if your project depends on it:

```html theme={"dark"}
<meta name="screen-orientation" content="landscape" />
```

That hint is non-binding. To force-rotate, listen for the orientation change and show a "Please rotate your device" overlay in portrait:

```javascript theme={"dark"}
function checkOrientation() {
  const portrait = window.matchMedia('(orientation: portrait)').matches;
  document.getElementById('rotate-hint').style.display = portrait ? 'flex' : 'none';
}
window.addEventListener('orientationchange', checkOrientation);
window.addEventListener('resize', checkOrientation);
checkOrientation();
```

The Screen Orientation API's `lock()` method works on Android Chrome but is not supported on iOS Safari, which is why a visual hint is the portable solution.

## Gotchas

<AccordionGroup>
  <Accordion title="Pinch-zoom hijacks touch input">
    Without the `viewport` meta tag pinning `user-scalable=no` and CSS `touch-action: none` on the stream container, browser pinch-zoom and double-tap-to-zoom intercept touches before they reach Unreal. The example above sets both.
  </Accordion>

  <Accordion title="iOS PWA mode strips features">
    Streams launched from a saved-to-home-screen PWA on iOS run in a stripped-down WebView with stricter autoplay, no PiP, and quirky media handling. Test there explicitly if you support that install path.
  </Accordion>

  <Accordion title="Android Chrome address bar resizes the viewport">
    Scrolling on Android Chrome shrinks/expands the URL bar, which resizes the viewport and can trigger video reflow. Using `100dvh` and avoiding scroll on the stream page (`overscroll-behavior: none`) prevents this.
  </Accordion>

  <Accordion title="Cellular networks often block UDP">
    Some carriers and corporate cellular gateways block UDP entirely, which kills WebRTC media. `forceTurn: true` falls back to TCP via TURN, at the cost of a few ms of latency and slightly higher relay load. Worth it for reach.
  </Accordion>

  <Accordion title="Android keyboard pushes the layout">
    If your stream UI includes a text input (chat, login), opening the soft keyboard reflows the page and can shift the video. Use `position: fixed` on the stream container and let the keyboard overlay it instead of resizing.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Hide / unhide touch controls" icon="hand-pointer" href="/resources/additional-unreal-engine-features/hide-unhide-touch-controls">
    Toggle the UE-side touch HUD from your web page.
  </Card>

  <Card title="Performance tuning" icon="gauge-high" href="/resources/recipes/performance-tuning">
    Bitrate and codec recipes for every device class.
  </Card>

  <Card title="Resolution settings" icon="display" href="/resources/quick-start-guide/resolution-settings">
    Set per-device opening resolution defaults.
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/resources/web-sdk/guides/troubleshooting">
    Mobile disconnects, audio gating, and other common issues.
  </Card>
</CardGroup>
