WebRTC – Criando uma Sala de Atendimento com Vídeo, sem uso de servidores.

Tempo de Leitura: 9 Minutos

Com a crescente necessidade de nos comunicarmos e com a recente pandemia mundial causada pelo coronavírus, é comum que maneiras alternativas de comunicação sejam criadas, e a procura por serviços pagos e gratuitos é crescente, porém, com o objetivo comercial esses serviços acabam custando altos valores e nem sempre são utilizados com a devida eficiência, o objetivo deste artigo, é apresentar usa solução que possa ser implementada de maneira imediata e estar disponível para por exemplo profissionais liberais como médicos e terapeutas para uma consulta a distância, psicólogos, advogados, contabilistas, profissionais de RH como recrutadores, e mesmo pessoas que precisem se comunicar através de um link de áudio e vídeo  independentes.

Sempre que vemos o WebRTC para a manipulação de vídeo, áudio e texto (chat) entre usuários, caímos no uso de serviços com NodeJS que não estão disponíveis em qualquer plano de hospedagem, então, o objetivo deste artigo é permitir a criação de salas sem a necessidade de um serviço de hospedagem complexo, onde qualquer plano possa ser utilizado. Pois só utiliza HTML e JAVASCRIPT e os servidores, usamos um gratuito, somente para fazer a troca dos IPs e fechar o link. Vamos ao código?A primeira coisa a se entender aqui, é que é algo simples, mais que pode ser usado tranquilamente para uma conversa pública, onde as salas são criadas de maneira dinâmica, ou seja, você cria a sala e passa o endereço para o interlocutor que assim que acessar os servidores vão realizar as devidas conexões e fechar a sala; da mesma maneira quando é encerrada a comunicação a sala é dinamicamente destruída, ou seja, nada é salvo, exceto se a tela for gravada ou capturada.

O script é baseado em WebRTC, com ele, você pode adicionar recursos de comunicação em tempo real ao seu aplicativo, que funcionam sobre um padrão aberto. Ele suporta dados de vídeo, voz e genéricos a serem enviados entre pares, permitindo que os desenvolvedores criem soluções poderosas de comunicação de voz e vídeo. A tecnologia está disponível em todos os navegadores modernos, bem como em clientes nativos para todas as principais plataformas. As tecnologias por trás do WebRTC são implementadas como um padrão da Web aberto e disponíveis como APIs JavaScript regulares em todos os principais navegadores. O projeto WebRTC é de código aberto e suportado pela Apple, Google, Microsoft e Mozilla, entre outros.

Existem muitos casos de uso diferentes para o WebRTC, desde aplicativos da Web básicos que usam a câmera ou microfone até aplicativos de videochamada mais avançados e compartilhamento de tela, porém, neste primeiro momento, vamos fazer algo simples, somente para uma vídeo chamada entre 2 usuários, com uso da câmera e do microfone.

Um aplicativo WebRTC geralmente passa por um fluxo comum, acessando os dispositivos de mídia, abrindo conexões de pares, descobrindo pares e iniciando a transmissão. Basicamente usamos 2 APIs diferentes, uma para o acesso e o controle de mídia e outra para o fechamento dos pares que é a interconexão dos usuários.

Dividi o projeto em somente 2 arquivos, sendo o HTML que será chamado para apresentar as salas e interligar todos os recursos e o CSS para a personalização da sala e o JS contendo todo o código para a manipulação das APIs e chamadas para os servidores externos para a troca dos pares.

A primeira coisa a fazer, é escolher um servidor STUN público como os da lista a seguir:

stun:stun01.sipphone.com
stun:stun.ekiga.net
stun:stun.fwdnet.net
stun:stun.ideasip.com
stun:stun.iptel.org
stun:stun.rixtelecom.se
stun:stun.schlund.de
stun:stun.l.google.com:19302
stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
stun:stun3.l.google.com:19302
stun:stun4.l.google.com:19302
stun:stunserver.org
stun:stun.softjoys.com
stun:stun.voiparound.com
stun:stun.voipbuster.com
stun:stun.voipstunt.com
stun:stun.voxgratia.org
stun:stun.xten.com

Depois, usamos um outro serviço gratuito que permite a troca de mensagens entre usuários, chamado https://scaledrone.com que é um serviço que tem por finalidade facilitar o uso se Socket na web. depois, a única coisa que precisamos é colocar o nosso CHANNEL_ID no lugar identificado.

O código HTML usado foi o seguinte:

