https://a.storyblok.com/f/270183/91890/ca51e96be3/blog_react-native_opentok_1200x600.png

Crie um aplicativo de videoconferência com o React Native e a Video API da Vonage

Tempo de leitura: 9 minutos

Hoje em dia, parece que é imprescindível que seu aplicativo tenha a funcionalidade de videochamada. No entanto, desenvolver um aplicativo nativo do zero e lidar com as diferentes funcionalidades do Android e do iOS pode ser um processo muito demorado e caro.

Neste tutorial, vamos mostrar como criar um aplicativo de chat por vídeo no React Native. Para criar esse aplicativo, vamos utilizar o biblioteca Opentok para React Native juntamente com a Video API da Vonage.

Para assistir à versão em Video deste tutorial, confira o Video abaixo:

Quer pular para a parte seguinte? Você pode encontrar o código deste tutorial no GitHub.

Pré-requisitos

Para desenvolver seu aplicativo de videochamadas em React Native, você precisará do seguinte:

Como criar um aplicativo de videochamada com React Native (5 etapas)

Para criar um aplicativo de videochamadas com o React Native e a API da Vonage, siga as etapas abaixo:

  1. Configure seu projeto

  2. Criar a interface do chat por vídeo

  3. Adicionar os fluxos de vídeo dos participantes

  4. Criar a barra de ferramentas de videochamada

  5. Otimize o aplicativo para múltiplos fluxos

1. Configure seu projeto

Para começar, vamos criar a estrutura do aplicativo de chat por vídeo no React Native.

Primeiro, clone o seguinte repositório: https://github.com/enricop89/multiparty-video-react-native.git

Em seguida, instale os módulos do Node.js necessários: npm install

No iOS, instale as dependências do Podfile: cd ios/ && pod install

A estrutura do projeto é composta pelos seguintes arquivos principais:

  1. Android e iOS pastas: é onde o código nativo é compilado e executado.

  2. App.js: é o arquivo principal, onde são definidos o estado, as props, os manipuladores de eventos e os métodos do React.

  3. config.js: é onde as credenciais são definidas.

O componente App será nossa tela inicial quando o aplicativo for iniciado. Ele terá uma função de renderização condicional principal para iniciar a videochamada.

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();
  }
}

A joinCall propriedade no estado do React acionará diferentes visualizações com base no valor. Quando o aplicativo for iniciado, uma visualização simples com um botão “Participar da chamada” será exibida ao usuário. O botão acionará uma mudança no estado e alternará o joinCall valor para exibir a visualização de videochamada, que é mais complexa.

2. Criar a interface do chat por vídeo

Agora que criamos uma estrutura para o nosso projeto, vamos desenvolver a interface do próprio aplicativo. Vamos nos referir a essa interface como a Visualização de Videochamada.

Antes de nos aprofundarmos na Visualização de Videochamadas, vale a pena dedicar algum tempo para explorar a opentok-react-native biblioteca.

A biblioteca é composta por três componentes principais: OTSession, OTPublisher e OTSubscriber. Cada um deles irá interagir com a camada nativa (iOS e Android), chamando os métodos nativos para se conectar, publicar e assinar.

Também precisaremos monitorar os eventos disparados por esses componentes, especialmente os eventos de sessão, como sessionConnected, sessionDisconnected, streamCreated e streamDestroyed (Documentação: Eventos de sessão).

A tela de videochamada é composta pelos seguintes componentes:

  • Editor. A visualização do editor exibe o fluxo de vídeo do usuário principal.

  • Assinantes. As telas dos assinantes exibem as transmissões de Video dos outros participantes.

  • Barra de ferramentas. A barra de ferramentas contém botões para acessar o microfone, acessar a câmera e encerrar a chamada.

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

3. Adicionar os fluxos dos participantes

A seguir, vamos adicionar transmissões de Video dos participantes do chat (os assinantes e o emissor).

Para adicionar transmissões de Video à interface, precisamos acompanhar as transmissões dos assinantes, o assinante principal e o status de publicação do microfone e da câmera locais. O lugar ideal para armazenar essas informações é o 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 O array armazena os assinantes dentro de uma sessão. Cada vez que recebemos um evento streamCreated , isso significa que alguém entrou na sessão e publicou um stream; portanto, precisamos adicionar o streamId dessa pessoa à subscriberIds array.

