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

# Embed a device stream in your app

> Build a custom device experience with @mobilerun/react

Use [`@mobilerun/react`](https://www.npmjs.com/package/@mobilerun/react) to add a
live, interactive Mobilerun cloud device to your React application.

The package provides:

* [`DeviceStream`](https://github.com/droidrun/mobilerun-react) for video, touch,
  keyboard input, and reconnection
* `NavigationBar` for Android Back, Home, and Recents controls
* `RemoteControlHandle` for actions such as opening a URL or taking a screenshot
* A prebuilt stylesheet that you can customize with CSS variables

Your application provides the surrounding layout, branding, loading states, and
access control.

## Keep the API key in your backend

<Warning>
  `MOBILERUN_CLOUD_API_KEY` is an account-level secret. Never include it in browser
  code, public environment variables, or a client-side SDK instance.

  In a multi-user application, your backend must authenticate the current user and
  verify that the user may access the requested device before returning stream
  credentials.
</Warning>

The browser does not need the account API key. It only needs the `streamUrl` and
`streamToken` for the selected device:

```mermaid theme={null}
flowchart LR
    Browser["React application"] -->|"Authenticated request"| Backend["Your backend"]
    Backend -->|"Account API key"| API["Mobilerun Cloud API"]
    API -->|"streamUrl + streamToken"| Backend
    Backend -->|"Device credentials"| Browser
    Browser -->|"WebRTC stream and controls"| Device["Mobilerun device"]
```

The stream token is device-scoped, but it is still sensitive. Do not store it in
local storage or send it to logs, analytics, traces, or error-reporting tools.

## Install

```bash theme={null}
npm install @mobilerun/react @mobilerun/sdk react react-dom
```

Import the package stylesheet once in your application:

```tsx app/layout.tsx theme={null}
import '@mobilerun/react/styles.css';
import './globals.css';
```

## Create a backend credentials endpoint

The following Next.js route uses `@mobilerun/sdk` only on the server. Replace the
example authentication imports with your application's own authorization layer.

```ts app/api/device-stream/[deviceId]/route.ts theme={null}
import Mobilerun from '@mobilerun/sdk';
import { type NextRequest, NextResponse } from 'next/server';

import { canAccessDevice, getCurrentUser } from '@/server/auth';

const apiKey = process.env.MOBILERUN_CLOUD_API_KEY;
if (!apiKey) {
  throw new Error('MOBILERUN_CLOUD_API_KEY is not configured');
}

const mobilerun = new Mobilerun({ apiKey });

export async function GET(
  request: NextRequest,
  context: { params: Promise<{ deviceId: string }> },
) {
  const user = await getCurrentUser(request);
  if (!user) {
    return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
  }

  const { deviceId } = await context.params;

  // A device ID is not authorization. Enforce your application's ownership or
  // membership rules before retrieving credentials with the account API key.
  if (!(await canAccessDevice(user.id, deviceId))) {
    return NextResponse.json({ error: 'forbidden' }, { status: 403 });
  }

  try {
    const device = await mobilerun.devices.retrieve(deviceId);

    return NextResponse.json(
      {
        streamUrl: device.streamUrl || null,
        streamToken: device.streamToken || null,
        state: device.state,
      },
      { headers: { 'Cache-Control': 'no-store' } },
    );
  } catch (error) {
    const status =
      error instanceof Mobilerun.APIError && error.status === 404 ? 404 : 502;

    return NextResponse.json(
      { error: status === 404 ? 'device_not_found' : 'stream_unavailable' },
      { status },
    );
  }
}
```

This endpoint returns only the fields required by the stream. It does not expose
the account API key or the full device response.

## Render `DeviceStream`

Fetch the device credentials from your authenticated backend, then pass them
directly to the package component:

```tsx components/device-stream-panel.tsx theme={null}
'use client';

import {
  DeviceStream,
  NavigationBar,
  type RemoteControlHandle,
} from '@mobilerun/react';
import { useCallback, useEffect, useRef, useState } from 'react';

interface StreamCredentials {
  streamUrl: string | null;
  streamToken: string | null;
  state: string;
}

export function DeviceStreamPanel({ deviceId }: { deviceId: string }) {
  const controls = useRef<RemoteControlHandle>(null);
  const [credentials, setCredentials] = useState<StreamCredentials>();
  const [error, setError] = useState<string>();

  const loadCredentials = useCallback(async () => {
    try {
      const response = await fetch(`/api/device-stream/${deviceId}`, {
        cache: 'no-store',
      });

      if (!response.ok) {
        setError(`Unable to load stream (${response.status})`);
        return;
      }

      setCredentials((await response.json()) as StreamCredentials);
      setError(undefined);
    } catch {
      setError('Unable to reach the stream endpoint');
    }
  }, [deviceId]);

  useEffect(() => {
    void loadCredentials();
  }, [loadCredentials]);

  useEffect(() => {
    if (!credentials || credentials.streamUrl || error) {
      return;
    }

    const timer = window.setTimeout(() => {
      void loadCredentials();
    }, 3_000);

    return () => window.clearTimeout(timer);
  }, [credentials, error, loadCredentials]);

  if (error) {
    return <p>{error}</p>;
  }

  return (
    <section className="device-panel" aria-label="Remote device">
      <div className="device-screen">
        <DeviceStream
          ref={controls}
          streamUrl={credentials?.streamUrl ?? undefined}
          streamToken={credentials?.streamToken ?? undefined}
          hasControl
          muted
          placeholderLabel={credentials?.state ?? 'loading'}
          onStreamHealed={loadCredentials}
        />
      </div>

      <NavigationBar
        onAction={(action) => controls.current?.sendSystemKey(action)}
      />
    </section>
  );
}
```

If a device is still starting, `streamUrl` may be empty. The second effect polls the
backend every three seconds until it becomes available. `onStreamHealed` also
retrieves the current credentials after a stable disconnect.

`NavigationBar` stays below the stream and provides Android Back, Home, and Recents.
The `hasControl` prop controls direct interaction with the device screen; it does not
disable `NavigationBar` or provide an authorization boundary. Anyone who receives
stream credentials must be authorized to control that device. If your UI includes a
view-only mode, also guard the `onAction` callback and treat that as a UI affordance,
not a security control.

The stream starts muted so browser autoplay policies do not block it; only unmute
after a user action.

## Customize the appearance

`DeviceStream` does not add Mobilerun logos or application chrome. Style its parent
and override the package CSS variables to match your product:

```css app/globals.css theme={null}
.device-panel {
  --background: #09090b;
  --foreground: #fafafa;
  --muted-foreground: #a1a1aa;
  --border: #3f3f46;
  --primary: #7c3aed;

  display: flex;
  width: min(100%, 390px);
  aspect-ratio: 9 / 19.5;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 32px;
  background: var(--background);
}

.device-screen {
  min-height: 0;
  flex: 1;
}
```

## Observe the integration safely

Instrument the backend credentials request with your existing OpenTelemetry setup.
Record the device ID, device state, and upstream status, but never credentials.

Use `onConnectionStateChange` for user-visible connection analytics:

```tsx theme={null}
onConnectionStateChange={(connected) => {
  posthog.capture(
    connected ? 'device_stream_connected' : 'device_stream_disconnected',
    { device_id: deviceId },
  );
}}
```

Do not include `streamUrl`, `streamToken`, or `MOBILERUN_CLOUD_API_KEY` in PostHog
events.

## Production checklist

* The account API key is used only by backend code.
* Every credentials request requires an authenticated application session.
* Your backend verifies that the current user may access the requested device.
* Credential responses use `Cache-Control: no-store`.
* Stream credentials are excluded from logs, traces, analytics, and error reports.
* Interactive control is enabled only for authorized users.

For all exported components and the local playground, see the
[`@mobilerun/react` package repository](https://github.com/droidrun/mobilerun-react).