<html>
  <head>
    <script type='text/javascript' src='https://cdn.scaledrone.com/scaledrone.min.js'></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css"
      integrity="sha256-L/W5Wfqfa0sdBNIKN9cG6QA5F2qx4qICmU2VgLruv9Y=" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"
      integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/js/bootstrap.min.js"
      integrity="sha256-WqU1JavFxSAMcLP2WIOI+GB2zWmShMI82mTpLDcqFUg=" crossorigin="anonymous"></script>
    <style>
    body {
        background: black;
        background-position: center;
        display: flex;
        height: 100vh;
        margin: 0;
        align-items: center;
        justify-content: center;
        padding: 0 50px;
        font-family: -apple-system, BlinkMacSystemFont, sans-serif;
        color: white;
      }

      .movie-area {
        min-height: 60vh;
        display: flex;
        justify-content: space-around;
        align-items: center;
      }

      #localVideo {
        /* background: #d21d86; */
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
        max-width: calc(50% - 5%);
        margin: 0 5%;
        box-sizing: border-box;
        border-radius: 14px;
        padding: 0;
        border: 4px solid #d21d86;
      }

      #remoteVideo {
        background-repeat: no-repeat;
        background-position: center;
        background-size: contain;
        max-width: calc(50% - 5%);
        margin: 0 5%;
        box-sizing: border-box;
        border-radius: 14px;
        padding: 0;
        border: 4px solid #d21d86;
      }
    </style>
  </head>
  <body>
  <section>
    <script src="script.js"></script>
    <h1>Teste WebRTC Meeting</h1>
    <h3>Não se esqueça de ligar sua WEBCAM e MICROFONES</h3>
    <section class="movie-area">
      <video id="localVideo" autoplay muted></video>
      <video id="remoteVideo" autoplay></video>
    </section>
    <p>Aguarde o outro participante abrir o vídeo, e dar inicio a chamada.</p>
  </section>
  </body>
</html>

Este arquivo não precisa muito de explicação, já que na primeira parte, importa de algumas CDNs os arquivos de trabalho para acesso a API do SCALEDRONE, os arquivos JS do Bootstrap e do JQuery e seus respectivos CSS base. No segundo grupo de instruções são criadas as classes CSS para a customização e layout das abas de vídeo. Por fim, importo o arquivo proprietário de acesso as APIs do WebRetc e Peer e criamos as tags para abrigar cada um dos fluxos de vídeo, além de algumas informações textuais relevantes.

A mágica ocorre no script.js que é o arquivo que faz o acesso e o controle, dos fluxos de vídeo. Veja o conteúdo dele e logo em seguida vamos fazer uma explicação detalhada de cada uma das funções, bem como onde você pode implementar outros recursos, como controles e além de estender essas funcionalidades.

// Gera um nome randomico para a sala, se nenhum foi passado, para passar basta inserir uma ancora com até 16 caracteres #NomeDaSala
if (!location.hash) {
  location.hash = Math.floor(Math.random() * 0xFFFFFF).toString(16);
}
const roomHash = location.hash.substring(1);

// Cria uma conexão com o ScaleDrone
const drone = new ScaleDrone('yiS12Ts5RdNhebyM'); // insira aqui o seu CHANNEL_ID Proprietário, este código é referente a um código de demo do scaledrone, que pode não funcionar, pois é limitado a 3 usuários, troque pelo seu próprio, que pode ser de um plano gratuíto.

// Cria o nome físico da sala, no scaledrone, por uma régra deles, deve ser precedido da string 'observable-'
const roomName = 'observable-' + roomHash;
const configuration = {
  iceServers: [{
    // Coloque aqui o endereço do servidor STUN que desejar utilizar
    urls: 'stun:stun.voipstunt.com'
  }]
};
let room;
let pc;

//este par de funções serve somente par direcionar as mensagens para a CONSOLE do navegador, ou caso seja importante poderia-mos direcionar para um email, um banco de dados, entre outros.
function onSuccess(msg) {
    console.log(msg); // aqui na verdade, é uma simples LOG de que foi bem sucedida uma chamada
};
function onError(error) {
  console.error(error); // aqui é simplesmente uma mensagem que informa o erro caso algum tenha ocorrido
};

//abre a comunicação com o servidor de signaling para verificar quem está na sala
drone.on('open', error => {
  if (error) {
    return onError(error);
  }
  room = drone.subscribe(roomName);
  room.on('open', error => {
    if (error) {
      onError(error);
    }
  });
  // Se receber-mos um array com pelo menos 2 membros, significa que a sala está pronta e os servidores de sinalização ativos.
  room.on('members', members => {
    onSuccess(members);
    // Verifica se você é o primeiro ou segundo usuário, se for o segundo, então abrimos a oferta de troca de conexões para habilitar os fluxos de vídeo e audio WebRTC
    const isOfferer = members.length === 2;
    startWebRTC(isOfferer);
  });
});

