Telnyx Video React Native
Getting Started
In this guide, you’ll learn how to get run and deploy Telnyx Meet
Just follow these steps:
- Sign up for a Telnyx Account
- Create an API KEY
- Create a Room ID
- Installation
Step 1: Sign Up for a Telnyx Mission Control Portal Account
Head to telnyx.com/sign-up to sign up for your free Telnyx account.
Once signed up you will have access to Telnyx Portal, where you can set up and manage your API KEY, and more.
Step 2: Create an API KEY
Go to API Keys section and click on Create API Key
button. It will generate a key for you. Copy and save this key in a safe place and don't share it with anyone it is a sensitive value.
You need this API Key to consume the API https://api.telnyx.com/v2/rooms
to manage your room ids.
Step 3: Create a Room ID
You should read this documentation video/Rooms to learn how to create a new video room id. When you get your roomId
you can join in a video meet conference.
Step 4. Installation
Install the package with:
npm install @telnyx/video-react-native --save
In android/settings.gradle file add following code
include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
In android/build.gradle file make minSdkVersion=24
In android/app/src/main/java/(package name)/MainApplication.java add following:
In import section add
import com.oney.WebRTCModule.WebRTCModulePackage;
In React package section add
packages.add(new WebRTCModulePackage());
In android/app/build.gradle file add
In dependency section add:
implementation project(':react-native-webrtc')
As long as you can import npm packages with a bundler like Webpack, you're ready to import Room
and any other class required and begin.
You will also need some classes from react-native-webrtc
such as RTCView
which can be used to display a video stream:
import { Room, initialize, State } from '@telnyx/video-react-native';
import { RTCView, mediaDevices, MediaStream } from 'react-native-webrtc';
You will also need https://babeljs.io/docs/en/babel-plugin-proposal-async-generator-functions#installation to be able to use some of the async calls in the SDK.
Permissions
In order to use native mobile features such as internet or record audio, you need to enable permissions for both platforms. You can follow these guides: Android
iOS
Usage
General usage is the same as the vanilla web JS video implementation which is described here
And then use a permission library such as react-native-permissions to verify that the permissions are enabled at runtime.
React Native Specific Usage
The key difference between the Web and React Native SDK is how we attach streams to a view. With React Native we are no longer using DOM elements, but rather an RTCView:
const [localStream, setStream] = useState<string>('');
...
const onPressPublish = async () => {
try {
let callerStream = await mediaDevices.getUserMedia({
audio: true,
video: true,
});
console.log('got local stream using getUserMedia...');
let callerAudioTrack = callerStream.getAudioTracks()[0];
let callerVideoTrack = callerStream.getVideoTracks()[0];
await room.addStream('self', {
audio: callerAudioTrack,
video: callerVideoTrack,
});
setStream(callerStream.toURL());
} catch (e) {
console.error("Unable to publish stream...");
}
};
....
<SafeAreaView style={styles.localStreamBox}>
{
localStream ?
<RTCView
mirror={true}
objectFit={'contain'}
streamURL={localStream!}
style={{ width: '100%', height: '100%' }}
zOrder={1}
/> : null
}
</SafeAreaView>
Full example:
Below is an adapted version of the following web example adapted for React Native with this library
import * as React from 'react';
import { StyleSheet, View, Alert, Platform, StatusBar, SafeAreaView, Button } from 'react-native';
import { RTCView, mediaDevices, MediaStream } from 'react-native-webrtc';
import { useEffect, useState } from 'react';
import { Room, initialize, State } from 'telnyx-video-react-native';
import { FlatGrid } from 'react-native-super-grid';
import { requestMultiple, PERMISSIONS } from 'react-native-permissions';
import Config from "react-native-config";
let ROOM_ID = Config.ROOM_ID
let API_KEY = Config.API_KEY
export default function App() {
const [localStream, setStream] = useState<string>('');
const [room, setRoom] = useState<typeof Room>();
const [participantStreamMaps, setParticipantStreamMaps] = useState<Map<String, MediaStream>>(new Map<string, MediaStream>())
const updateStreamMap = (k: string, v: MediaStream) => {
setParticipantStreamMaps(new Map(participantStreamMaps.set(k, v)));
}
const deleteFromStreamMap = (v: string) => {
participantStreamMaps.delete(v)
let map = new Map(participantStreamMaps)
map.delete(v)
setParticipantStreamMaps(new Map(map));
}
const [data, setData] = useState();
const [isLoading, setLoading] = useState(true);
useEffect(() => {
if (API_KEY == undefined || ROOM_ID == undefined) {
throw Error('Please create a .env file and add your ROOM_ID and API_KEY');
}
verifyPermissions();
getToken();
}, []);
const getToken = async () => {
const requestTokenUrl = `https://api.telnyx.com/v2/rooms/${ROOM_ID}/actions/generate_join_client_token`;
var tokenReceived;
try {
const response = await fetch(requestTokenUrl, {
body: '{"refresh_token_ttl_secs":3600,"token_ttl_secs":600}',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${API_KEY}`,
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
method: 'POST',
});
const json = await response.json();
setData(json);
tokenReceived = json;
} catch (error) {
console.error(error);
} finally {
setLoading(false);
makeCall(tokenReceived.data.token);
}
};
async function makeCall(token: string) {
let room;
console.log(`Token being provided: ${token}`);
const context = '{"id":99999,"username":"React Native User"}';
room = await initialize({
roomId: ROOM_ID,
clientToken: token,
context: context,
logLevel: 'DEBUG',
});
setRoom(room);
room.on("connected", (state) => {
let receivedState = state as State
console.log(`connected to the room... ` + JSON.stringify(receivedState));
const remoteParticipants = state.participants;
remoteParticipants.forEach((item) => {
const remoteParticipant = state.participants.get(item.participantId);
console.log(`participant connected ` + item);
});
state.streams.forEach((stream) => {
if (stream.participantId === room.getLocalParticipant().id) {
return;
}
room.addSubscription(stream.participantId, stream.key, {
audio: true,
video: true,
});
});
});
room.on("participant_joined", (participantId, state) => {
const remoteParticipant = state.participants.get(participantId);
console.log(`participant ${remoteParticipant.context} joined...`);
});
room.on("stream_published", async (participantId, streamKey, state) => {
let participant = state.participants.get(participantId);
if (participant.origin === "local") {
return;
}
await room.addSubscription(participantId, streamKey, {
audio: true,
video: true
});
console.log(
`subscription to the: ${participantId} ${streamKey} has been added...`
);
});
room.on("subscription_started", (participantId, streamKey, state) => {
console.log(
`subscription to the: ${participantId} ${streamKey} stream started...`
);
let receivedState = state as State
console.log(`Subscription started :: State .. ` + JSON.stringify(receivedState));
let remoteStream = room.getParticipantStream(participantId, streamKey);
let remoteMediaStream = new MediaStream([
remoteStream.audioTrack,
remoteStream.videoTrack
]);
updateStreamMap(participantId, remoteMediaStream)
});
room.on("subscription_ended", (participantId, streamKey, state) => {
console.log(
`subscription to the: ${participantId} ${streamKey} stream ended...`
);
deleteFromStreamMap(participantId)
});
await room.connect();
}
const verifyPermissions = async () => {
if (Platform.OS == 'android') {
let perm = [
PERMISSIONS.ANDROID.CAMERA,
PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
PERMISSIONS.ANDROID.RECORD_AUDIO,
PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE,
];
let permissionStatuses = await requestMultiple(perm);
console.log('obj', permissionStatuses);
const result = permissionStatuses[perm[0]];
if (result !== 'granted') {
Alert.alert(
'Insufficient permissions!',
'You need to grant camera and library access permissions to use this app.',
[{ text: 'Okay' }],
);
return false;
}
return true;
} else {
let perm = [PERMISSIONS.IOS.CAMERA, PERMISSIONS.IOS.MICROPHONE];
let permissionStatuses = await requestMultiple(perm);
console.log('obj', permissionStatuses);
const result = permissionStatuses[perm[0]];
if (result !== 'granted') {
Alert.alert(
'Insufficient permissions!',
'You need to grant camera and library access permissions to use this app.',
[{ text: 'Okay' }],
);
return false;
}
return true;
}
};
const onPressPublish = async () => {
try {
let callerStream = await mediaDevices.getUserMedia({
audio: true,
video: true,
});
console.log('got local stream using getUserMedia...');
let callerAudioTrack = callerStream.getAudioTracks()[0];
let callerVideoTrack = callerStream.getVideoTracks()[0];
await room.addStream('self', {
audio: callerAudioTrack,
video: callerVideoTrack,
});
setStream(callerStream.toURL());
} catch (e) {
console.error("Unable to publish stream...");
}
};
return (
<>
<StatusBar barStyle="dark-content" />
<View style={styles.box}>
<Button
title="Start"
onPress={onPressPublish} />
</View>
{
<FlatGrid
data={Array.from(participantStreamMaps.values())}
style={styles.gridView}
itemDimension={130}
spacing={10}
renderItem={({ item }) =>
<SafeAreaView style={styles.remoteStreamBox}>
<RTCView
mirror={false}
objectFit={'contain'}
streamURL={item.toURL()}
style={{ width: '100%', height: '100%' }}
zOrder={1}
/>
</SafeAreaView>}
keyExtractor={item => item.toURL()}
/>
}
<SafeAreaView style={styles.localStreamBox}>
{
localStream ?
<RTCView
mirror={true}
objectFit={'contain'}
streamURL={localStream!}
style={{ width: '100%', height: '100%' }}
zOrder={1}
/> : null
}
</SafeAreaView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
gridView: {
marginTop: 10,
flex: 1,
},
box: {
width: 60,
height: 60,
marginVertical: 20,
},
localStreamBox: {
width: 400,
height: 300,
marginVertical: 20,
},
remoteStreamBox: {
width: 200,
height: 125,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
});