Como crear una aplicación de videollamadas

Alan Antar
NicaSource
Published in
10 min readDec 2, 2022

Skype, Zoom, Google Meets, Discord entre otras han tomado un rol súper importante en nuestro dia a dia, con una reunión de trabajo, un curso online, entre muchas otras. Pero, qué tal crear tu propia aplicación de videollamadas? En este artículo te mostraré cómo puedes crear tu propia aplicación de videollamadas usando Agora SDK :).

LET ‘S START!

AGORA SDK

Agora SDK es una librería totalmente dedicada a las llamadas, video llamadas, streaming entre otras. Estas mismas se encuentran disponibles ya sea para Android Nativo, iOS nativo, Windows, Web, Unity, entre otros.

Hoy nos enfocaremos en cómo hacerlo funcionar en React Native!

Para mas info, no duden en visitar: https://www.agora.io/en/about-us/

Consola

Creamos nuestra cuenta

Antes de meternos a fondo en la aplicación, debemos de crearnos una cuenta en https://console.agora.io/

Creamos un Proyecto

Una vez tengamos nuestra cuenta creada, ¡vamos a crear nuestro proyecto!

Configuramos nuestro proyecto

Una vez creado nuestro proyecto y cerrada la ventana que teníamos anteriormente, le damos a donde dice

“config” -> Bajamos a la sección de “Features” -> ahí cliqueamos donde dice “Generate temp RTC Token”.

Una vez dentro, introducimos un “Channel name” como pueden ver en la imagen a continuación, y tocamos donde dice “Generate”.

Esto nos dará toda la información que vamos a necesitar para integrarlo en nuestra aplicación.

Listo! Una vez creado y configurado nuestro proyecto podemos probar que nuestras credenciales funcionen correctamente desde esta web https://webdemo.agora.io/basicVideoCall/index.html (aqui mismo ponemos los datos que nos brinda la consola y vemos que nos conectamos correctamente)

No lo cierres, que lo vamos a necesitar para probar nuestra implementación desde la app.

REACT NATIVE APP

Teniendo una breve introducción sobre que es Agora y teniendo nuestro proyecto configurado. Vamos a implementar lo necesario para hacerla funcionar en nuestra app!

Cómo funciona

Este gráfico muestra cual es el flujo de trabajo para implementar esta funcionalidad en nuestra app.

Agora SDK workflow — made by Nicasource Team.

Tranquilos, no se asusten, vamos a verlo dentro de la app y se entenderá todo mucho más. Así que basta de tanta charla y pongámonos a programar.

Preparando la app

1- Creemos nuestro nuevo proyecto corriendo la siguiente línea en nuestra terminal (la línea incluye typescript en nuestro proyecto. No están obligados a usarlo, pero lo super recomiendo):

npx react-native init ZoomTemeAnteNosotros - template react-native-template-typescript

2- Nuestro proyecto está creado. ¡Probemos que todo haya salido correctamente!

Simplemente vayamos a nuestra carpeta recién creada desde nuestra terminal y corramos

npx react-native run-android
npx react-native run-ios

Si todo salió correctamente deberíamos estar viendo nuestra app corriendo en nuestro emulador.

3- Por último antes de arrancar con nuestra integración es agregar la librería a nuestro proyecto.

Tengamos en cuenta que para integrar el SDK tenemos que estar en la versión de React Native 0.60.0 o mayor.

npm

npm i --save react-native-agora

yarn

// Instalamos yarn.
npm install -g yarn

// Descargamos el sdk de agora con yarn.
yarn add react-native-agora

En el caso que estemos corriendo la app en iOS, ejecutemos la siguiente línea para instalar la librería correctamente:

npx pod-install

Pequeño paso extra

Video SDK usa módulos nativos escritos en Swift, por lo tanto nuestro proyecto tiene que soportar la compilación de archivos Swift, para eso vamos a crear el siguiente archivo File.swift:

En Xcode, abrimos ios/ZoomTemeAnteNosotros.xcworkspace

Click en File (archivo) > New (nuevo) > Swift File (archivo Swift), clickeamos en siguiente (next) > Create (crear).

¿Cómo implementarlo?

Importamos los módulos del SDK

Borremos todo lo que se encuentra en nuestro App.tsx y ponemos lo siguiente:

