Skip to main content
Version: 2.x.x

Quick Start for Http Live Streaming(HLS) in React Native

This guide adds HTTP Live Streaming (HLS) to a React Native app.

info

For low-latency interactive live streaming (under 100ms), follow this documentation.

Prerequisites​

Before proceeding, ensure that your development environment meets the following requirements:

  • Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
  • Basic understanding of React Native
  • Node.js >= 22.11.0
  • React Native 0.74 or later
  • NPM v10+ (comes installed with newer Node versions)
  • Android Studio or Xcode installed
  • For Android, a device or emulator running Android 7.0 (API level 24) or later.
  • For iOS, an iPhone or iPad running iOS 18.0 or later.
important

You need a VideoSDK account to generate a token. Generate one from the VideoSDK dashboard.

App Architecture​

The App will contain the following screens:

  1. Join Screen : This screen allows the SPEAKER to create a studio or join a predefined studio, and SIGNALLING_ONLY to join a predefined studio.

  2. Speaker Screen : This screen contains the list of speakers and studio controls, such as enabling/disabling the microphone and camera, and leaving the studio.

  3. Viewer Screen : This screen includes a live stream player where viewers can watch the stream.

Getting Started with the Code!​

Create App​

Create a new React Native App using the below command.

npx @react-native-community/cli init AppName

For React Native setup, you can follow the Official Documentation.

Install VideoSDK and Dependencies​

