Como criar um app de chat por vídeo com o Twilio Programmable Video e React

March 29, 2018
Escrito por
Brian Kimokoti
Contribuidor
As opiniões expressas pelos colaboradores da Twilio são de sua autoria

Como criar um app de chat por vídeo com o Twilio Programmable Video e React

This article is for reference only. We're not onboarding new customers to Programmable Video. Existing customers can continue to use the product until December 5, 2024.


We recommend migrating your application to the API provided by our preferred video partner, Zoom. We've prepared this migration guide to assist you in minimizing any service disruption.

Este tutorial foi criado com base em uma versão anterior do React e uma versão mais antiga do SDK da Twilio Programmable Video.

Para novos desenvolvimentos, sugerimos estes dois tutoriais:

A demanda por ferramentas de colaboração é cada vez maior, principalmente por parte dos desenvolvedores remotos e até mesmo de profissionais que atuam em outros setores. É muito importante ter um aplicativo de chat por vídeo cuja "aparência" possa ser personalizada, já que você não fica restrito aos recursos da maioria dos provedores comerciais.
Neste tutorial, vamos aprender a criar um aplicativo de chat por vídeo usando o React, o Node.js e a Twilio Programmable Video.

Como configurar o React e o Node.js

Para desenvolver esse aplicativo, vamos precisar de conhecimentos básicos do React JS, do Node JS e de JavaScript por parte do cliente.
Também vamos precisar de uma instalação do Node JS igual ou superior à versão 7.0.0 e de um navegador para os testes.

Configuração da conta Twilio

Se você não tem uma conta Twilio, cadastre-se e, depois, crie um projeto. Quando o sistema solicitar quais recursos que deseja usar, selecione Programmable Video. Você encontra o TWILIO_ACCOUNT_SID no dashboard de projetos.

Console da Twilio com as informações de acesso a conta.

Em seguida, vamos criar uma API key (chave de API) na seção Programmable Video. Se tiver dificuldades com a configuração da API key (chave de API) da Twilio, este GitHub Issue contém mais informações que podem ser úteis. Usaremos o "SID" da figura abaixo para TWILIO_API_KEY e o "secret" (segredo) para TWILIO_API_SECRET.

Observação: o API key (SID da chave) de API sempre começa com SK.

Tela do Programmable Video com as informações da API Key a ser utilizada no projeto.

Configuração do projeto do React

Neste projeto, vamos nos concentrar principalmente em usar a Twilio com o React e, por isso, vamos pular a configuração do projeto e usar um texto clichê simplificado.

Para a configuração de SSH, digite o seguinte comando na linha de comando do terminal:

git clone -b setup git@github.com:kimobrian/TwilioReact.git

Como alternativa, para a configuração de HTTPS, digite o seguinte comando na linha de comando do terminal:

git clone -b setup https://github.com/kimobrian/TwilioReact.git

Em seguida, usamos cd na pasta TwilioReact e executamos yarn ou npm i para instalar os pacotes. Temos a seguinte estrutura de pastas.

Parcial da tela com a estrutura de arquivos do projeto.

Como desenvolver o aplicativo Node Programmable Video

Vamos começar pela configuração das chaves e dos segredos em um arquivo .env com base no arquivo env.template do repositório do GitHub que acabamos de clonar. É uma prática recomendada manter as chaves em um arquivo .env que facilita a alteração delas a qualquer momento, além de facilitar a implantação em qualquer plataforma.

Observação: não envie o arquivo .env para nenhum serviço de hospedagem git, como o GitHub, porque isso expõe nossas chaves e segredos e compromete a segurança.

Vamos criar o arquivo .env e adicionar o seguinte código a ele, substituindo cada chave e token por um disponível no Console da Twilio.

TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_SECRET=XXXXXXXXXXXXXXXXXXXXXXXX
NODE_ENV=DEV

Depois de acessar a pasta "TwilioReact", é possível testar a configuração inicial que executa o "npm start" na linha de comando. Agora, vamos abrir um navegador para acessar http://localhost:3000/, onde devemos ver a seguinte interface.