Por outro lado, quando recebemos o streamDestroyed evento, precisamos remover o streamId da matriz de assinantes.

    <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>
    

Na função de renderização da visualização de Video, precisamos adicionar OTSession, OTPublisher e OTSubscriber da opentok-react-native biblioteca. No OTSession definimos as credenciais e a função eventHandler como props do componente.

4. Criar a barra de ferramentas da videochamada

Agora que as transmissões de vídeo estão funcionando, vamos ativar os recursos na barra de ferramentas das nossas videochamadas.

O OTPublisher componente inicializará um publisher e publicará na sessão especificada ao ser montado. É possível especificar diferentes propriedades, como posição da câmera, resolução e outras (Lista de opções do Publisher: Opções do Publisher). Neste aplicativo de exemplo, definiremos apenas this.publisherProperties = { cameraPosition: 'front'};.

Certifique-se de ter habilitado o uso da câmera e do microfone adicionando as seguintes entradas ao seu Info.plist arquivo (Projeto 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>

Como alternativa, para o Android, adicione o seguinte (versões mais recentes do Android –API Level 23 (Android 6.0) – possuem um modelo de permissões diferente, que já é tratado 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" />

O OTPublisher componente também possui uma streamProperty propriedade que lida com as propriedades do publisher passadas para a instância nativa. Usando o estado do React, podemos acionar alterações na instância do Publisher atualizando a this.publisherProperties variável. Usamos essa abordagem para implementar a barra de ferramentas com as funções de silenciar/ativar o som para o microfone e a câmera. A implementação da função é simples; ela alterna a publishAudio ou publishVideo no this.publisherProperties e o localPublishAudio e localPublishVideo para ajustar o ícone do botão com base no valor.

O botão “Encerrar Chamada” funciona de maneira muito semelhante. A endCall função alterna o joinCall valor no “State” e redefine a “View” para a 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. Otimizar o aplicativo para múltiplos fluxos

Se você chegou até aqui, já implementamos a visualização “Join Call”, os componentes “Session” e “Publisher” e a barra de ferramentas.

A seguir, definiremos a visualização para os diferentes números possíveis de assinantes. Depois disso, adicionaremos recursos para ajudar a otimizar o desempenho dos vídeos em nosso aplicativo React Native.

Se tivermos nenhum usuário, vamos exibir um texto informativo simples.

Se tivermos apenas um assinante, exibiremos a transmissão dele no modo de tela cheia.

Por fim, se tivermos mais de um usuário, exibiremos o assinante principal na View grande (conforme mostrado na maquete) e o outro em um componente Scroll View para lidar com um número diferente de assinantes.

Como esse número pode aumentar e sobrecarregar a CPU do nosso dispositivo e a largura de banda da rede, implementaremos otimizações para cada um dos assinantes, como reduzir a resolução e desativar o Video para os assinantes que não estão visíveis.

Vamos explorar o OTSubscriber componente para lidar com os casos descritos acima. Em primeiro lugar, como queremos ter controle sobre cada assinante, precisaríamos implementar uma função de renderização para os assinantes (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 a renderização condicional no React (https://reactjs.org/docs/conditional-rendering.html) para lidar com os diferentes casos em que há zero, um ou N assinantes.

Em primeiro lugar, se não houver assinantes, nos enquadramos no último caso e exibimos um Text componente.

Em segundo lugar, se houver um assinante, exibimos esse assinante no modo de visualização completa.

Por fim, o caso mais interessante é quando há mais de um assinante: temos uma visualização principal dos assinantes e um ScrollView componente no qual alimentaremos os demais assinantes. O primeiro passo é verificar se temos um mainSubscriberStreamId. Se for o caso, ordenaremos a matriz para que o assinante principal seja o primeiro elemento. Os demais assinantes serão exibidos ScrollView horizontalmente. O ScrollView componente é ideal para o nosso caso de uso, pois podemos mostrar um número relativamente alto de assinantes sem a necessidade de alterar o layout, e podemos detectar quantos assinantes estão na vista de rolagem e quantos deles estão visíveis.

Chamadas em grupo em dispositivos móveis podem ser bastante desafiadoras, tanto do ponto de vista do hardware quanto da rede. Para oferecer um bom resultado ao usuário final, um aplicativo deve implementar uma série de práticas recomendadas para lidar com diferentes casos de uso e layouts. No nosso caso, temos uma visualização principal do assinante que precisa ter a melhor resolução possível, e o componente Scroll View com os demais assinantes em miniaturas menores, que podem ser otimizadas reduzindo a resolução recebida. Os SDKs do Opentok oferecem ao desenvolvedor a oportunidade de definir a resolução e a taxa de quadros preferidas para cada um dos assinantes (setPreferredFrameRate e setPreferredResolution).

Implementamos o handleSubscriberSelection método para lidar com a View do mainSubscriber e a resolução preferencial. A função está no TouchableOpacity componente pai de cada um dos assinantes.

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 };
    })
  }

