https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-a-video-conference-app-with-react-native-and-vonage-video-api/Blog_React-Native_Opentok_1200x600.png

Crea una aplicación de videoconferencia con React-Native y la API de Video de Vonage

Tiempo de lectura: 9 minutos

Tener la funcionalidad de videollamada en su aplicación parece ser un must-have en estos días. Sin embargo, crear una aplicación nativa desde cero y manejar las diferentes capacidades de Android e iOS puede ser un proceso muy largo y costoso.

En este tutorial, le mostraremos cómo crear una aplicación de chat de vídeo en React Native. Para construir esta aplicación, vamos a aprovechar la herramienta Opentok React Native con la Video API de Vonage.

Para ver una versión en vídeo de este tutorial, echa un vistazo al siguiente Video:

¿Quieres avanzar? Puedes encontrar el código de este tutorial en GitHub.

Requisitos previos

Para crear tu aplicación de videollamadas React Native, necesitarás lo siguiente:

Cómo crear una aplicación de videollamadas React Native (5 pasos)

Para crear una aplicación de videollamadas con React Native y la API de Vonage, sigue estos pasos:

  1. Configure su proyecto

  2. Construir la interfaz de Video Chat

  3. Añadir los Video Streams participantes

  4. Crear la barra de herramientas de la videollamada

  5. Optimizar la aplicación para múltiples flujos

1. Configure su proyecto

Para empezar, vamos a construir el marco para la aplicación de chat de vídeo en React Native.

Primero, clona el siguiente repositorio: https://github.com/enricop89/multiparty-video-react-native.git

A continuación, instale los módulos de nodo necesarios: npm install

Para iOS, instale las dependencias del Podfile: cd ios/ && pod install

La estructura del proyecto se compone de los siguientes archivos principales:

  1. Android y iOS carpetas: es donde se compila y ejecuta el código nativo.

  2. App.js: es el archivo principal, donde se definen el estado, los accesorios, los controladores de eventos y los métodos de React.

  3. config.js: es donde se definen las credenciales.

El componente App será nuestra vista de aterrizaje cuando nuestra App sea lanzada. Tendrá una función de renderizado condicional primaria para iniciar la videollamada.

class App extends Component {
  constructor(props) {
    super(props);
    this.apiKey = credentials.API_KEY;
    this.sessionId = credentials.SESSION_ID;
    this.token = credentials.TOKEN;
    this.state = {
      subscriberIds: [], // Array for storing subscribers
      localPublishAudio: true, // Local Audio state
      localPublishVideo: true, // Local Video state
      joinCall: false, // State variable used to start the call
      streamProperties: {}, // Handle individual stream properties,
      mainSubscriberStreamId: null
    }; 
  }
  
  joinCall = () => {
    const { joinCall } = this.state;
    if (!joinCall) {
      this.setState({ joinCall: true });
    }
  };
  
  endCall = () => {
    const { joinCall } = this.state;
    if (joinCall) {
      this.setState({ joinCall: !joinCall });
    }
  };
  
  joinVideoCall = () => {
    return (
      <View style={styles.fullView}>
        <Button
          onPress={this.joinCall}
          title="JoinCall"
          color="#841584"
          accessibilityLabel="Join call">
          Join Call
        </Button>
      </View>
    );
  };
  
  render() {
    return this.state.joinCall ? this.videoPartyView() : this.joinVideoCall();
  }
}

La propiedad joinCall del estado React activará diferentes vistas en función del valor. Cuando se lance la aplicación, se mostrará al usuario una vista simple con un botón "Join the Call". El botón activará un cambio de estado y conmutará el valor joinCall para mostrar la vista más compleja de Video Call.

2. Construir la interfaz de Video Chat

Ahora que hemos construido un marco para nuestro proyecto, vamos a construir la interfaz para la propia aplicación. Nos referiremos a esta interfaz como Vista de Videollamada.

Antes de profundizar en la vista de Video Call, vale la pena pasar algún tiempo explorando la opentok-react-native biblioteca.

La biblioteca consta de tres componentes principales: OTSession, OTPublisher y OTSubscriber. Cada uno de ellos interactuará con la capa nativa (iOS y Android), llamando a los métodos nativos para conectar, publicar y suscribirse.

También necesitaremos escuchar los eventos disparados por esos componentes, especialmente los eventos de sesión, como por ejemplo sessionConnected, sessionDisconnected, streamCreated y streamDestroyed (Documentación: Eventos de sesión).