import React, {useRef, useState, useEffect} from 'react';
import {
SafeAreaView,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import {PermissionsAndroid, Platform} from 'react-native';
import {
ClientRoleType,
createAgoraRtcEngine,
IRtcEngine,
RtcSurfaceView,
ChannelProfileType,
} from 'react-native-agora';

Importemos nuestras credenciales:

Como vimos al principio de este artículo, hemos generado algunas credenciales para poder probar nuestra integración.

const appId = '<--Insert app ID here-->';
const channelName = '<--Insert channel name here-->';
const token = '<--Insert authentication token here-->';
const localUid = 0;

const App = () => {
const agoraEngineRef = useRef<IRtcEngine>(); // Referencia a la instancia de Agora
const [isJoined, setIsJoined] = useState(false); // Indicador de que estamos conectados
const [remoteUid, setRemoteUid] = useState(0); // Uid del usuario remoto

};

const styles = StyleSheet.create({
button: {
paddingHorizontal: 25,
paddingVertical: 4,
fontWeight: 'bold',
color: '#ffffff',
backgroundColor: '#0055cc',
margin: 5,
},
main: {flex: 1, alignItems: 'center'},
scroll: {flex: 1, backgroundColor: '#ddeeff', width: '100%'},
scrollContainer: {alignItems: 'center'},
videoView: {width: '90%', height: 200},
btnContainer: {flexDirection: 'row', justifyContent: 'center'},
head: {fontSize: 20},
info: {backgroundColor: '#ffffe0', color: '#0000ff'}
});


export default App;

Permisos

Dependiendo de la plataforma que estamos usando, vamos a tener que agregar lo siguiente para poder otorgar los permisos de la cámara y el micrófono.

Android
En nuestro App.tsx agregamos la siguiente función:

const getPermission = async () => {
if (Platform.OS === 'android') {
await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
PermissionsAndroid.PERMISSIONS.CAMERA,
]);
}
};

iOS

En ios lo que tenemos que hacer es dirigiros al archivo info.plist y agregar estos dos valores

NSMicrophoneUsageDescription
NSCameraUsageDescription

Ahora, pongámosle algo de UI para poder probar la integración.

return (
<SafeAreaView style={styles.main}>
<Text style={styles.head}>Agora Videollamadas Inicio Rapido</Text>

<View style={styles.btnContainer}>
<Text onPress={join} style={styles.button}>
Join
</Text>
<Text onPress={leave} style={styles.button}>
Leave
</Text>
</View>
<ScrollView
style={styles.scroll}
contentContainerStyle={styles.scrollContainer}>
{isJoined ? (
<React.Fragment key={0}>
<RtcSurfaceView canvas={{uid: localUid}} style={styles.videoView} />
<Text>Usuario local uid: {uid}</Text>
</React.Fragment>
) : (
<Text>Unirse al canal</Text>
)}
{isJoined && remoteUid !== 0 ? (
<React.Fragment key={remoteUid}>
<RtcSurfaceView
canvas={{uid: remoteUid}}
style={styles.videoView}
/>
<Text>Usuario remoto uid: {remoteUid}</Text>
</React.Fragment>
) : (
<Text>Esperando que un usuario remoto se una</Text>
)}
<Text style={styles.info}>{message}</Text>
</ScrollView>
</SafeAreaView>
);

Implementando la lógica de las llamadas

Dentro de nuestro App.tsx vamos a pegar lo siguiente

const setupVoiceSDKEngine = async () => {
try {
// usamos la funcion ya creada para obtener los permisos
await getPermission();

// Le damos el valor a nuestra instancia de Agora
agoraEngineRef.current = createAgoraRtcEngine();
const agoraEngine = agoraEngineRef.current;

agoraEngine.registerEventHandler({
onJoinChannelSuccess: () => {
// Este es un listener que se ejecutará cada vez que nos conectemos a un canal correctamente.
setIsJoined(true);
},
onUserJoined: (_connection, Uid) => {
// Este es un listener que nos dirá cuando un usuario se conecta
setRemoteUid(Uid);
},
onUserOffline: (_connection, Uid) => {
// Este es un listener que nos dira cuando un usuario se desconecta
setRemoteUid(0);
},
});
agoraEngine.initialize({
appId,
});
} catch (e) {
console.log(e);
}
};

useEffect(() => {
// Inicializamos el motor de Agora cuando arranca la aplicación.
setupVoiceSDKEngine();
}, []);

Unirse a un canal para comenzar la video llamada

const join = async () => {
if (isJoined) {
// Si ya nos encontramos en la llamada, no hacemos nada.
return;
}

// Dejamos que la instancia de AGORA nos asigne el canal y nos una.
try {
agoraEngineRef.current?.setChannelProfile(
ChannelProfileType.ChannelProfileCommunication,
);
agoraEngineRef.current?.startPreview();
agoraEngineRef.current?.joinChannel(token, channel, localUid, {
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
});
} catch (e) {
console.log(e);
}
};