Tela do navegador com o exemplo em execução.

Configuração do servidor

Precisamos configurar o servidor para gerar tokens que possam ser enviados ao cliente. A Twilio usará o token para conceder acesso ao cliente para usar o Video SDK. Para usar as funções da Twilio Video, precisamos instalar o Pacote Twilio Node.js ao inserir na linha de comando.

npm install —save twilio

Nosso package.json inclui outro pacote, faker. Ele será usado para gerar nomes aleatórios e atribui-los a cada cliente conectado.

As API Keys (chaves de API) são credenciais para acessar a API da Twilio. Elas são usadas para autenticar a API REST e para criar e revogar tokens de acesso. Os tokens de acesso são usados para autenticar SDKs de cliente e para conceder acesso aos recursos da API.

O trecho de código abaixo cria um token de acesso JWT e o envia para o cliente com um nome aleatório. Adicionamos o código no server.js.

hl_lines="2  3  9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30"
// server.js
// ... Code before
var AccessToken = require('twilio').jwt.AccessToken;
var VideoGrant = AccessToken.VideoGrant;
var app = express();
if(process.env.NODE_ENV === 'DEV') {
    // ... initial code block here
}

// Endpoint to generate access token
app.get('/token', function(request, response) {
   var identity = faker.name.findName();

   // Create an access token which we will sign and return to the client,
   // containing the grant we just created
   var token = new AccessToken(
       process.env.TWILIO_ACCOUNT_SID,
       process.env.TWILIO_API_KEY,
       process.env.TWILIO_API_SECRET
   );

   // Assign the generated identity to the token
   token.identity = identity;

   const grant = new VideoGrant();
   // Grant token access to the Video API features
   token.addGrant(grant);

   // Serialize the token to a JWT string and include it in a JSON response
   response.send({
       identity: identity,
       token: token.toJwt()
   });
});
var port = process.env.PORT || 3000;
// Code after ...

Criação da sala de chamada de vídeo

Por padrão, nosso código do lado do cliente pode criar salas de chamada de vídeo. Vamos usar esse recurso no aplicativo e, caso precise restringir as salas a serem criadas, é possível desativar a criação de salas do lado do cliente nas configurações da sala.

Tela de configuração padrão de criação de nova sala.

Nessa situação, você precisa criar salas usando a API REST e conceder acesso a uma sala específica no token de acesso.

Configuração do cliente de vídeo

Até agora, temos um simples esqueleto de aplicativo do cliente. Vamos adicionar algumas funcionalidades a ele. Comece instalando o SDK twilio-video e o axios para fazer solicitações HTTP ao inserir o seguinte comando na linha de comando:

npm install twilio-video axios —save

Na pasta /app/, crie um arquivo VideoComponent.js. Ele conterá o componente do aplicativo que será incluído no arquivo principal/de entrada app.js.
Em VideoComponent.js, vamos criar um componente simplificado.

//app/VideoComponent.js
import React, { Component } from 'react';
import Video from 'twilio-video';
import axios from 'axios';

export default class VideoComponent extends Component {
 constructor(props) {
   super();
 }

 render() {
   return (
     <div>Video Component</div>
   );
 }
}

Em seguida, vamos incluir esse componente no arquivo principal app.js e adicionar o código destacado a esse arquivo:

hl_lines="3  10"
// app/app.js
// ... other import statements
import VideoComponent from './VideoComponent';

let dom = document.getElementById("app");
render(
    <MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}>
        <div>
            <AppBar title="React Twilio Video" />
            <VideoComponent />
        </div>
    </MuiThemeProvider>
    ,
    dom
);

De agora em diante, será tudo feito no arquivo "VideoComponent.js". Podemos iniciar o aplicativo ao inserir ‘npm start’ na linha de comando e acessar localhost:3000 no navegador.

Tela do navegador com o exemplo em execução e atualizado.

