Crie um aplicativo de chat por vídeo usando Python, JavaScript e Twilio Programmable Video
Tempo de leitura: 15 minutos
Devido aos esforços para conter a disseminação da COVID-19 no mundo, muitas pessoas começaram a trabalhar de casa e isso naturalmente fez aumentar o interesse por ferramentas de comunicação e colaboração.
Neste artigo, vamos analisar uma solução de videoconferência, mas em vez de recorrermos a um sistema de terceiros, vamos adotar a abordagem do tipo "faça você mesmo" e criar a nossa solução. Nosso sistema será executado em navegadores móveis e de desktop modernos, assim os participantes não precisarão baixar ou instalar um software. A parte do projeto relacionada ao servidor usará Python e a estrutura do Flask e a parte relacionada ao cliente será desenvolvida em JavaScript puro, com uma pitada de HTML e CSS para compor o cenário.
Preciso tranquilizar você caso esteja preocupado imaginando que este tutorial vai ser demorado, difícil e confuso. A mágica por trás desse projeto é o serviço Twilio Programmable Video, que se encarrega do trabalho pesado.
Veja abaixo uma chamada de teste à qual me conectei usando meu notebook e o celular.
Este artigo examina a implementação do projeto em detalhes, assim você pode ir acompanhando e desenvolvendo tudo no seu computador. Se você quiser baixar o projeto completo em vez de criá-lo passo a passo, acesse este repositório do GitHub: https://github.com/miguelgrinberg/flask-twilio-video.
Requisitos do tutorial
Para criar o projeto, você precisará do seguinte:
- Python 3.6 ou mais recente. Se o sistema operacional não fornece um interpretador Python, faça o download de um instalador em python.org.
- ngrok. Vamos usar este utilitário prático para conectar o aplicativo Flask em execução no sistema a um URL público ao qual o Twilio possa se conectar. Ele é necessário para a versão de desenvolvimento do aplicativo porque provavelmente o computador está atrás de um roteador ou firewall, o que faz com que não possa ser acessado diretamente na Internet. Se o ngrok não estiver instalado, baixe uma cópia para Windows, MacOS ou Linux.
- Uma conta da Twilio gratuita ou paga. Se você é novo na Twilio, obtenha uma conta gratuita agora mesmo! Com este link, você terá US$ 10 quando fizer o upgrade.
- Um navegador compatível com a biblioteca JavaScript Twilio Programmable Video (veja abaixo uma lista). Este requisito também se aplica aos usuários que você pretende convidar para utilizar o aplicativo depois de criado.
Navegadores compatíveis
Como a funcionalidade principal de vídeo e áudio do projeto é fornecida pelo Twilio Programmable Video, você precisa usar um navegador compatível com esses serviços. Esta é a lista atual de navegadores compatíveis:
- Android: Chrome e Firefox
- iOS: Safari
- Linux: Chrome e Firefox
- MacOS: Chrome, Firefox, Safari e Edge
- Windows: Chrome, Firefox e Edge
Confira a lista mais recente de navegadores compatíveis na documentação do Programmable Video.
Estrutura do projeto
Vamos começar criando o diretório onde armazenaremos os arquivos do projeto. Abra uma janela de terminal, encontre um diretório pai adequado e digite os seguintes comandos:
Seguindo a estrutura mais básica de aplicativo do Flask, agora vamos criar dois subdiretórios, static e templates para armazenar os arquivos que serão fornecidos ao cliente.
Configurar a conta da Twilio
Faça login na sua conta da Twilio para acessar o Console. Nessa página, você pode ver o "Account SID" (SID da conta) que foi atribuído à sua conta. Ele é importante porque identifica sua conta e é usado para autenticar solicitações à API da Twilio.
Como vamos precisar do Account SID (SID da conta) mais adiante, clique no botão "Copy to Clipboard" (Copiar para área de transferência), no lado direito. Em seguida, abra um novo arquivo chamado .env no editor de texto (observe o ponto à esquerda) e, colando o SID no local indicado, escreva o seguinte:
O serviço Programmable Video também requer uma API Key (chave de API) da Twilio para autenticação, por isso nesta etapa você adicionará uma à sua conta da Twilio. Para começar, vá até a seção API Keys (Chaves de API) do Console da Twilio.
Se você nunca criou uma chave de API antes, verá um botão "Create new API Key" (Criar chave de API). Se você já tiver uma ou mais chaves de API, verá um botão "+" vermelho para adicionar outras. Em ambos os casos, clique nesse botão para criar uma API Key (chave de API).
Digite videochat para o nome da chave (ou um nome de sua escolha), deixe o tipo de chave como "Standard" (Padrão) e clique no botão "Create API Key" (Criar chave de API).
Agora, você verá os detalhes da API Key (chave de API) que acabou de criar. Os valores "SID" e "SECRET" são usados para autenticação junto com o valor do Account SID (SID da conta) que salvamos anteriormente.
Abra o arquivo .env novamente no editor de texto e adicione duas linhas para registrar os detalhes da API key (chave de API):
Depois que a API Key (chave de API) for gravada com segurança no arquivo .env, você poderá sair da página API Keys (Chaves de API). Se você perder o segredo da chave de API, precisará gerar uma nova chave.
As informações contidas no arquivo .env são privadas. Não compartilhe esse arquivo com ninguém. Se você pretende armazenar o projeto no controle do código-fonte, é uma boa ideia configurar esse arquivo para que ele seja ignorado, já que você não quer confirmá-lo por engano.
Criar um ambiente virtual Python
Seguindo as práticas recomendadas, vamos criar um ambiente virtual para instalar as dependências do Python.
Se você estiver usando um sistema Unix ou MacOS, abra um terminal e digite os seguintes comandos para realizar as tarefas descritas acima:
Se você estiver seguindo o tutorial no Windows, digite os seguintes comandos em uma janela do prompt de comando:
O último comando usa pip
, o instalador de pacotes do Python, para instalar estes três pacotes que vamos usar no projeto:
- A Biblioteca auxiliar Python da Twilio, para trabalhar com as APIs da Twilio
- A estrutura do Flask, para criar o aplicativo da Web
- Python-dotenv, para importar o conteúdo do arquivo .env como variáveis de ambiente
Para sua referência, quando este tutorial foi lançado, estas eram as versões dos pacotes acima e das respectivas dependências:
Criar um servidor Web
Em nosso projeto, vamos desenvolver um aplicativo de página única. Ele será orientado por um servidor Web que fornecerá os arquivos HTML, CSS e JavaScript aos clientes e responderá a solicitações assíncronas emitidas do código JavaScript em execução no navegador.
Vamos começar pelo servidor Web, que é essencial no projeto. Depois que o servidor Web estiver em execução, vamos começar a adicionar todas as outras partes necessárias.
Conforme eu mencionei na seção de requisitos, vamos usar a estrutura do Flask para implementar a lógica no servidor Web. Como este projeto será simples, vamos codificar todo o servidor em um único arquivo chamado app.py.
Abaixo, você pode ver a primeira versão do nosso servidor Web. Copie o código em um arquivo app.py no diretório do projeto.
A variável app
é chamada de "instância do aplicativo". Ela fornece as funções de suporte de que precisamos para implementar o servidor Web. Usamos o decorator app.route
para definir um mapeamento entre os URLs e as funções do Python. Neste exemplo específico, quando um cliente solicitar o URL raiz do servidor, o Flask vai executar a função index()
e esperar que ela forneça a resposta. A implementação da função index()
renderiza um arquivo index.html que ainda vamos gravar. Esse arquivo incluirá a definição HTML da única e principal página Web do nosso aplicativo de chat por vídeo.
Embora nosso projeto esteja apenas começando, estamos prontos para iniciar o servidor Web. Se você estiver usando um computador Linux ou MacOS, utilize o seguinte comando:
Se estiver usando um computador Windows, utilize estes comandos:
Você verá algo parecido com o seguinte quando o servidor for iniciado:
Até agora, temos o servidor Web em execução e pronto para receber solicitações. Também ativamos o modo de depuração do Flask, que vai acionar o servidor Web para reiniciar sempre que forem feitas alterações no aplicativo; por isso, agora você pode deixar essa janela de terminal de lado enquanto começamos a codificar os componentes do projeto.
Se você tentar se conectar ao aplicativo usando o navegador, será exibido o erro "template not found" (modelo não encontrado) porque ainda não gravamos o arquivo index.html referenciado pela principal e única rota. Vamos gravar esse arquivo na próxima seção, então teremos uma primeira versão do aplicativo em execução.
Layout da página do aplicativo
O design da página será muito simples. Vamos incluir um título, um formulário da Web para o usuário inserir seu nome e entrar ou sair de chamadas de vídeo e, depois, a área de conteúdo, onde serão exibidos os fluxos de vídeo de todos os participantes. Por enquanto, vamos adicionar um espaço reservado para vídeo para nosso uso.
A página vai ser parecida com esta:
Para criar essa página, precisamos de uma combinação de HTML e CSS. Veja abaixo o arquivo templates/index.html.
A seção <head>
desse arquivo faz referência a um arquivo styles.css. Estamos usando a função url_for()
do Flask para gerar o URL correto. Isso é legal porque tudo o que precisamos fazer é colocar o arquivo no diretório static e deixar o Flask gerar o URL. Caso você esteja se perguntando qual é a diferença entre um arquivo de template (modelo) e um arquivo static (estático), é exatamente esta; os arquivos de template (modelo) podem ter espaços reservados que são gerados dinamicamente quando a função render_template()
acima é executada.
A seção <body>
da página define os seguintes elementos:
- Um título
<h1>
- Um elemento
<form>
com um campo de nome e um botão enviar - Um elemento
<p>
, que mostra o status da conexão e o número de participantes - Um contêiner
<div>
com um participante identificado com o nomelocal
, que mostra nosso feed de vídeo. Outros participantes serão adicionados de maneira dinâmica conforme ingressarem na chamada de vídeo - O
<div>
de cada participante contém um<div>
vazio no qual o vídeo será exibido e um segundo<div>
, onde vamos exibir o nome. - Links para dois arquivos JavaScript necessários: a versão oficial da biblioteca twilio-video.js e um app.js que vamos gravar em breve.
Este é o conteúdo do arquivo static/styles.css:
Essas definições de CSS são todas dedicadas ao layout do elemento container <div>
, que é estruturado como uma flexbox para que os participantes sejam adicionados automaticamente à direita e incluídos na próxima linha, conforme necessário, de acordo com o tamanho da janela do navegador.
A definição de .participant div:first-child
se aplica ao primeiro elemento filho de quaisquer elementos <div>
que têm a classe participant
. Aqui estamos limitando o tamanho do vídeo a 240 x 180 pixels. Também temos um plano de fundo mais escuro e uma borda preta, assim poderemos ver um espaço reservado para a janela de vídeo. O plano de fundo também será útil quando as dimensões do vídeo não corresponderem exatamente à proporção. Fique à vontade para ajustar essas opções conforme suas preferências.
Com os arquivos HTML e CSS implementados, o servidor deve poder responder ao navegador e mostrar o layout básico da página que vimos acima. Com o servidor em execução, abra o navegador e digite http://localhost:5000 na barra de endereços para ver a primeira versão do aplicativo em execução.
Exibir o feed de vídeo
Se você viu o log de rede do navegador, deve ter percebido que o navegador tentou carregar o arquivo app.js que referenciamos no final do arquivo index.html, mas não deu certo porque ainda não temos esse arquivo no projeto. Agora, vamos escrever a primeira função neste arquivo para adicionar o feed de vídeo à página.
Crie o arquivo e adicione o seguinte código a static/app.js:
A função addLocalVideo()
usa a biblioteca JavaScript Twilio Programmable Video para criar uma faixa de vídeo local. A função createLocalVideoTrack()
da biblioteca é assíncrona e retorna um objeto promise, por isso usamos o método then()
para adicionar alguma lógica em uma função de retorno de chamada após a criação da faixa de vídeo.
A função de retorno de chamada recebe um objeto LocalVideoTrack como argumento. Usamos o método attach() para adicionar o elemento de vídeo ao primeiro <div>
filho do elemento local
. Se isso estiver confuso, vamos analisar a estrutura da classe local participant no arquivo index.html:
Aqui podemos ver que o elemento local
tem dois elementos <div>
como filhos. O primeiro está vazio, que é o elemento ao qual estamos anexando o vídeo. O segundo elemento <div>
é para o rótulo exibido abaixo do vídeo.
Atualize a página no navegador para exibir o vídeo. A maioria dos navegadores pedirá permissão antes de ativar a câmera.
Gerar um token de acesso para um participante
A Twilio leva a segurança muito a sério. Antes de os usuários ingressarem em uma chamada de vídeo, o aplicativo deve verificar se eles têm permissão e gerar um token de acesso. Os tokens devem ser gerados no servidor Python, porque os objetos secret que armazenamos no arquivo .env são necessários nesse processo.
Em um aplicativo real, esse é o lugar onde o aplicativo autentica o usuário que deseja ingressar na chamada. A solicitação de conexão nesse aplicativo provavelmente incluiria uma senha, um cookie de autenticação ou alguma outra forma de identificação, além do nome de usuário. Um token de acesso à sala de chat por vídeo só seria gerado depois que o usuário que estivesse solicitando acesso à chamada de vídeo fosse devidamente autenticado.
Veja abaixo o arquivo app.py atualizado.
Como dito acima, precisamos dos três objetos secret que armazenamos no arquivo .env, por isso chamamos a função load_dotenv()
do pacote python-dotenv para importar esses objetos e atribuí-los a variáveis.
A geração do token acontece em uma nova rota que vamos chamar no lado do JavaScript, vinculada ao URL /login. A função receberá o nome de usuário em um payload JSON. Como esse é um aplicativo simples, na única autenticação que faremos no usuário, o nome de usuário não pode estar vazio. Se a validação falhar, será retornado um erro 401 para indicar que o usuário não tem acesso à chamada de vídeo. Como falamos acima, um aplicativo real implementaria um mecanismo de autenticação mais completo aqui.
O token é gerado usando a classe auxiliar AccessToken
da biblioteca auxiliar Python da Twilio. Anexamos uma concessão de vídeo para uma sala de chamada de vídeo com o nome "My Room" (Minha sala). Um aplicativo mais complexo pode trabalhar com mais de uma sala de chamada de vídeo e decidir em quais salas o usuário pode entrar.
O token é retornado em um payload JSON no seguinte formato:
Processar o formulário de conexão
Em seguida, vamos implementar o processamento do formulário de conexão na página da Web. O participante vai digitar o nome dele e clicar no botão "Join call" (Ingressar na chamada). Quando a conexão for estabelecida, o mesmo botão será usado para desconectar da chamada.
Para gerenciar o botão de formulário, precisamos anexar um manipulador para o evento click
. Veja abaixo as alterações feitas em static/app.js.
Você pode ver que agora temos algumas variáveis globais declaradas na parte superior. Quatro delas são para acesso prático a elementos na página, como o campo de entrada de nome, o botão de envio no formulário da Web e assim por diante. O booleano connected
controla o estado da conexão, principalmente para ajudar a decidir se é necessário clicar em um botão para se conectar ou desconectar. A variável room
armazenará o objeto da sala de chat por vídeo assim que o tivermos.
Na parte inferior do script, adicionamos a função connectButtonHandler()
ao evento click no botão de formulário. A função é um pouco longa, mas cuida principalmente da confirmação de que o usuário inseriu um nome e da atualização da aparência do botão quando o estado da conexão muda. Se você filtrar o gerenciamento de formulários, poderá ver que a conexão e a desconexão reais são processadas por duas funções: connect()
e disconnect()
, que vamos escrever nas próximas seções.
Conectar a uma sala de chat por vídeo
Agora, chegamos à parte mais importante (e também a mais complexa!) do nosso aplicativo. Para conectar um usuário à sala de chat por vídeo, o aplicativo JavaScript em execução no navegador deve executar duas operações em sequência. Primeiro, o cliente precisa contatar o servidor Web e solicitar um token de acesso para o usuário e, depois que o token for recebido, o cliente precisa chamar a biblioteca twilio-video com esse token para fazer a conexão.
A função connect()
retorna um objeto promise, que o autor da chamada pode usar para anexar ações a serem executadas depois que a conexão for estabelecida ou também para tratar erros. Internamente, o resultado de promise é controlado por meio das funções resolve()
e reject()
que são passadas como argumentos para a função de execução passada no construtor Promise()
. Você pode ver chamadas para essas funções em toda a lógica da conexão. Uma chamada para resolve()
acionará o retorno de chamada bem-sucedida do autor da chamada, enquanto uma chamada para reject()
fará o mesmo para o retorno de chamada com erro.
A lógica da conexão tem duas etapas, como indicado acima. Primeiro, usamos a função fetch()
do navegador para enviar uma solicitação à rota /login no aplicativo Flask que criamos acima. Essa função também retorna um objeto promise, por isso usamos os manipuladores then(...).catch(...)
para fornecer retornos de chamada bem-sucedidos e com falha.
Se ocorrer uma falha na chamada de fetch, vamos chamar reject()
para causar uma falha no objeto promise. Se a chamada for bem-sucedida, vamos decodificar o payload JSON na variável data
e, depois, chamar a função connect()
da biblioteca twilio-video passando o token recém-adquirido.
A chamada da conexão de vídeo também é um objeto promise, por isso, mais uma vez, continuamos encadeando as funções com then(...)
. O controlador de sucesso da conexão é a parte mais interessante, onde nos conectamos à sala de chat por vídeo; precisamos organizar a parte do container
da página para refletir isso.
Esse retorno de chamada bem-sucedido recebe um argumento _room
, que representa a sala de chamada de vídeo. Por ser uma variável muito útil, atribuímos _room
à variável global room
, para que o restante do aplicativo possa ter acesso à sala quando for preciso.
O array room.participants
contém a lista das pessoas que já estão na chamada. Para cada uma delas, precisamos adicionar uma seção <div>
, que mostra o vídeo e o nome. Tudo é encapsulado na função participantConnected()
, por isso a invocamos para cada participante. Também queremos que os futuros participantes sejam tratados da mesma forma, então configuramos um manipulador para o evento participantConnected
apontando para a mesma função. O evento participantDisconnected
é igualmente importante, pois queremos remover todos os participantes que saírem da chamada, então também criamos um manipulador para esse evento.
Agora, estamos totalmente conectados e podemos indicar isso na variável booleana connected
. A última ação é atualizar o elemento <p>
, que mostra o status da conexão, para exibir o número de participantes. Isso é feito em uma função separada, porque precisaremos fazer isso em vários lugares. A função atualiza o texto do elemento com base no comprimento do array room.participants
. Adicione a implementação dessa função a static/app.js.
O array room.participants
inclui todos os participantes, com exceção de nós, por isso o número total de pessoas em uma chamada é sempre um a mais do que o mostrado na lista.
Conectar e desconectar participantes
Na seção anterior, vimos que, quando um participante ingressa na chamada, chamamos o manipulador participantConnected
. Essa função precisa criar um <div>
dentro do elemento container
, seguindo a mesma estrutura que usamos para o elemento local
que mostra nosso fluxo de vídeo.
Veja abaixo a implementação da função participantConnected()
junto com a contrapartida participantDisconnected()
e algumas funções auxiliares, sendo que todas elas são incluídas emstatic/app.js.
O retorno de chamada participantConnected()
recebe um objeto Participant da biblioteca twilio-video. As duas propriedades importantes deste objeto são participant.sid
e participant.identity
, que são um identificador de sessão exclusivo e um nome, respectivamente. O atributo identity
vem diretamente do token que geramos. Lembre-se de que passamos identity=username
na função do Python de geração de token.
A estrutura HTML de um participante é semelhante à que usamos para o vídeo local. A grande diferença é que agora precisamos criar essa estrutura de maneira dinâmica usando a API DOM do navegador. Esta é a marcação que precisamos criar para cada participante:
No início da função participantConnected()
, criamos um participant_div
, ao qual adicionamos um tracks_div
e um label_div
como filhos. Por último, adicionamos participant_div
como filho de container
, que é o elemento <div>
principal em que temos todos os participantes da chamada.
A segunda parte da função envolve anexar as faixas de vídeo e de áudio ao elemento tracks_div
que acabamos de criar. Percorremos todas as faixas exportadas pelos participantes e, seguindo o uso básico mostrado na documentação da biblioteca, anexamos aquelas que assinamos. O processo de anexar as faixas é tratado em uma função auxiliar trackSubscribed()
definida logo abaixo.
Em usos mais avançados dessa biblioteca, um participante pode adicionar ou remover faixas de maneira dinâmica durante uma chamada (por exemplo, se ele desligar o vídeo temporariamente, silenciar o áudio ou até mesmo começar a compartilhar a tela). Como queremos responder a todas essas mudanças de faixa, também criamos manipuladores de eventos para os eventos trackSubscribed
e trackUnsubscribed
, que usam os métodos attach()
e detach()
do objeto track para adicionar e remover os elementos HTML que contêm os feeds.
Desconectar-se da sala de chat
A contrapartida da função connect()
é disconnect()
, que tem de restaurar o estado da página anterior à conexão. Isso é muito mais simples porque envolve principalmente a remoção de todos os filhos do elemento container
, exceto o primeiro, que é nosso fluxo de vídeo local.
Como você pode ver aqui, removemos todos os filhos do elemento container, começando do final e até chegarmos a <div>
com id local
, que criamos de maneira estática na página index.html. Também usamos a oportunidade de atualizar nossa variável global connected
, alteramos o texto do botão connect (conectar) e atualizamos o elemento <p>
para mostrar uma mensagem "Disconnected" (Desconectado).
Executar o servidor de chat por vídeo
Se você iniciou o servidor Web nos primeiros estágios deste tutorial, verifique se ele ainda está em execução. Se não estiver, inicie-o mais uma vez com o seguinte comando:
Se estiver usando um computador Windows, utilize estes comandos:
Com o servidor em execução, você pode se conectar do mesmo computador digitando http://localhost:5000 na barra de endereços do navegador. Mas, claro, é provável que você também queira se conectar de um segundo computador ou de um smartphone, ou talvez até convidar um amigo para participar do chat por vídeo. Isso requer mais uma etapa, porque o servidor está sendo executado apenas internamente no computador e não pode ser acessado pela Internet.
Existem algumas maneiras diferentes de expor o servidor à Internet. Um jeito rápido e fácil de fazer isso é usando o ngrok, um utilitário prático que cria um túnel entre nosso servidor em execução local e um URL público no domínio ngrok.io. Se o ngrok não estiver instalado, você poderá baixar uma cópia para Windows, MacOS ou Linux.
Com o servidor Flask em execução, abra uma segunda janela de terminal e inicie o ngrok da seguinte maneira:
Agora, o terminal vai mostrar algo parecido com esta tela:
Localize as linhas Forwarding
para ver qual é o URL público que o ngrok atribuiu ao servidor. Use o que começa com https://, pois muitos navegadores não permitem que sites não criptografados acessem a câmera e o microfone. No exemplo acima, o URL público é https://bbf1b72b.ngrok.io. O seu será parecido com este, mas o primeiro componente do domínio vai ser diferente sempre que você iniciar o ngrok.
Com o servidor Flask e o ngrok em execução no computador, você pode utilizar o URL público https:// do ngrok para se conectar ao servidor usando outros computadores e smartphones; então, agora você está pronto para convidar seus amigos para um chat por vídeo!
Conclusão
Espero que este tutorial tenha sido divertido e interessante. Se você decidir se basear nele para criar seu projeto de chat por vídeo, saiba que a API Twilio Programmable Video tem muito mais recursos que não exploramos, inclusive as opções para:
Mal posso esperar para ver o que você criou!
Publicações relacionadas
Recursos relacionados
Twilio Docs
De APIs a SDKs e aplicativos de amostra
Documentação de referência de API, SDKs, bibliotecas auxiliares, guias de início rápido e tutoriais para sua linguagem e plataforma.
Centro de Recursos
Os mais recentes e-books, relatórios do setor e webinars
Aprenda com especialistas em engajamento do cliente para melhorar sua própria comunicação.
Ahoy
Centro da comunidade de desenvolvedores da Twilio
Melhores práticas, exemplos de código e inspiração para criar comunicações e experiências de engajamento digital.