Stream live video from the glasses camera using session.camera.startStream(). One method, three modes - managed relay (default), relay with restreaming, or direct to your own server.

Quick Start

Pick the option that matches what you want to do:

Stream with viewer URLs (managed relay)

The default. Glasses stream to the MentraOS cloud relay, which handles quality normalization, reconnection, and provides playback URLs.
const stream = await session.camera.startStream();
console.log("Watch at:", stream.webrtcUrl);  // sub-second latency
console.log("HLS:", stream.hlsUrl);

// Stop when done
await session.camera.stopStream();

Stream to YouTube, Twitch, etc.

Stream through the managed relay and fan out to external platforms. More reliable than streaming directly from the glasses because the relay handles quality and reconnection.
const stream = await session.camera.startStream({
  destinations: [
    "rtmp://a.rtmp.youtube.com/live2/YOUR-STREAM-KEY",
    "rtmp://live.twitch.tv/app/YOUR-STREAM-KEY",
  ],
});

console.log("HLS:", stream.hlsUrl);

await session.camera.stopStream();

Stream directly to your own server

Glasses connect straight to your URL. No cloud relay in the middle. Supports SRT, RTMP, RTMPS, and WHIP protocols.
await session.camera.startStream({
  direct: "srt://192.168.1.100:4201?mode=caller",
});

// Stop when done
await session.camera.stopStream();
Direct streaming is for developers who have their own infrastructure, want to record locally, or need the absolute lowest latency. It does not provide viewer URLs.

Which should I use?

I want to…Use
Get WebRTC/HLS viewer URLs for my appstartStream() (default)
Go live on YouTube/TwitchstartStream({ destinations: [...] })
Stream to my own local serverstartStream({ direct: "srt://..." })
Record video locally for testingstartStream({ direct: "srt://..." }) + FFmpeg
Process video with CV/AIstartStream() then consume the WebRTC URL

Managed Streaming

When you call startStream() without direct, the glasses stream to the MentraOS cloud relay. The relay provides:
  • WebRTC playback with sub-second latency (default)
  • HLS/DASH playback when destinations are provided
  • Quality normalization so upstream services accept the stream
  • Reconnection handling if the glasses have a brief network blip
  • Multi-region servers for lower latency to the relay
  • Fan-out to multiple destinations from a single stream

Stream result

const stream = await session.camera.startStream();

stream.hlsUrl;        // HLS playback URL
stream.dashUrl;       // DASH playback URL
stream.webrtcUrl;     // WebRTC playback URL (sub-second latency)
stream.previewUrl;    // Hosted player page
stream.thumbnailUrl;  // Stream thumbnail
stream.streamId;      // Unique stream identifier

Quality options

const stream = await session.camera.startStream({
  quality: "1080p",  // "720p" or "1080p"
});

Video and audio configuration

Use frameRate (not fps) for the SDK field name.

Supported video parameters

When you set video, each field you pass must be an integer within these ranges (matching glasses-side parsing). Omitted fields use glasses defaults.
  • width: 320–1920 (default 854)
  • height: 240–1080 (default 480)
  • frameRate: 10–60 fps (default 15)
  • bitrate: 100_000–10_000_000 bits per second (default 1_000_000)
Behavior: The glasses pick a native camera mode and may center-crop or downscale to your requested size without upscaling. If the camera cannot reach the resolution without upscale, stream start is rejected on the device. Out-of-range video values cause the SDK to throw RangeError before any request is sent. Portrait resolutions are not yet recommended on Mentra Live.
const stream = await session.camera.startStream({
  video: {
    width: 1920,
    height: 1080,
    frameRate: 30,
    bitrate: 4000000,
  },
  audio: {
    sampleRate: 44100,
    bitrate: 128000,
  },
});

Direct Streaming

When you pass direct, the glasses connect straight to the URL you provide. The cloud relay is not used. No viewer URLs are returned.
await session.camera.startStream({
  direct: "srt://192.168.1.100:4201?mode=caller",
});

Supported protocols

ProtocolURL formatNotes
SRTsrt://host:port?mode=callerRecommended. Low latency, handles packet loss well.
RTMPrtmp://host/app/keyWidely supported. Higher latency than SRT.
RTMPSrtmps://host/app/keyRTMP over TLS.
WHIPhttps://host/whip/endpointWebRTC ingest.

Recording locally with FFmpeg