Com base no assinante selecionado, a função move o assinante selecionado para o início da matriz de assinantes. Conforme mencionado anteriormente, o primeiro elemento da matriz de assinantes será exibido na View principal. Em seguida, precisamos atualizar a streamProperties do OTSubscriber componente para definir as diferentes resoluções preferidas. Definimos a resolução máxima (width: 1280, height: 720) para o assinante principal e uma resolução menor para os demais ({ width: 352, height: 288 }). Se também quisermos alterar a taxa de quadros preferencial, com base no layout ou no caso de uso, bastaria adicionar a preferredFrameRate propriedade no streamProperties objeto.

Por fim, queremos otimizar o ScrollView componente. O componente ScrollView pode ter um grande número de assinantes, mas só consegue exibir dois simultaneamente. Por exemplo, se tivermos cinco assinantes, um estará na visualização principal de assinantes; os quatro restantes estarão no ScrollView. Apenas dois deles ficam visíveis na Visualização, e os demais só ficarão visíveis se rolarmos a tela horizontalmente.

O componente ScrollView possui um ouvinte de evento chamado onScrollEndDrag, que é acionado quando o usuário para de arrastar a visualização de rolagem e ela para ou começa a deslizar. Podemos usar esse evento para identificar quais assinantes estão visíveis e silenciar o Video dos demais. Silenciar o Video das transmissões não visíveis melhorará o desempenho do aplicativo e economizará consumo de CPU e largura de banda da rede.

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 }
    })
  }

No onScrollEndDrag evento, temos as informações sobre o contentOffset , que é o ponto em que a origem da visualização do conteúdo está deslocada em relação à origem da visualização de rolagem. Usaremos esse valor para entender quais fluxos estão visíveis no momento, dividindo o deslocamento do conteúdo pela metade da largura da tela (event.nativeEvent.contentOffset.x / (dimensions.width / 2)).

O resultado será o primeiro assinante visível. Nesse momento, sabemos que os fluxos visíveis são o fluxo na posição firstVisibleIndex e firstVisibleIndex + 1. O último passo é percorrer a matriz de assinantes e silenciar o Video dos assinantes não visíveis.

Conclusão

Parabéns! Você acabou de concluir a criação do seu primeiro aplicativo de videochamadas no React Native.

Este tutorial mostra como é fácil adicionar um chat por vídeo a aplicativos React Native com a ajuda da Video API da Vonage. À medida que as chamadas de vídeo se tornam cada vez mais populares, aplicativos de chat por vídeo como este oferecem cada vez mais valor para marcas e empresas.

Se você tiver alguma dúvida ou comentário, entre em contato conosco pelo Twitter ou participe do nosso canal da Comunidade no Slack. Você pode acessar o código deste tutorial no GitHub.

Leituras complementares

Como criar um aplicativo de videochamada com Python e Flask

Como criar um aplicativo de videochamada com ASP.NET e Angular

Como adicionar chat por vídeo a aplicativos Next.js

Como adicionar chat por vídeo a aplicativos do Firebase

Como fazer chamadas telefônicas em aplicativos Android com o React Native

Compartilhar:

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

Enrico é um ex-funcionário da Vonage. Ele trabalhou como engenheiro de soluções, auxiliando a equipe de vendas com seus conhecimentos técnicos. Ele é apaixonado pela nuvem, por startups e por novas tecnologias. É cofundador de uma startup de WebRTC na Itália. No tempo livre, gosta de viajar e experimentar o máximo possível de comidas exóticas.