Quando um usuário carrega a página, esperamos que o aplicativo receba um token de acesso do servidor e use-o para entrar em uma sala. Já que permitimos que o cliente crie salas, também vamos incluir na interface uma seção onde os usuários possam entrar em uma sala. Se o usuário tentar entrar em uma sala que não existe, a sala vai ser criada automaticamente.

Como obter um token

Quando o componente é carregado, é feita uma chamada de API em componentDidMount para o servidor, que retorna um token com um nome aleatório.

Observação: é recomendável fazer chamadas de API ou realizar qualquer outra operação que cause efeitos colaterais em componentDidMount e não em componentWillMount porque, quando o componente componentDidMount é chamado, o componente é montado e uma atualização do estado garante atualizações em nodes (nós) do DOM. componentWillMount é chamado antes de o componente ser montado, e isso significa que, se uma chamada de API for concluída antes de render, não haverá componente a ser atualizado.

O cliente usa o token para entrar em uma sala. Isso significa que vamos precisar de variáveis de estado para armazenar alguns valores.

Também garantimos que um usuário só poderá se conectar a uma sala se for especificado um nome de sala; caso contrário, aparecerá um erro. O vídeo do usuário é exibido somente quando estiver conectado a uma sala.

Vamos atualizar gradualmente o "VideoComponent.js" e adicionar novas funcionalidades.
Primeiro, inicializamos diversas variáveis para rastrear o estado e inserimos algumas informações no construtor.

hl_lines="3  4  5  6  7  8  9  10  11  12"
constructor(props) {
    super();
    this.state = {
      identity: null,  /* Will hold the fake name assigned to the client. The name is generated by faker on the server */
      roomName: '',    /* Will store the room name */
      roomNameErr: false,  /* Track error for room name TextField. This will    enable us to show an error message when this variable is true */
      previewTracks: null,
      localMediaAvailable: false, /* Represents the availability of a LocalAudioTrack(microphone) and a LocalVideoTrack(camera) */
      hasJoinedRoom: false,
      activeRoom: null // Track the current active room
   };
 }

Com as variáveis necessárias definidas no estado, agora podemos fazer uma chamada de API para obter o token. Adicione a função "componentDidMount" abaixo do constructor (construtor).

hl_lines="4  5  6  7  8  9  10  11  12"
constructor(props) {
    //... skipped constructor content
}
componentDidMount() {
  axios.get('/token').then(results => {
    /*
Make an API call to get the token and identity(fake name) and  update the corresponding state variables.
    */
    const { identity, token } = results.data;
    this.setState({ identity, token });
  });
}

Precisamos importar alguns componentes de material-ui que adiciona as instruções a seguir em VideoComponent.js. Esses componentes serão usados no método render().

// ... other imports
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';
import { Card, CardHeader, CardText } from 'material-ui/Card';

Na sequência, excluímos o método render() existente e o substituímos pela nova versão a seguir. O método "render()" é responsável por solicitar a maioria dos métodos criados e por mostrar a interface.

render() {
  /* 
   Controls showing of the local track
   Only show video track after user has joined a room else show nothing 
  */
  let showLocalTrack = this.state.localMediaAvailable ? (
    <div className="flex-item"><div ref="localMedia" /> </div>) : '';   
  /*
   Controls showing of 'Join Room' or 'Leave Room' button.  
   Hide 'Join Room' button if user has already joined a room otherwise 
   show `Leave Room` button.
  */
  let joinOrLeaveRoomButton = this.state.hasJoinedRoom ? (
  <RaisedButton label="Leave Room" secondary={true} onClick={() => alert("Leave Room")}  />) : (
  <RaisedButton label="Join Room" primary={true} onClick={this.joinRoom} />);
  return (
    <Card>
    <CardText>
      <div className="flex-container">
    {showLocalTrack} {/* Show local track if available */}
    <div className="flex-item">
    {/* 
The following text field is used to enter a room name. It calls  `handleRoomNameChange` method when the text changes which sets the `roomName` variable initialized in the state.
    */}
    <TextField hintText="Room Name" onChange={this.handleRoomNameChange} 
errorText = {this.state.roomNameErr ? 'Room Name is required' : undefined} 
     /><br />
    {joinOrLeaveRoomButton}  {/* Show either 'Leave Room' or 'Join Room' button */}
     </div>
    {/* 
The following div element shows all remote media (other                             participant's tracks) 
    */}
    <div className="flex-item" ref="remoteMedia" id="remote-media" />
  </div>
</CardText>
    </Card>
  );
}