// Esta mensagem, é a sinalização feita dentro do scaledrone que notifica um e o outro clientes com os dados para o WebRTC
function sendMessage(message) {
  drone.publish({
    room: roomName,
    message
  });
}

function startWebRTC(isOfferer) {
  pc = new RTCPeerConnection(configuration);

  // 'onicecandidate' notifica um ou outro participante para permitir a troca de comunicação pelo WebRTC
  pc.onicecandidate = event => {
    if (event.candidate) {
      sendMessage({'candidate': event.candidate});
    }
  };

  // Se é o segundo usuário, 'negotiationneeded' é criado e passa a oferecer o fluxo de dados entre os usuários
  if (isOfferer) {
    pc.onnegotiationneeded = () => {
      pc.createOffer().then(localDescCreated).catch(onError);
    }
  }

  // Quando recebemos a notificação de fluxo de vídeo recebido, apresentamos ele dentro do elemento #remoteVideo
  pc.ontrack = event => {
    const stream = event.streams[0];
    if (!remoteVideo.srcObject || remoteVideo.srcObject.id !== stream.id) {
      remoteVideo.srcObject = stream;
    }
  };

  navigator.mediaDevices.getUserMedia({
    audio: true,  //Exige que o audio esteja habilitado e permitido
    video: true,  //Exige que o video esteja habilitado e permitido
  }).then(stream => {
    // Mostra o fluxo de vídeo aberto no elemento #localVideo
    localVideo.srcObject = stream;
    // Notifica os pares presente que este é o fluxo de vídeo
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
  }, onError);

  // Fica escutando o Scaledrone para receber os fluxos de troca de dados
  room.on('data', (message, client) => {
    // verifica se mandamnos os dados da nossa conexão
    if (client.id === drone.clientId) {
      onSuccess(message);
      onSuccess(client);
      return;
    }

    if (message.sdp) {
      // Quando recebemos a mensagem do outro cliente para fechar os pares
      pc.setRemoteDescription(new RTCSessionDescription(message.sdp), () => {
        // Respondemos a mensagem com nossos dados
        if (pc.remoteDescription.type === 'offer') {
          pc.createAnswer().then(localDescCreated).catch(onError);
        }
      }, onError);
    } else if (message.candidate) {
      // Adiciona um novo ICE para esperar nova conexão
      pc.addIceCandidate(
        new RTCIceCandidate(message.candidate), onSuccess, onError
      );
    }
  });
}

function localDescCreated(desc) {
  pc.setLocalDescription(
    desc,
    () => sendMessage({'sdp': pc.localDescription}),
    onError
  );
}

Basicamente, a comunicação ocorre da seguinte maneira, primeiro criamos uma sala de troca de mensagens no servidor scaledrone para fechar os pares, criando uma sala única, depois verificamos se temos um segundo usuário e a troca de dados é feita (veja que essa troca até o fechamento da conexão, pode ser tentada várias vezes, pois os servidor TURN localiza o IP do seu outro cliente, e testa diversas portas para achar o melhor fluxo e formato) éssa troca de dados ocorre pois o WebRTC faz uma conexão entre você, o servidor STUN, o servidor localiza o segundo usuário e cria uma conexão entre VOCÊ e o OUTRO USUÁRAIO, veja o exemplo dos fluxos na imagem abaixo

A Study of WebRTC Security · A Study of WebRTC Security

Basicamente, o fluxo de vídeo ocorre entre você e o seu interlocutor, independente do servidor, este servidor externo, signaling, usa um outro servidor STUN para pegar o endereço externo ou público da sua conexão e da conexão do outro usuário, no nosso caso, são serviços diferentes, porém, poderiam ser todos os serviços executados no mesmo servidor.

Pronto, basicamente esse script é suficiente para criar um link de audio e vídeo similar a dezenas de aplicativos de chamada. Você pode estender para permitir a criação de outros fluxos, permitir mais usuários assistirem simultaneamente, criando uma sala de aula virtual (1 transmissor e vários receptores) ou mesmo um bate-papo com múltiplos usuários (vários transmissores e vários receptores), e uma infinidade de opções a sua determinada escolha.

A idéia básica aqui é por exemplo, permitir que um profissional liberal como um advogado, médico, ou outro prestador de serviços possa ter uma sala de reunião virtual implementada e integrada diretamente ao seu servidor web, por exemplo, permitindo que o cliente chame-o diretamente através do próprio link sem necessidade de instalar novos aplicativos.

O WebRTC funciona, na maioria dos browsers disponíveis no mercado. Tanto em celulares, como em tablets, computadores e outros dispositivos.