Abandonar un canal

const leave = () => {
// Cuando deseamos salir del canal.
try {
agoraEngineRef.current?.leaveChannel();
setIsJoined(false);
setRemoteUid(0);
} catch (e) {
console.log(e);
}
};

Probemos nuestra integración

Es hora de correr la aplicación; personalmente les recomiendo que la corran en un dispositivo físico para no tener ningún problema ya que los emuladores no suelen funcionar super bien con estas librerías. Vamos a ingresar a la página anteriormente mencionada https://webdemo.agora.io/basicVideoCall/index.html con nuestras credenciales donde podremos vernos a nosotros.

Si todo salió bien, y damos al botón de “Unirse al canal” deberíamos de poder vernos desde la app y desde la página.

Como ya vimos en nuestro ejemplo, la app funciona. Pero se ve medio precaria, ¿Qué tal si quiero mutear el micrófono? O si ¿Quiero cambiar la cámara? ¡Vamos a darle el estilo que se merece!

Let‘s go for it!

Antes que nada les recomiendo que agreguen estas dos librerias

Bien, ahora. Dentro de nuestro App.tsx haremos lo siguiente

export default function App() {
useKeepAwake();

const agoraEngineRef = useRef<IRtcEngine>(); // Instancia de Agora

const [isJoined, setIsJoined] = useState(false); // Indica si el usuario local se unio al canal
const [isMute, setIsMute] = useState(false); // Indica si el usuario local se unio al canal

const [remoteUid, setRemoteUid] = useState(0); // Uid of the remote user

const getPermission = async () => {
if (Platform.OS !== 'android') {
// El info.plist es el que se ocupara del lado de iOS
return;
}

// Pedimos los premisos adecuados para Android
await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
PermissionsAndroid.PERMISSIONS.CAMERA,
]);
};

const setupVideoSDKEngine = async () => {
try {
// Llamamos a los permisos
await getPermission();

// Le damos el valor a nuestra instancia de Agora
agoraEngineRef.current = createAgoraRtcEngine();
const agoraEngine = agoraEngineRef.current;

agoraEngine.registerEventHandler({
onJoinChannelSuccess: () => {
// Este es un listener que se ejecutara cada vez que nos conectemos a un canal correctamente.
setIsJoined(true);
},
onUserJoined: (_connection, Uid) => {
// Este es un listener que nos dira cuando un usuario se conecta
setRemoteUid(Uid);
},
onUserOffline: (_connection, _Uid) => {
// Este es un listener que nos dira cuando un usuario se desconecta
setRemoteUid(0);
},
onError: (errorCode, msg) => {
console.log('Error Code', errorCode);
console.log('Mesasge:', msg);
},
});
agoraEngine.initialize({
appId,
});
agoraEngine.enableVideo();
} catch (e) {
console.log(e);
}
};

const join = async () => {
if (isJoined) {
// Si ya nos encontramos en la llamada, no hacemos nada.
return;
}

// Dejamos que la instancia de AGORA nos asigne el canal y nos una.
try {
agoraEngineRef.current?.setChannelProfile(
ChannelProfileType.ChannelProfileCommunication,
);
agoraEngineRef.current?.startPreview();
agoraEngineRef.current?.joinChannel(token, channel, localUid, {
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
});
} catch (e) {
console.log(e);
}
};

const leave = () => {
// Cuando deseamos salir del canal.
try {
agoraEngineRef.current?.leaveChannel();

// setRemoteUids([]);
setIsJoined(false);
setIsMute(false);

setRemoteUid(0);
} catch (e) {
console.log(e);
}
};

const muteMic = () => {
try {
agoraEngineRef.current?.muteLocalAudioStream(!isMute);

setIsMute(!isMute);
} catch (e) {
console.log(e);
}
};

const switchCamera = () => {
try {
agoraEngineRef.current?.switchCamera();
} catch (e) {
console.log(e);
}
};

useEffect(() => {
// Inicializamos el motor de Agora cuando inicia la app
setupVideoSDKEngine();

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
console.log(isJoined);
}, [isJoined]);

return (
<SafeAreaProvider>
<View style={styles.container}>
{isJoined ? (
<ActiveCall
localUid={localUid}
remoteUid={remoteUid}
isMute={isMute}
onMuteMicPress={muteMic}
onSwitchCameraPress={switchCamera}
onLeavePress={leave}
/>
) : (
<InactiveoCall onJoinChannelPress={join} />
)}
</View>
</SafeAreaProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
});

Bien, se preguntarán de donde salieron <ActivaCall /> e <InactivaCall />. Estos dos serán componentes separados que nos ayudarán para mostrar las cosas un poco más prolijas, así que vamos a crearlos.

