Skip to main content

Expo Complete Starter App

This is a complete Expo starter for mediasfu-reactnative-expo@2.5.0. It opens a compact call, host dashboard, guest queue, classroom, or remote-podcast room and leaves authority, room policy, and MediaSFU credentials on your authenticated backend.

Before you start

Install Node.js 22.11 or newer and Expo SDK 57, then create the Expo application. Use a development build for native media functionality. The starter's token field is only an application session token for isolated development; replace it with your normal signed-in session before distribution.

Your backend must accept authenticated POST requests at /api/mediasfu/create-room and /api/mediasfu/join-room, apply room and role rules, and return the normal MediaSFU create-or-join response.

Complete example

package.json
{
"name": "my-mediasfu-expo-app",
"private": true,
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"check": "tsc --noEmit"
},
"dependencies": {
"@expo/metro-runtime": "57.0.8",
"@expo/vector-icons": "15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/slider": "5.2.0",
"@react-native-picker/picker": "2.11.4",
"expo": "57.0.9",
"expo-audio": "57.0.3",
"expo-camera": "57.0.3",
"expo-clipboard": "57.0.1",
"expo-dev-client": "57.0.10",
"expo-image-manipulator": "57.0.7",
"expo-image-picker": "57.0.7",
"expo-screen-orientation": "57.0.1",
"expo-splash-screen": "57.0.5",
"mediasfu-reactnative-expo": "2.5.0",
"mediasoup-client": "3.20.0",
"react": "19.2.3",
"react-native": "0.86.2",
"react-native-gesture-handler": "2.32.0",
"react-native-picker-select": "9.3.0",
"react-native-reanimated": "4.5.1",
"react-native-safe-area-context": "5.7.0",
"react-native-screens": "4.26.0",
"react-native-webrtc": "124.0.8",
"react-native-webrtc-web-shim": "1.0.7",
"react-native-worklets": "0.10.1",
"reanimated-color-picker": "4.2.0",
"socket.io-client": "4.8.0"
},
"devDependencies": {
"@types/react": "19.2.4",
"typescript": "6.0.3"
}
}
app.json
{
"expo": {
"name": "MediaSFU Starter",
"slug": "my-mediasfu-expo-app",
"version": "1.0.0",
"orientation": "default",
"plugins": ["mediasfu-reactnative-expo"],
"ios": { "bundleIdentifier": "com.example.mediasfustarter" },
"android": { "package": "com.example.mediasfustarter" }
}
}
tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"strict": true
}
}
room-backend.ts
import type { CreateRoomOnMediaSFUType, JoinRoomOnMediaSFUType } from 'mediasfu-reactnative-expo';

type Result = Awaited<ReturnType<CreateRoomOnMediaSFUType>>;
export type AppFetch = (url: string, init: { method: 'POST'; headers: { 'Content-Type': 'application/json'; Authorization: string }; body: string }) => Promise<{ ok: boolean; status: number; json(): Promise<unknown> }>;

function endpoint(baseUrl: string, path: string) {
const base = new URL(baseUrl);
if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash) throw new TypeError('Use an absolute HTTPS backend URL without embedded credentials');
return new URL(path, base.toString().endsWith('/') ? base : `${base}/`).toString();
}