La vista de Videollamada está compuesta por los siguientes componentes:

  • Editor. La vista del editor muestra el flujo de vídeo del usuario principal.

  • Abonados. Las vistas de abonados muestran los flujos de vídeo de otros participantes.

  • Barra de herramientas. La barra de herramientas contiene botones para acceder al micrófono, a la cámara y para finalizar la llamada.

An example of the multiparty video view being built in this tutorialAn example of the multiparty video view being built in this tutorial

3. Añadir los flujos de participantes

A continuación, añadiremos secuencias de vídeo de los participantes en el chat (los abonados y el editor).

Para añadir secuencias de vídeo a la interfaz, necesitamos hacer un seguimiento de las secuencias de los suscriptores, el suscriptor principal y el estado de publicación del micrófono y la cámara locales. El lugar perfecto para almacenar esta información es el React State.

constructor(props) {
    super(props);
    this.apiKey = credentials.API_KEY;
    this.sessionId = credentials.SESSION_ID;
    this.token = credentials.TOKEN;
    this.state = {
      subscriberIds: [], // Array for storing subscribers
      localPublishAudio: true, // Local Audio state
      localPublishVideo: true, // Local Video state
      joinCall: false, // State variable used to start the call
      streamProperties: {}, // Handle individual stream properties,
      mainSubscriberStreamId: null
    }; 
  }
  
  this.sessionEventHandlers = {
      streamCreated: (event) => {
        const streamProperties = {
          ...this.state.streamProperties,
          [event.streamId]: {
            subscribeToAudio: true,
            subscribeToVideo: true,
          },
        };
        this.setState({
          streamProperties,
          subscriberIds: [...this.state.subscriberIds, event.streamId],
        });
      },
      streamDestroyed: (event) => {
        const indexToRemove = this.state.subscriberIds.indexOf(event.streamId);
        const newSubscriberIds = this.state.subscriberIds;
        const streamProperties = { ...this.state.streamProperties };
        if (indexToRemove !== -1) {
          delete streamProperties[event.streamId];
          newSubscriberIds.splice(indexToRemove, 1);
          this.setState({ subscriberIds: newSubscriberIds });
        }
      }
    }  

SubscriberIds almacena los suscriptores dentro de una sesión. Cada vez que recibimos un streamCreated significa que alguien se ha unido a la sesión y ha publicado un flujo, por lo que necesitamos añadir su streamId en el array subscriberIds array.

Por otro lado, cuando recibamos el evento streamDestroyed necesitamos eliminar el streamId del array de suscriptores.

    <View style={styles.fullView}>
      <OTSession
        apiKey={this.apiKey}
        sessionId={this.sessionId}
        token={this.token}
        eventHandlers={this.sessionEventHandlers}>
        <OTPublisher
          properties={this.publisherProperties}
          eventHandlers={this.publisherEventHandlers}
          style={styles.publisherStyle}
        />
        <OTSubscriber style={{ height: dimensions.height, width: dimensions.width }}
          eventHandlers={this.subscriberEventHandlers}
          streamProperties={this.state.streamProperties}
        >
          {this.renderSubscribers}
        </OTSubscriber>
      </OTSession>
    </View>
    

En la función de renderizado de la vista de Video, necesitamos añadir OTSession, OTPublisher y OTSubscriber de la biblioteca opentok-react-native biblioteca. En la función OTSession establecemos las credenciales y la función eventHandler como props del componente.

4. Crear la barra de herramientas de videollamada

Ahora que los flujos de vídeo funcionan, activaremos las funciones de nuestra barra de herramientas de videollamada.

El componente OTPublisher inicializará un editor y publicará en la sesión especificada tras el montaje. Es posible especificar diferentes propiedades, como la posición de la cámara, la resolución y otras (Lista de opciones del editor: Opciones del editor). En esta App de ejemplo, sólo estableceremos this.publisherProperties = { cameraPosition: 'front'};.

Asegúrese de que ha habilitado el uso de la cámara y el micrófono añadiendo las siguientes entradas a su archivo Info.plist (Proyecto iOS):

<key>NSCameraUsageDescription</key>
<string>Your message to user when the camera is accessed for the first time</string>
<key>NSMicrophoneUsageDescription</key>
<string>Your message to user when the microphone is accessed for the first time</string>