Install the VideoSDK along with @react-native-clipboard/clipboard (used to copy the room id) and react-native-safe-area-context (used to render the app within the device's safe area). Ensure that you are in your project directory before running this command.

npm install "@videosdk.live/react-native-sdk@2.0.0" "@react-native-clipboard/clipboard" "react-native-safe-area-context"

Project Structure​

Directory Structure
root
├── node_modules
├── android
├── ios
├── App.js
├── api.js
├── index.js

Project Configuration​

Android Setup​

android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
</manifest>
note

React Native links the SDK automatically, so settings.gradle and MainApplication need no changes.

android/app/proguard-rules.pro
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**

Keep both lines. The -keep rule alone fails the R8 step with a missing-class error.

android/build.gradle
buildscript {
ext {
minSdkVersion = 24
}
}

iOS Setup​

To update CocoaPods, you can reinstall the gem using the following command:

sudo gem install cocoapods

Add the VideoSDKRTC pod inside your app's target block in the Podfile, and raise the platform to iOS 18.0.

ios/Podfile
platform :ios, '18.0'

target 'AppName' do
# ...
pod 'VideoSDKRTC', :podspec => '../node_modules/@videosdk.live/react-native-sdk/ios/VideoSDKRTC.podspec'
end
note

Set the iOS Deployment Target of your app target to 18.0 in Xcode as well. The SDK's own pod is linked automatically.

You need to install the pods by running the following command:

pod install

Add the following lines to your info.plist file located at (project folder/ios/projectname/info.plist):

ios/projectname/info.plist
<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>

Step 1: Get started with API.js​

Prior to moving on, you must create an API request to generate a unique roomId. You will need an authentication token, which you can create either through the videosdk-rtc-api-server-examples or directly from the VideoSDK Dashboard for developers.

api.js
// Generate your token at https://app.videosdk.live/api-keys and paste it below.
export const authToken = "";

// API call to create room
export const createRoom = async ({ token }) => {
try {
const res = await fetch(`https://api.videosdk.live/v2/rooms`, {
method: "POST",
headers: {
authorization: `${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});

if (!res.ok) {
throw new Error(`Failed to create room: ${res.status}`);
}

//Destructuring the roomId from the response
const { roomId } = await res.json();
return roomId;
} catch (error) {
console.error("createRoom failed", error);
throw error;
}
};
caution

This quick start pastes a temporary token from the VideoSDK Dashboard into the client and lets the user choose the mode. Use this setup only for development. In production, your client must obtain its token from your backend server, and your server must decide whether a participant joins as a host or as an audience member. A token cannot restrict the mode, so any client that holds a valid token can join in SEND_AND_RECV mode. See Authentication and Tokens.

Step 2: Wireframe App.js with all the components​

To build the wireframe of App.js, you need to use VideoSDK Hooks and Context Providers. VideoSDK provides RoomProvider, RoomConsumer, useRoom and useParticipant hooks.

First you need to understand Context Provider and Consumer. Context is primarily used when some data needs to be accessible by many components at different nesting levels.

  • RoomProvider: This is the Context Provider. It accepts config and token as props, and passes the room to every component inside it. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.
  • RoomConsumer: This is the Context Consumer. All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.
  • useRoom: This is the room hook API. It includes all the information related to room such as join, leave, toggle mic or webcam etc.
  • useParticipant: This is the participant hook API. It is responsible for handling all the events and props related to one particular participant such as name, webcamStream, micStream etc.

The methods of these hooks are asynchronous and return a Promise, so await them and handle failures with try / catch.

The Room Context provides a way to listen for any changes that occur when a participant joins the room or makes modifications to their microphone, camera, and other settings.

Begin by making a few changes to the code in the App.js file.

App.js
import React, { useState, useMemo, useRef, useEffect } from "react";
import {
TouchableOpacity,
Text,
TextInput,
View,
FlatList,
} from "react-native";
import {
RoomProvider,
useRoom,
useParticipant,
useMediaDevice,
MediaStream,
RTCView,
Constants,
} from "@videosdk.live/react-native-sdk";
import Clipboard from "@react-native-clipboard/clipboard";
import { createRoom, authToken } from "./api";
import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";

// Responsible for either scheduling a new room or joining an existing one as a host or as a viewer.
function JoinScreen({ getRoomAndToken, setMode }) {
return null;
}

// Responsible for managing participant video stream
function ParticipantView(props) {
return null;
}

// Responsible for managing room controls such as toggle mic / webcam and leave
function Controls() {
return null;
}

// Responsible for Speaker side view, which contains Room Controls(toggle mic/webcam & leave) and Participant list
function SpeakerView() {
return null;
}

// Responsible for Viewer side view, which contains video player for streaming HLS and managing HLS state (HLS_STARTED, HLS_STOPPING, HLS_STARTING, etc.)
function ViewerView() {
return null;
}

// Responsible for managing two views (Speaker & Viewer) based on provided mode (`SEND_AND_RECV` & `SIGNALLING_ONLY`)
function Container(props) {
return null;
}

function AppContent() {
const [roomId, setRoomId] = useState(null);

//State to handle the mode of the participant i.e. SEND_AND_RECV or SIGNALLING_ONLY
const [mode, setMode] = useState(
/** @type {"SEND_AND_RECV" | "SIGNALLING_ONLY"} */ ("SEND_AND_RECV")
);

//Getting RoomId from the API created earlier
const getRoomAndToken = async (id) => {
if (!authToken) {
console.error("PLEASE PROVIDE TOKEN IN api.js FROM app.videosdk.live");
return;
}
const roomId =
id == null ? await createRoom({ token: authToken }) : id;
setRoomId(roomId);
};

return authToken && roomId ? (
<RoomProvider
config={{
roomId,
micEnabled: true,
camEnabled: true,
participantName: "C.V. Raman",
//This will be the mode of the participant SEND_AND_RECV or SIGNALLING_ONLY
mode: mode,
defaultCamera: "front",
}}
token={authToken}
>
<Container mode={mode} />
</RoomProvider>
) : (
<JoinScreen getRoomAndToken={getRoomAndToken} setMode={setMode} />
);
}

function App() {
return (
<SafeAreaProvider>
<AppContent />
</SafeAreaProvider>
);
}

export default App;

Step 3: Implement Join Screen​

The join screen lets the user either create a new room or join an existing one as a host or as a viewer.

This functionality will have 3 buttons:

1. Join as Host: When this button is clicked, the person will join the room with the entered roomId as a host (SEND_AND_RECV).

2. Join as Viewer: When this button is clicked, the person will join the room with the entered roomId as SIGNALLING_ONLY.

3. Create Studio Room: When this button is clicked, the person will join a new room as a host (SEND_AND_RECV).

On Android, the SDK does not ask for camera and microphone access, so both host buttons first ask with requestPermission() from useMediaDevice.

JoinScreen Component
function JoinScreen({ getRoomAndToken, setMode }) {
const [roomVal, setRoomVal] = useState("");
const { requestPermission } = useMediaDevice();

// On Android, the SDK does not ask for camera and microphone access,
// so ask before joining as a host
const joinAsHost = async (roomId) => {
await requestPermission(Constants.permission.AUDIO_AND_VIDEO);
setMode("SEND_AND_RECV");
getRoomAndToken(roomId);
};

const JoinButton = ({ value, onPress }) => {
return (
<TouchableOpacity
style={{
backgroundColor: "#1178F8",
padding: 12,
marginVertical: 8,
borderRadius: 6,
}}
onPress={onPress}
>
<Text style={{ color: "white", alignSelf: "center", fontSize: 18 }}>
{value}
</Text>
</TouchableOpacity>
);
};
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: "black",
justifyContent: "center",
paddingHorizontal: 6 * 10,
}}
>
<TextInput
value={roomVal}
onChangeText={setRoomVal}
placeholder={"XXXX-XXXX-XXXX"}
placeholderTextColor={"grey"}
style={{
padding: 12,
borderWidth: 1,
borderColor: "white",
borderRadius: 6,
color: "white",
marginBottom: 16,
}}
/>
<JoinButton
onPress={() => {
joinAsHost(roomVal);
}}
value={"Join as Host"}
/>
<JoinButton
onPress={() => {
setMode("SIGNALLING_ONLY");
getRoomAndToken(roomVal);
}}
value={"Join as Viewer"}
/>
<Text
style={{
alignSelf: "center",
fontSize: 22,
marginVertical: 16,
fontStyle: "italic",
color: "grey",
}}
>
---------- OR ----------
</Text>

<JoinButton
onPress={() => {
joinAsHost();
}}
value={"Create Studio Room"}
/>
</SafeAreaView>
);
}

Output​

Step 4: Implement Container Component​

Next step is to create a container to manage Join screen, SpeakerView and ViewerView components based on the mode.

After the participant joins, use the mode chosen on the join screen: if it is SEND_AND_RECV, display the SpeakerView component, otherwise show the ViewerView component.

note

Do not call join() from a mount effect inside RoomProvider. It fails with 4002 INVALID_TOKEN even when the token is valid, and a click on a previous screen does not make a mount effect safe. Call join() from a user action inside RoomProvider, or set joinWithoutUserInteraction.

Use exactly one of these two — setting the flag and calling join() together can create two room instances.

  • There is a button inside RoomProvider. Call join() from its click handler: from the event, never from an effect.
  • There is no button, because the user already acted on an earlier screen — a device check, a room link, a button elsewhere in your app. Set joinWithoutUserInteraction to true as a prop on RoomProvider, not a key inside config, and do not call join() at all.

With the flag set, the SDK makes the join() call itself, so there is no promise of yours to wrap in try / catch — the error handling in the sample does not apply. Subscribe to onError from useRoom to catch a failed join and read its code. For a host, ask for camera and microphone access with requestPermission() before you render RoomProvider, because on Android the SDK does not ask for it.

Container Component
function Container({ mode }) {
const [joined, setJoined] = useState(false);
const { join } = useRoom({
onRoomJoined: () => setJoined(true),
onRoomLeft: () => setJoined(false),
onError: (error) => {
console.log(error.message);
},
});

return (
<View style={{ flex: 1 }}>
{joined ? (
mode === Constants.Mode.SEND_AND_RECV ? <SpeakerView /> : <ViewerView />
) : (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "black",
}}
>
<Text style={{ fontSize: 20, color: "white" }}>
Press Join button to enter studio.
</Text>
<Button
btnStyle={{
marginTop: 8,
paddingHorizontal: 22,
padding: 12,
borderWidth: 1,
borderColor: "white",
borderRadius: 8,
}}
buttonText={"Join"}
onPress={async () => {
try {
await join();
} catch (error) {
console.error("Failed to join room", error);
}
}}
/>
</View>
)}
</View>
);
}

// Common Component which will also be used in Controls Component
const Button = ({ onPress, buttonText, backgroundColor = "transparent", btnStyle = {} }) => {
return (
<TouchableOpacity
onPress={onPress}
style={{
...btnStyle,
backgroundColor: backgroundColor,
padding: 10,
borderRadius: 8,
}}
>
<Text style={{ color: "white", fontSize: 12 }}>{buttonText}</Text>
</TouchableOpacity>
);
};

Output​

Step 5: Implement SpeakerView​

Next step is to create SpeakerView and Controls components to manage features such as join, leave, mute and unmute.

  1. Retrieve all the participants using the useRoom hook and render a ParticipantView for each. Viewers join in SIGNALLING_ONLY mode and publish no audio or video, so ParticipantView shows only participants whose camera or microphone is on.
SpeakerView
function SpeakerView() {
// Get the Participant Map and roomId
const { roomId, participants } = useRoom({});

const participantIds = [...participants.keys()];

return (
<SafeAreaView style={{ backgroundColor: "black", flex: 1 }}>
{/* Render Header for copy roomId and leave room*/}
<HeaderView />

{/* Render Participant List */}
{participantIds.length > 0 ? (
<FlatList
data={participantIds}
renderItem={({ item }) => {
return <ParticipantView participantId={item} />;
}}
/>
) : null}

{/* Render Controls */}
<Controls />
</SafeAreaView>
);
}

function HeaderView() {
const { roomId, leave } = useRoom();
return (
<View
style={{
flexDirection: "row",
marginTop: 12,
justifyContent: "space-evenly",
alignItems: "center",
}}
>
<Text style={{ fontSize: 24, color: "white" }}>{roomId}</Text>
<Button
btnStyle={{
borderWidth: 1,
borderColor: "white",
}}
onPress={() => {
Clipboard.setString(roomId);
alert("RoomId copied successfully");
}}
buttonText={"Copy RoomId"}
backgroundColor={"transparent"}
/>
<Button
onPress={async () => {
try {
await leave();
} catch (error) {
console.error("Failed to leave room", error);
}
}}
buttonText={"Leave"}
backgroundColor={"#FF0000"}
/>
</View>
);
}

function Container({ mode }) {
const [joined, setJoined] = useState(false);

const mRoom = useRoom({
onRoomJoined: async () => {
setJoined(true);
// Pin the local participant if they join in SEND_AND_RECV mode
if (mode === Constants.Mode.SEND_AND_RECV) {
try {
await mRoomRef.current.localParticipant.pin("SHARE_AND_CAM");
} catch (error) {
console.error("Failed to pin the local participant", error);
}
}
},
onRoomLeft: () => setJoined(false),
onError: (error) => {
console.log(error.message);
},
});
const { join } = mRoom;

// Create a ref to room object so that when used inside the
// Callback functions, room state is maintained
const mRoomRef = useRef(mRoom);
useEffect(() => {
mRoomRef.current = mRoom;
}, [mRoom]);

return <>...</>;
}
  1. You need to then create the ParticipantView to display the participant's video. Render the webcamStream from the useParticipant hook in an RTCView. The SDK plays remote audio for you, so you do not need to render micStream.
ParticipantView
function ParticipantView({ participantId }) {
const { webcamStream, webcamOn, micOn } = useParticipant(participantId);

// Viewers publish no audio or video, so show only speakers
if (!webcamOn && !micOn) {
return null;
}

return webcamOn && webcamStream ? (
<RTCView
streamURL={new MediaStream([webcamStream.track]).toURL()}
objectFit={"cover"}
style={{
height: 300,
marginVertical: 8,
marginHorizontal: 8,
}}
/>
) : (
<View
style={{
backgroundColor: "grey",
height: 300,
justifyContent: "center",
alignItems: "center",
marginVertical: 8,
marginHorizontal: 8,
}}
>
<Text style={{ fontSize: 16 }}>NO MEDIA</Text>
</View>
);
}
  1. You have to add the Controls component which will allow the speaker to toggle their media and start/stop HLS.
Controls Component
function Controls() {
const {
toggleCam,
toggleMic,
startHLS,
stopHLS,
hlsState,
} = useRoom({});

const _handleHLS = async () => {
try {
if (!hlsState || hlsState === "HLS_STOPPED") {
await startHLS({
layout: {
type: "GRID",
priority: "PIN",
gridSize: 4,
},
theme: "DARK",
mode: "video-and-audio",
quality: "high",
orientation: "portrait",
});
} else if (hlsState === "HLS_STARTED" || hlsState === "HLS_PLAYABLE") {
await stopHLS();
}
} catch (error) {
console.error("Failed to toggle HLS", error);
}
};

return (
<View
style={{
padding: 24,
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Button
onPress={async () => {
try {
await toggleCam();
} catch (error) {
console.error("Failed to toggle webcam", error);
}
}}
buttonText={"Toggle Webcam"}
backgroundColor={"#1178F8"}
/>
<Button
onPress={async () => {
try {
await toggleMic();
} catch (error) {
console.error("Failed to toggle mic", error);
}
}}
buttonText={"Toggle Mic"}
backgroundColor={"#1178F8"}
/>
{hlsState === "HLS_STARTED" ||
hlsState === "HLS_STOPPING" ||
hlsState === "HLS_STARTING" ||
hlsState === "HLS_PLAYABLE" ? (
<Button
onPress={() => {
_handleHLS();
}}
buttonText={
hlsState === "HLS_STARTED"
? `Live Starting`
: hlsState === "HLS_STOPPING"
? `Live Stopping`
: hlsState === "HLS_PLAYABLE"
? `Stop Live`
: `Loading...`
}
backgroundColor={"#FF5D5D"}
/>
) : (
<Button
onPress={() => {
_handleHLS();
}}
buttonText={`Go Live`}
backgroundColor={"#1178F8"}
/>
)}
</View>
);
}

Output Of SpeakerView Component​

Step 6: Implement ViewerView​

When a host (SEND_AND_RECV mode participant) starts the live stream, viewers can watch it.

To implement the player view, you have to use react-native-video. It will be helpful for playing the HLS stream.

Begin by adding this package.

npm install react-native-video

With react-native-video installed, you can now get the hlsState and hlsUrls from the useRoom hook which will be used to play the HLS in the player.

// imports react-native-video
import Video from "react-native-video";

function ViewerView({}) {
const { hlsState, hlsUrls } = useRoom();

return (
<SafeAreaView style={{ flex: 1, backgroundColor: "black" }}>
{hlsState == "HLS_PLAYABLE" ? (
<>
{/* Render Header for copying the roomId and leaving room*/}
<HeaderView />

{/* Render VideoPlayer that will play `playbackHlsUrl`*/}
<Video
controls={true}
source={{
uri: hlsUrls.playbackHlsUrl,
}}
resizeMode={"contain"}
style={{
flex: 1,
backgroundColor: "black",
}}
onError={(e) => console.log("error", e)}
/>
</>
) : (
<SafeAreaView
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
>
<Text style={{ fontSize: 20, color: "white" }}>
HLS is not started yet or is stopped
</Text>
</SafeAreaView>
)}
</SafeAreaView>
);
}

Output of ViewerView Component​

Got a Question? Ask us on discord