React Native Integration Guide with Streaming API + LiveKit
This guide demonstrates how to integrate HeyGen's Streaming API with LiveKit in a React Native/Expo application to create real-time avatar streaming experiences.
Quick Start
Create a new Expo project and install necessary dependencies:
# Create new Expo project
bunx create-expo-app heygen-livekit-demo
cd heygen-livekit-demo
# Install dependencies
bunx expo install @livekit/react-native react-native-webrtc react-native-safe-area-context
bun install -D @config-plugins/react-native-webrtc
Step-by-Step Implementation
1. Project Configuration
Update app.json
for necessary permissions and plugins:
{
"expo": {
"plugins": [
"@config-plugins/react-native-webrtc",
[
"expo-build-properties",
{
"ios": {
"useFrameworks": "static"
}
}
]
],
"ios": {
"bitcode": false,
"infoPlist": {
"NSCameraUsageDescription": "For streaming video",
"NSMicrophoneUsageDescription": "For streaming audio"
}
},
"android": {
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
]
}
}
}
2. API Configuration
Create variables for API configuration:
const API_CONFIG = {
serverUrl: "https://api.heygen.com",
apiKey: "your_api_key_here",
};
3. State Management
Set up necessary state variables in your main component:
const [wsUrl, setWsUrl] = useState<string>("");
const [token, setToken] = useState<string>("");
const [sessionToken, setSessionToken] = useState<string>("");
const [sessionId, setSessionId] = useState<string>("");
const [connected, setConnected] = useState(false);
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const [speaking, setSpeaking] = useState(false);
4. Session Creation Flow
4.1 Create Session Token
const getSessionToken = async () => {
const response = await fetch(
`${API_CONFIG.serverUrl}/v1/streaming.create_token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_CONFIG.apiKey}`,
},
}
);
const data = await response.json();
return data.data.session_token;
};
4.2 Create New Session
const createNewSession = async (sessionToken: string) => {
const response = await fetch(`${API_CONFIG.serverUrl}/v1/streaming.new`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
quality: "high",
version: "v2",
video_encoding: "H264",
}),
});
const data = await response.json();
return data.data;
};
4.3 Start Streaming Session
const startStreamingSession = async (
sessionId: string,
sessionToken: string
) => {
const response = await fetch(`${API_CONFIG.serverUrl}/v1/streaming.start`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
session_token: sessionToken,
silence_response: "false",
stt_language: "en",
}),
});
const data = await response.json();
return data.data;
};
5. LiveKit Room Setup
Embed LiveKit’s LiveKitRoom
in your component:
<LiveKitRoom
serverUrl={wsUrl}
token={token}
connect={true}
options={{
adaptiveStream: { pixelDensity: "screen" },
}}
audio={false}
video={false}
>
<RoomView
onSendText={sendText}
text={text}
onTextChange={setText}
speaking={speaking}
onClose={closeSession}
loading={loading}
/>
</LiveKitRoom>
6. Video Track Component
Implement a RoomView
component to display video streams:
const RoomView = ({
onSendText,
text,
onTextChange,
speaking,
onClose,
loading,
}: RoomViewProps) => {
const tracks = useTracks([Track.Source.Camera], { onlySubscribed: true });
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
>
<View style={styles.videoContainer}>
{tracks.map((track, idx) =>
isTrackReference(track) ? (
<VideoTrack
key={idx}
style={styles.videoView}
trackRef={track}
objectFit="contain"
/>
) : null
)}
</View>
{/* Controls */}
</KeyboardAvoidingView>
</SafeAreaView>
);
};
7. Send Text to Avatar
const sendText = async () => {
try {
setSpeaking(true);
const response = await fetch(`${API_CONFIG.serverUrl}/v1/streaming.task`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
text: text,
task_type: "talk",
}),
});
const data = await response.json();
setText("");
} catch (error) {
console.error("Error sending text:", error);
} finally {
setSpeaking(false);
}
};
8. Close Session
const closeSession = async () => {
try {
setLoading(true);
const response = await fetch(`${API_CONFIG.serverUrl}/v1/streaming.stop`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
}),
});
// Reset states
setConnected(false);
setSessionId("");
setSessionToken("");
setWsUrl("");
setToken("");
setText("");
setSpeaking(false);
} catch (error) {
console.error("Error closing session:", error);
} finally {
setLoading(false);
}
};
Running the App
# Install dependencies
bun install
# Create development build
expo prebuild
# Run on iOS
expo run:ios
# or with physical device
expo run:ios --device
# Run on Android
expo run:android
Use React Native Debugger for network inspection
Note: Use physical devices or simulators for WebRTC support. You can't use Expo Go with WebRTC.
System Flow
- Session setup (steps 1-3)
- Video streaming (step 4)
- Avatar interaction loop (step 5)
- Session closure (step 6)
Complete Demo Code
For a full example, check the complete code in App.tsx
:
import { useEffect, useState } from "react";
import {
StyleSheet,
View,
TextInput,
Text,
KeyboardAvoidingView,
Platform,
SafeAreaView,
TouchableOpacity,
Pressable,
} from "react-native";
import { registerGlobals } from "@livekit/react-native";
import {
LiveKitRoom,
AudioSession,
VideoTrack,
useTracks,
isTrackReference,
} from "@livekit/react-native";
import { Track } from "livekit-client";
registerGlobals();
const API_CONFIG = {
apiKey: "apikey",
serverUrl: "https://api.heygen.com",
};
export default function App() {
const [wsUrl, setWsUrl] = useState<string>("");
const [token, setToken] = useState<string>("");
const [sessionToken, setSessionToken] = useState<string>("");
const [sessionId, setSessionId] = useState<string>("");
const [connected, setConnected] = useState(false);
const [text, setText] = useState("");
const [webSocket, setWebSocket] = useState<WebSocket | null>(null);
const [loading, setLoading] = useState(false);
const [speaking, setSpeaking] = useState(false);
// Start audio session on app launch
useEffect(() => {
const setupAudio = async () => {
await AudioSession.startAudioSession();
};
setupAudio();
return () => {
AudioSession.stopAudioSession();
};
}, []);
const getSessionToken = async () => {
try {
const response = await fetch(
`${API_CONFIG.serverUrl}/v1/streaming.create_token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": API_CONFIG.apiKey,
},
}
);
const data = await response.json();
console.log("Session token obtained", data.data.token);
return data.data.token;
} catch (error) {
console.error("Error getting session token:", error);
throw error;
}
};
const startStreamingSession = async (
sessionId: string,
sessionToken: string
) => {
try {
console.log("Starting streaming session with:", {
sessionId,
sessionToken,
});
const startResponse = await fetch(
`${API_CONFIG.serverUrl}/v1/streaming.start`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
}),
}
);
const startData = await startResponse.json();
console.log("Streaming start response:", startData);
if (startData) {
setConnected(true);
return true;
}
return false;
} catch (error) {
console.error("Error starting streaming session:", error);
return false;
}
};
const createSession = async () => {
try {
setLoading(true);
// Get new session token
const newSessionToken = await getSessionToken();
setSessionToken(newSessionToken);
const response = await fetch(`${API_CONFIG.serverUrl}/v1/streaming.new`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${newSessionToken}`,
},
body: JSON.stringify({
quality: "high",
avatar_name: "",
voice: {
voice_id: "",
},
version: "v2",
video_encoding: "H264",
}),
});
const data = await response.json();
console.log("Streaming new response:", data.data);
if (data.data) {
const newSessionId = data.data.session_id;
// Set all session data
setSessionId(newSessionId);
setWsUrl(data.data.url);
setToken(data.data.access_token);
// Connect WebSocket
const params = new URLSearchParams({
session_id: newSessionId,
session_token: newSessionToken,
silence_response: "false",
// opening_text: "Hello from the mobile app!",
stt_language: "en",
});
const wsUrl = `wss://${
new URL(API_CONFIG.serverUrl).hostname
}/v1/ws/streaming.chat?${params}`;
const ws = new WebSocket(wsUrl);
setWebSocket(ws);
// Start streaming session with the new IDs
await startStreamingSession(newSessionId, newSessionToken);
}
} catch (error) {
console.error("Error creating session:", error);
} finally {
setLoading(false);
}
};
const sendText = async () => {
try {
setSpeaking(true);
// Send task request
const response = await fetch(
`${API_CONFIG.serverUrl}/v1/streaming.task`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
text: text,
task_type: "talk",
}),
}
);
const data = await response.json();
console.log("Task response:", data);
setText(""); // Clear input after sending
} catch (error) {
console.error("Error sending text:", error);
} finally {
setSpeaking(false);
}
};
const closeSession = async () => {
try {
setLoading(true);
if (!sessionId || !sessionToken) {
console.log("No active session");
return;
}
const response = await fetch(
`${API_CONFIG.serverUrl}/v1/streaming.stop`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
body: JSON.stringify({
session_id: sessionId,
}),
}
);
// Close WebSocket
if (webSocket) {
webSocket.close();
setWebSocket(null);
}
// Reset all states
setConnected(false);
setSessionId("");
setSessionToken("");
setWsUrl("");
setToken("");
setText("");
setSpeaking(false);
console.log("Session closed successfully");
} catch (error) {
console.error("Error closing session:", error);
} finally {
setLoading(false);
}
};
if (!connected) {
return (
<View style={styles.startContainer}>
<View style={styles.heroContainer}>
<Text style={styles.heroTitle}>HeyGen Streaming API + LiveKit</Text>
<Text style={styles.heroSubtitle}>React Native/Expo Demo</Text>
</View>
<Pressable
style={({ pressed }) => [
styles.startButton,
{ opacity: pressed ? 0.8 : 1 },
]}
onPress={createSession}
disabled={loading}
>
<Text style={styles.startButtonText}>
{loading ? "Starting..." : "Start Session"}
</Text>
</Pressable>
</View>
);
}
return (
<LiveKitRoom
serverUrl={wsUrl}
token={token}
connect={true}
options={{
adaptiveStream: { pixelDensity: "screen" },
}}
audio={false}
video={false}
>
<RoomView
onSendText={sendText}
text={text}
onTextChange={setText}
speaking={speaking}
onClose={closeSession}
loading={loading}
/>
</LiveKitRoom>
);
}
const RoomView = ({
onSendText,
text,
onTextChange,
speaking,
onClose,
loading,
}: {
onSendText: () => void;
text: string;
onTextChange: (text: string) => void;
speaking: boolean;
onClose: () => void;
loading: boolean;
}) => {
const tracks = useTracks([Track.Source.Camera], { onlySubscribed: true });
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.container}
>
<View style={styles.videoContainer}>
{tracks.map((track, idx) =>
isTrackReference(track) ? (
<VideoTrack
key={idx}
style={styles.videoView}
trackRef={track}
objectFit="contain"
/>
) : null
)}
</View>
<TouchableOpacity
style={[styles.closeButton, loading && styles.disabledButton]}
onPress={onClose}
disabled={loading}
>
<Text style={styles.closeButtonText}>
{loading ? "Closing..." : "Close Session"}
</Text>
</TouchableOpacity>
<View style={styles.controls}>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Enter text for avatar to speak"
placeholderTextColor="#666"
value={text}
onChangeText={onTextChange}
editable={!speaking && !loading}
/>
<TouchableOpacity
style={[
styles.sendButton,
(speaking || !text.trim() || loading) && styles.disabledButton,
]}
onPress={onSendText}
disabled={speaking || !text.trim() || loading}
>
<Text style={styles.sendButtonText}>
{speaking ? "Speaking..." : "Send"}
</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
startContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#fff",
padding: 20,
},
heroContainer: {
alignItems: "center",
marginBottom: 40,
},
heroTitle: {
fontSize: 22,
fontWeight: "700",
color: "#1a73e8",
marginBottom: 8,
textAlign: "center",
},
heroSubtitle: {
fontSize: 18,
color: "#666",
fontWeight: "500",
textAlign: "center",
},
startButton: {
backgroundColor: "#2196F3",
paddingHorizontal: 32,
paddingVertical: 16,
borderRadius: 30,
elevation: 3,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
startButtonText: {
color: "white",
fontSize: 18,
fontWeight: "600",
},
videoContainer: {
flex: 1,
position: "relative",
},
videoView: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
},
closeButton: {
position: "absolute",
top: 50,
right: 20,
backgroundColor: "#ff4444",
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 25,
zIndex: 1,
elevation: 3,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
closeButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
controls: {
width: "100%",
padding: 20,
borderTopWidth: 1,
borderColor: "#333",
// backgroundColor: "rgba(0, 0, 0, 0.75)",
},
inputContainer: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
input: {
flex: 1,
height: 50,
borderColor: "#333",
borderWidth: 1,
paddingHorizontal: 15,
borderRadius: 25,
backgroundColor: "rgba(255, 255, 255, 0.9)",
fontSize: 16,
color: "#000",
},
sendButton: {
backgroundColor: "#2196F3",
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 25,
elevation: 3,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
sendButtonText: {
color: "white",
fontSize: 16,
fontWeight: "600",
},
disabledButton: {
opacity: 0.5,
},
});
You can find the complete demo repository here.
Resources
Conclusion
You’ve now integrated HeyGen’s Streaming API with LiveKit in your React Native project. This setup enables real-time interactive avatar experiences with minimal effort.
Updated 15 days ago