Agora, vamos implementar alguns dos métodos mencionados na função de renderização.

    handleRoomNameChange(e) {
  /* Fetch room name from text field and update state */
      let roomName = e.target.value; 
      this.setState({ roomName });
    }

    joinRoom() {
   /* 
Show an error message on room name text field if user tries         joining a room without providing a room name. This is enabled by setting `roomNameErr` to true
  */
        if (!this.state.roomName.trim()) {
            this.setState({ roomNameErr: true });
            return;
        }

        console.log("Joining room '" + this.state.roomName + "'...");
        let connectOptions = {
            name: this.state.roomName
        };

        if (this.state.previewTracks) {
            connectOptions.tracks = this.state.previewTracks;
        }

        /* 
Connect to a room by providing the token and connection    options that include the room name and tracks. We also show an alert if an error occurs while connecting to the room.    
*/  
Video.connect(this.state.token, connectOptions).then(this.roomJoined, error => {
  alert('Could not connect to Twilio: ' + error.message);
});
}

Para usar os dois métodos acima, precisamos associá-los no construtor.

hl_lines="6  7"
constructor(props) {
    super();
    this.state = {
        // ...
}
this.joinRoom = this.joinRoom.bind(this);
        this.handleRoomNameChange = this.handleRoomNameChange.bind(this);
}

Associar métodos define o contexto deles como VideoComponent e garante que qualquer chamada feita para eles usando this.methodName(…) utiliza VideoComponent como o contexto (this). Para saber mais sobre associação de métodos, consulte o artigo de Jason Arnold.

A função "joinRoom()" acima é muito importante porque dá origem a uma conexão com uma sala, o que leva os participantes locais e remotos a entrar na sala e trazer fluxos de mídia que podemos anexar ao DOM. Se a conexão for bem-sucedida, "joinRoom()" chama o "roomJoined(room)" passando para a instância da sala.

// Attach the Tracks to the DOM.
attachTracks(tracks, container) {
  tracks.forEach(track => {
    container.appendChild(track.attach());
  });
}

// Attach the Participant's Tracks to the DOM.
attachParticipantTracks(participant, container) {
  var tracks = Array.from(participant.tracks.values());
  this.attachTracks(tracks, container);
}

roomJoined(room) {
  // Called when a participant joins a room
  console.log("Joined as '" + this.state.identity + "'");
  this.setState({
    activeRoom: room,
    localMediaAvailable: true,
    hasJoinedRoom: true  // Removes 'Join Room' button and shows 'Leave Room'
  });

  // Attach LocalParticipant's tracks to the DOM, if not already attached.
  var previewContainer = this.refs.localMedia;
  if (!previewContainer.querySelector('video')) {
    this.attachParticipantTracks(room.localParticipant, previewContainer);
  }
    // ... more event listeners
}
// ... more code

Em seguida, associamos esses métodos no constructor (construtor), logo abaixo dos outros métodos.

this.roomJoined = this.roomJoined.bind(this);

A associação de "attachTracks" e "attachParticipantTracks" é opcional, uma vez que não estamos usando como manipuladores de eventos ou em qualquer função de retorno de chamada, por isso, o contexto deles será definido automaticamente para o componente.