async function post(fetchApp: AppFetch, url: string, appToken: string, payload: unknown): Promise<Result> {
if (!appToken.trim()) throw new Error('Sign in before creating or joining a room.');
const response = await fetchApp(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${appToken}` }, body: JSON.stringify(payload) });
if (!response.ok) throw new Error(`Room request failed (${response.status})`);
return await response.json() as Result;
}

export function createRoomBackend(fetchApp: AppFetch, baseUrl: string, appToken: string) {
return {
createMediaSFURoom: (async ({ payload }) => post(fetchApp, endpoint(baseUrl, 'api/mediasfu/create-room'), appToken, payload)) as CreateRoomOnMediaSFUType,
joinMediaSFURoom: (async ({ payload }) => post(fetchApp, endpoint(baseUrl, 'api/mediasfu/join-room'), appToken, payload)) as JoinRoomOnMediaSFUType,
};
}
CompactCall.tsx
import { useMemo, useState } from 'react';
import { ModernMediasfuGeneric, type MediasfuGenericOptions } from 'mediasfu-reactnative-expo';
import { createRoomBackend, type AppFetch } from './room-backend';

export function CompactCall({ backendBaseUrl, appToken, fetchApp }: { backendBaseUrl: string; appToken: string; fetchApp: AppFetch }) {
const [parameters, setParameters] = useState<Record<string, unknown>>({});
const room = useMemo(() => createRoomBackend(fetchApp, backendBaseUrl, appToken), [appToken, backendBaseUrl, fetchApp]);
const updateSourceParameters: NonNullable<MediasfuGenericOptions['updateSourceParameters']> = setParameters;
return <ModernMediasfuGeneric connectMediaSFU returnUI sourceParameters={parameters} updateSourceParameters={updateSourceParameters} {...room} />;
}
ScenarioRooms.tsx
import { useMemo, useState } from 'react';
import { MediasfuConference, MediasfuWebinar, type MediasfuConferenceOptions, type MediasfuWebinarOptions } from 'mediasfu-reactnative-expo';
import { createRoomBackend, type AppFetch } from './room-backend';

type RoomProps = { backendBaseUrl: string; appToken: string; fetchApp: AppFetch };

function useRoom(props: RoomProps) {
const [sourceParameters, updateSourceParameters] = useState<Record<string, unknown>>({});
const gateway = useMemo(() => createRoomBackend(props.fetchApp, props.backendBaseUrl, props.appToken), [props.appToken, props.backendBaseUrl, props.fetchApp]);
return { sourceParameters, updateSourceParameters, ...gateway };
}

export function HostDashboard(props: RoomProps) {
const room: MediasfuConferenceOptions = { connectMediaSFU: true, returnUI: true, ...useRoom(props) };
return <MediasfuConference {...room} />;
}

export function LiveGuestQueue(props: RoomProps) {
const room: MediasfuWebinarOptions = { connectMediaSFU: true, returnUI: true, ...useRoom(props) };
return <MediasfuWebinar {...room} />;
}

export function ClassroomWorkshop(props: RoomProps) {
const room: MediasfuConferenceOptions = { connectMediaSFU: true, returnUI: true, ...useRoom(props) };
return <MediasfuConference {...room} />;
}

export function RemotePodcast(props: RoomProps) {
const room: MediasfuConferenceOptions = { connectMediaSFU: true, returnUI: true, ...useRoom(props) };
return <MediasfuConference {...room} />;
}
App.tsx
import { useState } from 'react';
import { Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
import { CompactCall } from './CompactCall';
import { ClassroomWorkshop, HostDashboard, LiveGuestQueue, RemotePodcast } from './ScenarioRooms';

type TutorialScenario = 'compact-call' | 'host-dashboard' | 'live-guest-queue' | 'classroom-workshop' | 'remote-podcast';

const labels: Record<TutorialScenario, string> = {
'compact-call': 'Compact call',
'host-dashboard': 'Host dashboard',
'live-guest-queue': 'Live guest queue',
'classroom-workshop': 'Classroom workshop',
'remote-podcast': 'Remote podcast',
};

type Config = { backendBaseUrl: string; appToken: string; fetchApp: typeof fetch };

function Room({ scenario, config }: { scenario: TutorialScenario; config: Config }) {
if (scenario === 'compact-call') return <CompactCall {...config} />;
if (scenario === 'live-guest-queue') return <LiveGuestQueue {...config} />;
if (scenario === 'host-dashboard') return <HostDashboard {...config} />;
if (scenario === 'classroom-workshop') return <ClassroomWorkshop {...config} />;
return <RemotePodcast {...config} />;
}

export default function App() {
const [scenario, setScenario] = useState<TutorialScenario>('compact-call');
const [backendBaseUrl, setBackendBaseUrl] = useState('');
const [appToken, setAppToken] = useState('');
const ready = backendBaseUrl.startsWith('https://') && appToken.trim().length > 0;
if (ready) return <Room scenario={scenario} config={{ backendBaseUrl, appToken, fetchApp: fetch }} />;

return <SafeAreaView style={styles.page}><ScrollView contentContainerStyle={styles.content}>
<Text style={styles.title}>MediaSFU Expo starter</Text>
<Text style={styles.copy}>Choose an outcome, then enter the HTTPS address of your signed-in app backend and its short-lived app session token. These are not MediaSFU Cloud credentials.</Text>
<View style={styles.choices}>{(Object.keys(labels) as TutorialScenario[]).map((value) => <Pressable key={value} onPress={() => setScenario(value)} style={[styles.choice, scenario === value && styles.selected]}><Text>{labels[value]}</Text></Pressable>)}</View>
<TextInput value={backendBaseUrl} onChangeText={setBackendBaseUrl} autoCapitalize="none" keyboardType="url" placeholder="https://your-app.example" style={styles.input} />
<TextInput value={appToken} onChangeText={setAppToken} autoCapitalize="none" autoCorrect={false} secureTextEntry placeholder="Short-lived app session token" style={styles.input} />
<Text style={styles.hint}>Replace this temporary app-token screen with your normal signed-in session before release. Do not put a MediaSFU API key in this app.</Text>
</ScrollView></SafeAreaView>;
}

const styles = StyleSheet.create({ page: { flex: 1 }, content: { gap: 12, padding: 20 }, title: { fontSize: 24, fontWeight: '700' }, copy: { lineHeight: 21 }, choices: { gap: 8 }, choice: { borderColor: '#9ca3af', borderWidth: 1, borderRadius: 8, padding: 12 }, selected: { borderColor: '#0284c7', backgroundColor: '#e0f2fe' }, input: { borderColor: '#9ca3af', borderWidth: 1, borderRadius: 8, padding: 12 }, hint: { color: '#374151', lineHeight: 20 } });

Run

npx create-expo-app@3.5.0 my-mediasfu-expo-app --template blank-typescript
cd my-mediasfu-expo-app
npm install
npm run android

Replace the generated App.tsx, app.json, package.json, and tsconfig.json, then add the three TypeScript files beside App.tsx. npm run ios requires a Mac. Because the MediaSFU package uses native media modules, use expo run:android or expo run:ios, not Expo Go.

What you should see

Choose an outcome on the first screen, enter application authority, and open the room after the backend accepts the request. Compact call uses the supplied generic room, guest queue uses the supplied webinar room, and the other choices use the supplied conference room.

Release checklist

  • Set your own unique ios.bundleIdentifier and android.package in app.json before building.
  • Replace the temporary app-token field with the app's normal signed-in session.
  • Test create, join, local media, remote audio, leave, and authorized room end on physical devices.