Start FFmpeg as an SRT listener on your computer:
ffmpeg -i "srt://0.0.0.0:4201?mode=listener" -c copy recording.mp4
Then start the stream from your app:
await session.camera.startStream({
  direct: "srt://YOUR_COMPUTER_IP:4201?mode=caller",
});
The glasses connect to FFmpeg, which saves the video as an MP4. Press Ctrl+C in FFmpeg when done.

Stream Status

Monitor the stream status regardless of mode (managed or direct):
const cleanup = session.camera.onStreamStatus((status) => {
  console.log("Stream status:", status);
});

// Stop listening
cleanup();
Status values include "initializing", "active", "stopped", and "error". Always subscribe to status events before starting a stream. For managed streams, don’t use playback URLs until status reports "active".
session.camera.onStreamStatus((status) => {
  if (status.status === "active") {
    console.log("Stream is live!");
  } else if (status.status === "error") {
    console.error("Stream error:", status.message);
  }
});

const stream = await session.camera.startStream();

Stopping a Stream

stopStream() stops whichever mode is active (managed or direct):
await session.camera.stopStream();
You can also check if a stream is active:
if (session.camera.isCurrentlyStreaming()) {
  await session.camera.stopStream();
}

Permissions

Your app needs the camera permission to stream video. Add it in the Developer Console when creating or editing your app.

Sound

By default, the glasses play a sound when streaming starts and stops (privacy indicator for bystanders). You can control the sound:
await session.camera.startStream({ sound: false });
The LED flash is always on during streaming and cannot be disabled (hardware privacy indicator).

Checking for Existing Streams

If your app crashes or a session disconnects unexpectedly, the glasses may still have an active stream running. Use checkExistingStream() to detect and adopt orphaned streams from previous sessions.
const existing = await session.camera.checkExistingStream();

if (existing.hasActiveStream) {
  session.logger.info("Found existing stream:", existing.streamInfo.streamId);
  session.logger.info("Status:", existing.streamInfo.status);

  // Access viewer URLs if it's a managed stream
  if (existing.streamInfo.hlsUrl) {
    session.logger.info("HLS:", existing.streamInfo.hlsUrl);
  }
  if (existing.streamInfo.webrtcUrl) {
    session.logger.info("WebRTC:", existing.streamInfo.webrtcUrl);
  }
}

Return type

checkExistingStream() returns Promise<ExistingStreamInfo>:
interface ExistingStreamInfo {
  hasActiveStream: boolean;
  streamInfo?: {
    type: "managed" | "direct";
    streamId: string;
    status: string;
    hlsUrl?: string;
    dashUrl?: string;
    webrtcUrl?: string;
    previewUrl?: string;
    thumbnailUrl?: string;
  };
}

Typical usage at session start

app.onSession(async (session: MentraSession) => {
  const existing = await session.camera.checkExistingStream();

  if (existing.hasActiveStream) {
    session.logger.info("Adopting existing stream:", existing.streamInfo.streamId);
    // Reuse the stream instead of starting a new one
  } else {
    const stream = await session.camera.startStream();
    session.logger.info("Started new stream:", stream.streamId);
  }
});

Complete Example

import { MiniAppServer, type MentraSession } from "@mentra/sdk";

const app = new MiniAppServer({
  packageName: "com.example.streamer",
  apiKey: process.env.API_KEY!,
  port: 3000,
});

app.onSession(async (session: MentraSession) => {
  session.logger.info("Starting stream...");

  session.camera.onStreamStatus((status) => {
    session.logger.info(`Stream: ${status.status}`);
  });

  try {
    const stream = await session.camera.startStream();
    session.logger.info(`Live! ${stream.webrtcUrl}`);
  } catch (error) {
    session.logger.info(`Error: ${error}`);
  }

  session.onStopped(async () => {
    await session.camera.stopStream();
  });
});

await app.start();

Migrating from v2

// v2 - two separate methods
await session.camera.startLocalLivestream({ streamUrl: "srt://..." });
await session.camera.stopLocalLivestream();
const urls = await session.camera.startLivestream({ restreamDestinations: [...] });
await session.camera.stopLivestream();

// v3 - one method with modes
await session.camera.startStream({ direct: "srt://..." });
await session.camera.stopStream();
const urls = await session.camera.startStream({ destinations: [...] });
await session.camera.stopStream();
The v2 methods (startLocalLivestream, startLivestream, stopLocalLivestream, stopLivestream) still work via the compatibility layer. See the Migration Guide for the full list of changes.