Alternativamente, para Android, añade lo siguiente (las versiones más nuevas de Android-API Level 23 (Android 6.0)- tienen un modelo de permisos diferente que ya es manejado por esta biblioteca):

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-feature android:name="android.hardware.camera" android:required="true" />
    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
    <uses-feature android:name="android.hardware.microphone" android:required="true" />

El componente OTPublisher también tiene una propiedad streamProperty que gestiona las propiedades del editor pasadas a la instancia nativa. Usando React State, podemos provocar cambios en la instancia Publisher actualizando la variable this.publisherProperties variable. Utilizamos este enfoque para implementar la barra de herramientas con las funciones de silenciar/activar el micrófono y la cámara. La implementación de la función es sencilla; activa la función publishAudio o publishVideo en los botones this.publisherProperties y en localPublishAudio y localPublishVideo para ajustar el icono del botón en función del valor.

El botón Finalizar llamada tiene un enfoque muy similar. La función endCall cambia el valor joinCall valor del Estado y restablece la Vista a la inicial.

toggleAudio = () => {
    let publishAudio = this.state.localPublishAudio;
    this.publisherProperties = { ...this.publisherProperties, publishAudio: !publishAudio };
    this.setState({
      localPublishAudio: !publishAudio,
    });
  };

  toggleVideo = () => {
    let publishVideo = this.state.localPublishVideo;
    this.publisherProperties = { ...this.publisherProperties, publishVideo: !publishVideo };
    this.setState({
      localPublishVideo: !publishVideo,
    });
  };
  
 endCall = () => {
    const { joinCall } = this.state;
    if (joinCall) {
      this.setState({ joinCall: !joinCall });
    }
  };

<View style={styles.buttonView}>
  <Icon.Button
    style={styles.iconStyle}
    backgroundColor="#131415"
    name={this.state.localPublishAudio ? 'mic' : 'mic-off'}
    onPress={this.toggleAudio}
  />
  <Icon.Button
    style={styles.iconStyle}
    backgroundColor="#131415"
    name="call-end"
    onPress={this.endCall}
  />
  <Icon.Button
    style={styles.iconStyle}
    backgroundColor="#131415"
    name={this.state.localPublishVideo ? 'videocam' : 'videocam-off'}
    onPress={this.toggleVideo}
  />
</View>

5. Optimizar la aplicación para múltiples flujos

Si ha llegado a este punto, ya hemos implementado la vista de la llamada conjunta, el componente Sesión y editor y la barra de herramientas.

A continuación, definiremos la Vista para los distintos números posibles de suscriptores. Después, añadiremos funciones para ayudar a optimizar el rendimiento del vídeo en nuestra app React Native.

Si no tenemos ningún usuariovamos a mostrar un simple texto informativo.

Si sólo tenemos un solo abonadomostraremos su flujo en modo de pantalla completa.

Por último, si tenemos más de un usuariomostraremos el abonado principal en la vista grande (como se muestra en la maqueta), y el otro en un componente de vista de desplazamiento para gestionar un número diferente de abonados.

Como el número podría crecer y poner a prueba la CPU de nuestro dispositivo y el ancho de banda de la red, aplicaremos optimizaciones en cada uno de los abonados, como bajar la resolución y desactivar el vídeo para los abonados que no estén visibles.

Exploremos el componente OTSubscriber para manejar los casos descritos anteriormente. En primer lugar, como queremos tener control sobre cada suscriptor, necesitaríamos implementar una función de renderizado para los suscriptores (custom-rendering-of-streams).