Primero que nada creamos una carpeta en nuestro proyecto que se llama “src” y dentro de ella otra carpeta más que se llama “components” para darle un poquito más de organización al proyecto.

Dentro de components vamos a crear dos archivos nuevos

ActiveCall.tsx

import React from 'react';
import {Image, Pressable, StyleSheet, Text, View} from 'react-native';
import {RtcSurfaceView} from 'react-native-agora';
import {useSafeAreaInsets} from 'react-native-safe-area-context';

export default function ActiveCall({
localUid,
remoteUid,
isMute,
onSwitchCameraPress,
onMuteMicPress,
onLeavePress,
}: {
localUid: number;
remoteUid: number;
isMute: boolean;
onMuteMicPress: () => void;
onSwitchCameraPress: () => void;
onLeavePress: () => void;
}) {
const insets = useSafeAreaInsets();
return (
<View style={styles.container}>
{/* Remote user preview */}
{remoteUid !== 0 ? (
<RtcSurfaceView
canvas={{uid: remoteUid}}
style={styles.remoteVideoView}
/>
) : (
<View>
<Text>Esperando a que alguien se una….</Text>
</View>
)}

{/* Local user preview */}
<RtcSurfaceView
canvas={{uid: localUid}}
style={
remoteUid === 0
? styles.remoteVideoView //Mostramos la cámara full size si no hay nadie : [styles.localVideoView, {bottom: insets.bottom + 100}] // En el caso que haya un usuario unido, vamos a darle un estilo pequeño }
/>
<View style={[styles.actionsContainer, {bottom: insets.bottom}]}>
{/* Boton para silenciar */}
<Pressable
style={styles.actionButtonContainer}
onPress={onMuteMicPress}>
<Image
style={styles.actionButtonImage}
source={
isMute
? require('../../assets/icons/ic_mute.png')
: require('../../assets/icons/ic_unmute.png')
}
resizeMode="contain"
/>
</Pressable>

{/* Boton para cerrar la llamada */}
<Pressable style={styles.leaveButtonContainer} onPress={onLeavePress}>
<Image
style={styles.leaveButtonImage}
source={require('../../assets/icons/ic_phone.png')}
resizeMode="contain"
/>
</Pressable>

{/* Boton para cambiar la camara */}
<Pressable
style={styles.actionButtonContainer}
onPress={onSwitchCameraPress}>
<Image
style={styles.actionButtonImage}
source={require('../../assets/icons/ic_camera.png')}
resizeMode="contain"
/>
</Pressable>
</View>
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'white',
},
localVideoView: {
overflow: 'hidden',
position: 'absolute',
right: 10,
height: 200,
width: 150,
borderRadius: 10,
borderWidth: 2,
borderColor: 'white',
},
remoteVideoView: {
flex: 1,
},
actionsContainer: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 30,
bottom: 10,
left: 0,
right: 0,
},
leaveButtonContainer: {
width: 70,
height: 70,
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 40,
backgroundColor: 'red',
},
actionButtonContainer: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 40,
backgroundColor: '#000000CC',
},
leaveButtonImage: {
width: 35,
height: 35,
tintColor: 'white',
},
actionButtonImage: {
width: 25,
height: 25,
tintColor: 'white',
},
});

InactiveCall.tsx

import React from 'react';
import {Pressable, StyleSheet, Text} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';

export default function InactiveoCall({
onJoinChannelPress,
}: {
onJoinChannelPress: () => void;
}) {
return (
<SafeAreaView style={styles.container}>
<Pressable style={styles.buttonContainer} onPress={onJoinChannelPress}>
<Text style={styles.buttonText}>Unirme al canal -&gt;</Text>
</Pressable>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#491A85',
},
contentContainer: {
flex: 1,
},
buttonContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 20,
paddingVertical: 14,
borderTopStartRadius: 10,
borderBottomStartRadius: 30,
borderTopEndRadius: 30,
borderBottomEndRadius: 10,
backgroundColor: '#3BE261',
},
buttonText: {
fontSize: 20,
fontWeight: '700',
color: '#DDFDE5',
},
});

Y LISTO! Si todo sale bien deberían de estar viendo esto:

Para las imagenes les recomiendo que entren a Flaticon.com y busquen ahí las que más le gusten. Una vez descargadas, crean en su proyecto una nueva carpeta llamada “assets” y dentro de ella “icons”

Cualquier cosa pueden observar el el repo de github.

(https://github.com/Alancito-Antar/agora-sdk-tutorial/tree/main)

--

--