Até o momento, a configuração acima lida somente com a entrada em uma sala e a exibição da faixa de vídeo local.
Podemos iniciar o aplicativo ao digitar "npm start" na linha de comando e acessar localhost:3000 no navegador. Depois de inserir o nome de uma sala e clicar em "Join Room" (Ingressar na sala), deve aparecer a seguinte janela. Por enquanto, o botão "Leave Room" (Sair da sala) só deve mostrar um alerta. Se tiver algum problema, o código completo até esta parte pode ser encontrado neste branch do GitHub.

Tela do navegador com o exemplo em execução e agora com o vídeo da câmera aberta.

Nas próximas etapas, vamos explorar como sair de uma sala e o que acontece quando outros participantes entram na sala.

Como sair de uma sala

Para sair de uma sala, chamamos "disconnect()" na sala ativa e atualizamos algumas variáveis de estado para atualizar a interface. Também muda o valor de hasJoinedRoom no estado para falso e, consequentemente, remove o botão "Leave Room" (Sair da sala) e mostra o botão "Join Room" (Ingressar na sala) no lugar dele.

// ... code before
leaveRoom() {
   this.state.activeRoom.disconnect();
   this.setState({ hasJoinedRoom: false, localMediaAvailable: false });
}
// ... code after

Também precisamos atualizar a função de renderização "joinOrLeaveRoomButton" para chamar "leaveRoom".


let joinOrLeaveRoomButton = this.state.hasJoinedRoom ? (
<RaisedButton label="Leave Room" secondary={true} onClick={this.leaveRoom} />
        ) : (
<RaisedButton label="Join Room" primary={true} onClick={this.joinRoom} />
        );

Em seguida, associamos o método leaveRoom() no constructor (construtor).


  constructor(props) {
    super();
    // other code

    this.joinRoom = this.joinRoom.bind(this);
    this.handleRoomNameChange = this.handleRoomNameChange.bind(this);
    this.leaveRoom = this.leaveRoom.bind(this);
  }

Como abordar outros participantes

Nesta seção, vamos atualizar a seção remoteMedia quando outro participante entrar na sala. Também vamos lidar com situações em que um participante remove uma de suas faixas ou sai da sala. Vamos começar com as funções que desconectam as faixas dos participantes da sala.

detachTracks(tracks) {
    tracks.forEach(track => {
      track.detach().forEach(detachedElement => {
        detachedElement.remove();
      });
    });
  }

detachParticipantTracks(participant) {
  var tracks = Array.from(participant.tracks.values());
  this.detachTracks(tracks);
}

Como de costume, associamos os métodos acima no constructor (construtor).

  constructor(props) {
    super();
    // other code

    this.joinRoom = this.joinRoom.bind(this);
    this.handleRoomNameChange = this.handleRoomNameChange.bind(this);
    this.leaveRoom = this.leaveRoom.bind(this);
    this.detachTracks = this.detachTracks.bind(this);
    this.detachParticipantTracks =this.detachParticipantTracks.bind(this);
  }

Na função roomJoined(room), adicionamos o seguinte trecho de código, que executa várias funcionalidades com base no evento acionado na instância de sala. As funcionalidades incluem:

  • Conectar faixas dos participantes da sala ao DOM.
  • Registrar as identidades dos participantes depois que entram na sala.
  • Desconectar faixas dos participantes do DOM quando saem da sala.
  • Desconectar a faixa de um único participante quando é desativada. Por exemplo, desconectar a faixa de áudio quando o participante desliga o microfone.
  • Desconectar todas as faixas do DOM quando um participante local sai de uma sala.