renderSubscribers = (subscribers) => {
    if (this.state.mainSubscriberStreamId) {
      subscribers = subscribers.filter(sub => sub !== this.state.mainSubscriberStreamId);
      subscribers.unshift(this.state.mainSubscriberStreamId);
    }
    return subscribers.length > 1 ? (
      <>
        <View style={styles.mainSubscriberStyle}>
          <TouchableOpacity
            onPress={() => this.handleSubscriberSelection(subscribers, subscribers[0])}
            key={subscribers[0]}>
            <OTSubscriberView
              streamId={subscribers[0]}
              style={{
                width: '100%', height: '100%'
              }}
            />
          </TouchableOpacity>
        </View>

        <View style={styles.secondarySubscribers}>
          <ScrollView
            horizontal={true}
            decelerationRate={0}
            snapToInterval={dimensions.width / 2}
            snapToAlignment={'center'}
            onScrollEndDrag={(e) => this.handleScrollEnd(e, subscribers.slice(1))}
            style={{
              width: dimensions.width,
              height: dimensions.height / 4,
            }}>
            {subscribers.slice(1).map((streamId) => (
              <TouchableOpacity
                onPress={() => this.handleSubscriberSelection(subscribers, streamId)}
                style={{
                  width: dimensions.width / 2,
                  height: dimensions.height / 4,
                }}
                key={streamId}
              >
                <OTSubscriberView
                  style={{
                    width: '100%', height: '100%'
                  }}
                  key={streamId}
                  streamId={streamId}
                />
              </TouchableOpacity>
            ))}
          </ScrollView>
        </View>
      </>
    ) : subscribers.length > 0 ? (
      <TouchableOpacity style={styles.fullView}>
        <OTSubscriberView streamId={subscribers[0]}
          key={subscribers[0]}
          style={{ width: '100%', height: '100%' }}
        />
      </TouchableOpacity>
    ) : (<Text>No one connected</Text>)
  };  