hl_lines="7  8  9  10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  50"
  roomJoined(room) {
    // ... existing code 
    if (!previewContainer.querySelector('video')) {
      this.attachParticipantTracks(room.localParticipant, previewContainer);
    }

    // Attach the Tracks of the room's participants.
    room.participants.forEach(participant => {
      console.log("Already in Room: '" + participant.identity + "'");
      var previewContainer = this.refs.remoteMedia;
      this.attachParticipantTracks(participant, previewContainer);
    });

    // Participant joining room
    room.on('participantConnected', participant => {
      console.log("Joining: '" + participant.identity + "'");
    });

    // Attach participant's tracks to DOM when they add a track
    room.on('trackAdded', (track, participant) => {
      console.log(participant.identity + ' added track: ' + track.kind);
      var previewContainer = this.refs.remoteMedia;
      this.attachTracks([track], previewContainer);
    });

    // Detach participant's track from DOM when they remove a track.
    room.on('trackRemoved', (track, participant) => {
      this.log(participant.identity + ' removed track: ' + track.kind);
      this.detachTracks([track]);
    });

    // Detach all participant's track when they leave a room.
    room.on('participantDisconnected', participant => {
      console.log("Participant '" + participant.identity + "' left the room");
      this.detachParticipantTracks(participant);
    });

    // Once the local participant leaves the room, detach the Tracks
    // of all other participants, including that of the LocalParticipant.
    room.on('disconnected', () => {
      if (this.state.previewTracks) {
        this.state.previewTracks.forEach(track => {
          track.stop();
        });
      }
      this.detachParticipantTracks(room.localParticipant);
      room.participants.forEach(this.detachParticipantTracks);
      this.state.activeRoom = null;
      this.setState({ hasJoinedRoom: false, localMediaAvailable: false });
    });  }

No trecho de código acima, outros participantes podem ingressar na sala e a faixa de vídeo pode ser vista na extrema direita da janela do navegador. Ela será desconectada do DOM quando os participantes saírem da sala. O aplicativo também desconecta todos os participantes quando o participante local se desconecta da sala. Em resumo, "roomJoined(room)" oferece acesso a diferentes propriedades da sala, incluindo participantes e eventos que podem ocorrer na sala. Na sequência, definimos o comportamento do aplicativo com base nesses eventos. Também controlamos a aparência do DOM pela atualização de diferentes variáveis de estado.

Como testar o aplicativo de vídeo

Vamos testar nosso aplicativo localmente ao executar "npm start" na linha de comando. Podemos acessar localhost:3000 no navegador. A interface deve ser parecida com aquela mostrada nas capturas de tela abaixo. Digite um nome para a sala e clique em "Join Room" (Ingressar na sala). Depois, abra uma janela do navegador e repita o processo com o mesmo nome de sala.

Um aplicativo de demonstração que pode ser usado está hospedado na Heroku para referência futura. Esta é uma captura de tela exibida antes da entrada, com um usuário e dois usuários.

Antes de entrar em uma sala

Tela do navegador com um formulário para informar o nome da sala e botão de entrar na sala.

Somente o participante local na sala
Tela do navegador com o exemplo em execução agora com controle de sala e vídeo da câmera aberta.

Outro participante entra na sala (dois navegadores separados)
Segunda tela do navegador aberta exibindo agora duas câmeras e o exemplo em execução.

Conclusão: Programmable Video com React e Node.js

A necessidade e a demanda por ferramentas de colaboração, como o Google Hangouts, têm sido cada vez maiores. A desvantagem dessas ferramentas empresariais é que não permitem que o usuário seja criativo para personalizá-las de acordo com o necessário. O Twilio Programmable Video conta com diversos recursos pré-implementados, o que permite que o usuário escolha o que quer e desenvolva ferramentas personalizadas com pouco trabalho.

Para ter acesso ao código completo, consulte o Repositório do GitHub e a Heroku para assistir a uma demonstração ao vivo do aplicativo. Acesse a documentação oficial da Twilio Video para JavaScript para saber mais sobre como controlar dispositivos de áudio e vídeo, compartilhamento de tela, uso da faixa de dados e muitos outros recursos.

Conclusões

  • O Twilio Programmable Video é uma API excelente, com muitos recursos de escolha.
  • A API é simples de configurar e usar, principalmente os eventos descritivos, como participantConnectedparticipantDisconnected e outros.
  • A API ajuda os desenvolvedores a criarem suas próprias ferramentas de colaboração personalizadas e escolher entre os vários recursos disponíveis.

Este artigo foi traduzido do original "Building a Video Chat App with Twilio Programmable Video and React". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.