Utilizamos el renderizado condicional en React (https://reactjs.org/docs/conditional-rendering.html) para manejar los diferentes casos con cero, uno o N suscriptores.

En primer lugar, si no hay abonados, caemos en el último caso, y mostramos un Text componente.

En segundo lugar, si hay un abonado, lo mostramos en modo de vista completa.

Por último, el caso más interesante es cuando los abonados son más de uno: Tenemos una vista de suscriptor principal y un componente ScrollView en el que alimentaremos a los demás abonados. El primer paso es comprobar si tenemos un componente mainSubscriberStreamId. Si es así, ordenaremos el array para que el suscriptor principal sea el primer elemento. El resto de suscriptores se mostrarán en la vista ScrollView horizontalmente. El componente ScrollView es ideal para nuestro caso de uso, ya que podemos mostrar un número relativamente alto de suscriptores sin necesidad de cambiar el diseño, y podemos detectar cuántos suscriptores hay en la vista de desplazamiento y cuántos de ellos son visibles.

Las llamadas de grupo en dispositivos móviles pueden ser todo un reto, tanto desde el punto de vista del hardware como de la red. Para ofrecer un buen resultado al usuario final, una aplicación debe implementar una lista de mejores prácticas para manejar diferentes casos de uso y diseño. En nuestro caso, tenemos una vista principal de suscriptores que necesita tener la mejor resolución posible, y el componente Scroll View con el resto de suscriptores en miniaturas más pequeñas que podrían optimizarse bajando la resolución recibida. Los SDKs de Opentok dan al desarrollador la oportunidad de establecer la resolución y velocidad de fotogramas preferidas para cada uno de los suscriptores (setPreferredFrameRate y setPreferredResolution).

Implementamos el método handleSubscriberSelection para manejar la vista mainSubscriber y la resolución preferida. La función está en el TouchableOpacity componente padre de cada uno de los suscriptores.

const mainSubscribersResolution = { width: 1280, height: 720 };
const secondarySubscribersResolution = { width: 352, height: 288 };


  handleSubscriberSelection = (subscribers, streamId) => {
    let subscriberToSwap = subscribers.indexOf(streamId);
    let currentSubscribers = subscribers;
    let temp = currentSubscribers[subscriberToSwap];
    currentSubscribers[subscriberToSwap] = currentSubscribers[0];
    currentSubscribers[0] = temp;
    this.setState(prevState => {
      const newStreamProps = { ...prevState.streamProperties };
      for (let i = 0; i < currentSubscribers.length; i += 1) {
        if (i === 0) {
          newStreamProps[currentSubscribers[i]] = { ...prevState.streamProperties[currentSubscribers[i]] }
          newStreamProps[currentSubscribers[i]].preferredResolution = mainSubscribersResolution;
        } else {
          newStreamProps[currentSubscribers[i]] = { ...prevState.streamProperties[currentSubscribers[i]] }
          newStreamProps[currentSubscribers[i]].preferredResolution = secondarySubscribersResolution;
        }
      }
      return { mainSubscriberStreamId: streamId, streamProperties: newStreamProps };
    })
  }

En función del abonado seleccionado, la función mueve el abonado seleccionado a la cabecera de la matriz de abonados. Como se ha mencionado antes, el primer elemento de la matriz de suscriptores se mostrará en la Vista principal. Después de eso, necesitamos actualizar el streamProperties del componente OTSubscriber para establecer la resolución preferida. Establecemos la resolución máxima (width: 1280, height: 720) para el suscriptor principal y una resolución inferior para los demás ({ width: 352, height: 288 }). Si también queremos cambiar la velocidad de fotogramas preferida, en función del diseño o del caso de uso, sólo tendríamos que añadir la propiedad preferredFrameRate en el objeto streamProperties objeto.

Por último, queremos optimizar el ScrollView componente. El componente ScrollView podría tener un elevado número de suscriptores, pero sólo puede mostrar dos simultáneamente. Por ejemplo, si tenemos cinco suscriptores, uno estará en la vista principal de suscriptores; los cuatro restantes estarán en el ScrollView. Sólo dos de ellos serán visibles en la Vista, y los restantes sólo lo serán si nos desplazamos horizontalmente.

El componente ScrollView tiene un evento llamado onScrollEndDragal que se llama cuando el usuario deja de arrastrar la vista de desplazamiento y ésta se detiene o comienza a deslizarse. Podemos utilizar este evento para saber qué suscriptores son visibles y silenciar el vídeo de los restantes. Silenciando el video del flujo no visible mejorará el rendimiento de la App, y ahorrará consumo de CPU y ancho de banda de red.

handleScrollEnd = (event, subscribers) => {
    let firstVisibleIndex;
    if (event && event.nativeEvent && !isNaN(event.nativeEvent.contentOffset.x)) {
      firstVisibleIndex = parseInt(event.nativeEvent.contentOffset.x / (dimensions.width / 2), 10);
    }
    this.setState(prevState => {
      const newStreamProps = { ...prevState.streamProperties };
      if (firstVisibleIndex !== undefined && !isNaN(firstVisibleIndex)) {
        for (let i = 0; i < subscribers.length; i += 1) {
          if (i === firstVisibleIndex || i === (firstVisibleIndex + 1)) {
            newStreamProps[subscribers[i]] = { ...prevState.streamProperties[subscribers[i]] }
            newStreamProps[subscribers[i]].subscribeToVideo = true;
          } else {
            newStreamProps[subscribers[i]] = { ...prevState.streamProperties[subscribers[i]] }
            newStreamProps[subscribers[i]].subscribeToVideo = false;
          }
        }
      }
      return { streamProperties: newStreamProps }
    })
  }

En el onScrollEndDrag evento, tenemos la información sobre el contentOffset que es el punto en el que el origen de la vista de contenido se desplaza del origen de la vista de desplazamiento. Utilizaremos este valor para saber qué flujos son visibles actualmente, dividiendo el desplazamiento del contenido por la mitad del ancho de la pantalla (event.nativeEvent.contentOffset.x / (dimensions.width / 2)).

El resultado será el primer abonado visible. En este punto, sabemos que los flujos visibles son el flujo en posición firstVisibleIndex y firstVisibleIndex + 1. El último paso es hacer un bucle en la matriz de abonados y silenciar el vídeo de los abonados no visibles.

Conclusión

¡Enhorabuena! Acabas de terminar de crear tu primera aplicación de videollamadas en React Native.

Este tutorial muestra lo fácil que es añadir videochat a las aplicaciones React Native con la ayuda de la Video API de Vonage. A medida que las videollamadas se hacen más y más populares, las aplicaciones de videochat como esta ofrecen cada vez más valor a marcas y empresas.

Si tiene alguna pregunta o comentario, póngase en contacto con nosotros en Twitter o únase a nuestro Canal Slack de la Comunidad. Puede acceder al código de este tutorial en GitHub.

Lecturas complementarias

Cómo crear una aplicación de videochat con Python y Flask

Cómo crear una aplicación de chat de vídeo con ASP.NET y Angular

Cómo añadir videochat a las aplicaciones Next.js

Cómo añadir Video Chat a las aplicaciones Firebase

Cómo hacer llamadas telefónicas en aplicaciones Android con React Native

Compartir:

https://a.storyblok.com/f/270183/400x266/5bd495df3c/enrico-portolan.png
Enrico PortolanAutor invitado

Enrico es un antiguo miembro del equipo de Vonage. Trabajó como ingeniero de soluciones, ayudando al equipo de ventas con su experiencia técnica. Es un apasionado de la nube, las startups y las nuevas tecnologías. Es cofundador de una startup WebRTC en Italia. Fuera del trabajo, le gusta viajar y probar tantas comidas raras como sea posible.