https://a.storyblok.com/f/270183/24748/69a7a753cd/blog_stream-video_1200x600.png

Transmita uma videochamada com a Video API da Vonage

Publicado em May 5, 2021

Tempo de leitura: 6 minutos

Esta série de tutoriais explorará a Video API da Vonage (anteriormente TokBox OpenTok) e o que você pode criar com ela. A Video API é muito robusta e altamente personalizável, e em cada postagem mostraremos como implementar um recurso específico usando a API. Desta vez, veremos como transmitir seu bate-papo por vídeo para um público que não está participando do bate-papo.

Como essa aplicação exigirá algum código do lado do servidor, vamos usar o Glitch para facilitar a configuração. Você também pode baixar o código deste projeto do Glitch e implantá-lo no seu servidor ou na plataforma de hospedagem de sua escolha (provavelmente será necessário fazer alguns ajustes de configuração de acordo com os requisitos da sua plataforma).

Não utilizaremos nenhum framework de front-end nesta série, apenas JavaScript puro, para manter o foco na própria Video API. Ao final deste tutorial, seu aplicativo de chat por vídeo também deverá oferecer uma opção para simplesmente assistir à transmissão do chat por vídeo.

Screenshot of viewer page

O código final desta aplicação pode ser encontrado neste repositório do GitHub ou modificado no Glitch.

Pré-requisitos

Antes de começarmos, você precisará de um Account da Video API da Vonage, que pode ser criado gratuitamente aqui. Você também precisará do do Node.js instalado (caso você não esteja usando o Glitch).

Este tutorial dá continuidade ao primeiro post introdutório da série: Criação de um chat de vídeo básico. Se esta for a primeira vez que você usa a Video API, recomendamos fortemente que você leia esse post, pois ele aborda a configuração básica a seguir:

  • Criar um projeto da Video API da Vonage

  • Configuração no Glitch

  • Estrutura básica do projeto

  • Inicializando uma sessão

  • Conectando-se à sessão, assinando e publicando

  • Estilos básicos de layout para um bate-papo por Video

Criação de tokens e funções

Todo usuário que se conecta a uma sessão precisa ser autenticado por meio de um token. A cada token é atribuída uma função, que determina o que o cliente pode fazer enquanto estiver conectado. Existem três funções disponíveis: Assinante, Editor e Moderador. Usaremos apenas os dois primeiros neste tutorial.

Um emissor pode se conectar a sessões, transmitir fluxos de áudio e Video para a sessão e se inscrever nas sessões de outros clientes. Um assinante pode se conectar a sessões e se inscrever nas sessões de outros clientes, mas não pode publicar na sessão.

Crude illustration of subscribers and publishers

Para este tutorial, forneceremos aos participantes tokens de editor, enquanto os espectadores receberão tokens de assinante.

Mais informações sobre tokens podem ser encontradas na documentação.

Configuração inicial

Como vamos desenvolver um chat de vídeo básico, comece remixando o projeto do chat de vídeo básico criado no tutorial anterior. Clique no grande botão “Remix” abaixo para fazer isso. 👇

remix this

Sua estrutura de pastas deve ficar mais ou menos assim:

Folder structure of the project

Conforme mencionado no início, o TokBox OpenTok agora é a Video API da Vonage. Não fizemos nenhuma alteração nos nomes dos nossos pacotes, portanto, você continuará fazendo referência ao OpenTok em seu código.

Se você tivesse feito um remix do projeto Glitch, seu server.js arquivo já deveria estar assim:

const express = require("express");
const app = express();
const OpenTok = require("opentok");
const OT = new OpenTok(process.env.API_KEY, process.env.API_SECRET);

let sessions = {};

app.use(express.static("public"));

app.get("/", (request, response) => {
  response.sendFile(__dirname + "/views/landing.html");
});

app.get("/session/:room", (request, response) => {
  response.sendFile(__dirname + "/views/index.html");
});

app.post("/session/:room", (request, response) => {
  const roomName = request.params.room;
  // Check if the session already exists
  if (sessions[roomName]) {
    // Generate the token
    generateToken(roomName, response);
  } else {
    // If the session does not exist, create one
    OT.createSession((error, session) => {
      if (error) {
        console.log("Error creating session:", error);
      } else {
        // Store the session in the sessions object
        sessions[roomName] = session.sessionId;
        // Generate the token
        generateToken(roomName, response);
      }
    });
  }
});

function generateToken(roomName, response) {
  // Configure token options
  const tokenOptions = {
    role: "publisher",
    data: `roomname=${roomName}`
  };
  // Generate token with the Video API Client SDK
  let token = OT.generateToken(
    sessions[roomName],
    tokenOptions
  );
  // Send the required credentials back to to the client
  // as a response from the fetch request
  response.status(200);
  response.send({
    sessionId: sessions[roomName],
    token: token,
    apiKey: process.env.API_KEY
  });
}

const listener = app.listen(process.env.PORT, () => {
  console.log("Your app is listening on port " + listener.address().port);
});

Para colocar o chat por vídeo em funcionamento, acesse o .env arquivo e preencha com sua chave de API e seu segredo do projeto, que você encontra no painel de controle. Depois disso, faremos algumas adições ao projeto para disponibilizar uma interface para os espectadores.

Adicione a marcação necessária

Nosso aplicativo será composto por três páginas: uma página inicial para que os usuários criem ou participem de uma sessão, bem como para selecionar se desejam ser espectadores ou participantes, e as duas páginas de videochamada para cada função, respectivamente.

Precisaremos criar uma página adicional para o visualizador. Vamos adicionar um viewer.html arquivo à views pasta clicando no botão botão “Novo Arquivo” na barra lateral esquerda. Nomeie o arquivo views/viewer.html e cole a seguinte marcação na página. Essa página é quase exatamente igual ao index.html arquivo, exceto que não possui um div editor.

Add a viewer.html to the views folder

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Stream your video chat</title>
    <meta
      name="description"
      content="Stream a basic audio-video chat with Vonage Video API in Node.js"
    />
    <link
      id="favicon"
      rel="icon"
      href="https://tokbox.com/developer/favicon.ico"
      type="image/x-icon"
    />
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <link rel="stylesheet" href="/style.css" />
  </head>

  <body>
    <header>
      <h1>Viewer</h1>
    </header>

    <main>
      <div id="subscriber" class="subscriber"></div>
    </main>

    <footer>
      <p>
        <small
          >Built on <a href="https://glitch.com">Glitch</a> with the
          <a href="https://tokbox.com/developer/">Vonage Video API</a>.</small
        >
      </p>
    </footer>

    <script src="https://static.opentok.com/v2/js/opentok.min.js"></script>
    <script src="/viewer.js"></script>
  </body>
</html>

O viewer.html e o index.html arquivo utilizará arquivos de script diferentes, pois sua implementação é ligeiramente diferente devido às respectivas funções dos tokens, conforme explicado na seção acima.

A seguir, faremos algumas modificações no formulário da landing.html página para incluir uma opção que permita aos usuários selecionar suas funções por meio de botões de opção. Se eles selecionarem “Espectador”, serão direcionados para a página que exibe a transmissão do bate-papo por Video. Se selecionarem Participante, aparecerá outro campo de texto para o nome de usuário, que será usado para identificar sua transmissão.

<form id="registration" class="registration">
  <label>
    <span>Room</span>
    <input
      type="text"
      name="room-name"
      placeholder="Enter room name"
      required
    />
  </label>
  
  <!-- Add the user type radio buttons -->
  <p>Select your role:</p>
  <fieldset id="userRoles">
    <label>
      <input type="radio" name="user-type" value="viewer" checked />
      <span>Viewer</span>
    </label>

    <label>
      <input type="radio" name="user-type" value="participant" />
      <span>Participant</span>
    </label>
  </fieldset>
  
  <!-- Add the user name input field and label -->
  <label id="userName" class="hidden">
    <span>User name</span>
    <input type="text" name="user-name" placeholder="Enter your name" />
  </label>

  <button>Enter</button>
</form>

Definir o estilo do formulário da página de destino

Vamos complementar os estilos existentes para acomodar o novo fieldset e os botões de opção.

fieldset {
  border: 0;
  display: flex;
  justify-content: space-between;
  margin-bottom: 1em;
}

fieldset label {
  padding: 0.25em 0em;
  cursor: pointer;
}

.hidden {
  display: none;
}

Basic styles for fieldset and radio buttons

Reestruturar o JavaScript do lado do cliente

Vamos trabalhar na landing.html página primeiro. Para mostrar ou ocultar o campo do nome de usuário de forma condicional, podemos adicionar um ouvinte de evento que verifique o valor do botão de opção selecionado e altere os estilos de acordo com isso.

const userRoles = document.getElementById("userRoles");
const userName = document.getElementById("userName");
const userNameField = document.querySelector('[name="user-name"]');
userRoles.addEventListener(
  "click",
  event => {
    if (event.target.value === "participant") {
      userName.classList.remove("hidden");
      userNameField.required = true;
    } else {
      userName.classList.add("hidden");
      userNameField.required = false;
    }
  },
  false
);

Também precisamos modificar a lógica para direcionar nossos usuários às páginas corretas, dependendo de qual visualizador eles escolheram visualizador ou participante. Os espectadores serão direcionados para /session/viewer/ROOM_NAME , enquanto os participantes serão direcionados para /session/participant/ROOM_NAME?username=USER_NAME. Estamos utilizando a string de consulta na URL para passar o nome de usuário ao servidor.

const form = document.getElementById("registration");
form.addEventListener("submit", event => {
  event.preventDefault();
  
  // Check the selected option and redirect accordingly
  const isViewer = form.elements["user-type"].value === "viewer";

  if (isViewer) {
    location.href = `/session/viewer/${form.elements["room-name"].value}`;
  } else {
    location.href = `/session/participant/${form.elements["room-name"].value}?username=${form.elements["user-name"].value}`;
  }
});

A seguir, vamos criar o viewer.js arquivo para a viewer.html página. Da mesma forma que fizemos para o viewer.html, clique em “Novo Arquivo” novamente, mas, desta vez, adicione os arquivos de JavaScript à public pasta.

A pasta do seu projeto deve estar assim agora:

Project structure after all files are added

O viewer.js arquivo é um pouco mais curto que o client.js arquivo, pois não inclui a criação de um editor. Estamos fazendo uma POST solicitação para /session/viewer/ROOM_NAME e recebendo os dados de resposta necessários para nos conectarmos a uma sessão.

fetch(location.pathname, { method: "POST" })
  .then(res => {
    return res.json();
  })
  .then(res => {
    const apiKey = res.apiKey;
    const sessionId = res.sessionId;
    const token = res.token;
    initializeSession(apiKey, sessionId, token);
  })
  .catch(handleCallback);

function initializeSession(apiKey, sessionId, token) {
  // Create a session object with the sessionId
  const session = OT.initSession(apiKey, sessionId);

  // Connect to the session
  session.connect(token, error => handleCallback(error));

  // Subscribe to a newly created stream
  session.on("streamCreated", event => {
    session.subscribe(
      event.stream,
      "subscriber",
      {
        insertMode: "append",
        width: "100%",
        height: "100%",
        name: event.stream.name
      },
      handleCallback
    );
  });
}

// Callback handler
function handleCallback(error) {
  if (error) {
    console.log("error: " + error.message);
  } else {
    console.log("callback success");
  }
}

Precisamos fazer alguns pequenos ajustes no client.js arquivo também, pois queremos nomear o stream de cada participante com o nome de usuário que ele digitou na página inicial.

fetch(location.pathname, { method: "POST" })
  .then(res => {
    return res.json();
  })
  .then(res => {
    const apiKey = res.apiKey;
    const sessionId = res.sessionId;
    const token = res.token;
    // Declare the stream name and pass it to the initializeSession() function
    const streamName = res.streamName;
    initializeSession(apiKey, sessionId, token, streamName);
  })
  .catch(handleCallback);

A initializeSession() função agora aceitará mais um parâmetro para streamName e será usada no initPublisher() método e no subscribe() método. Ambos os métodos aceitam um argumento opcional de propriedades, o que nos permite passar opções de personalização para os fluxos.

// Create a publisher
const publisher = OT.initPublisher(
  "publisher",
  {
    insertMode: "append",
    width: "100%",
    height: "100%",
    name: streamName
  },
  handleCallback
);

// Subscribe to a newly created stream
session.on("streamCreated", event => {
  session.subscribe(
    event.stream,
    "subscriber",
    {
      insertMode: "append",
      width: "100%",
      height: "100%",
      name: event.stream.name
    },
    handleCallback
  );
});

Seus arquivos finais client.js ficarão assim:

fetch(location.pathname, { method: "POST" })
  .then(res => {
    return res.json();
  })
  .then(res => {
    const apiKey = res.apiKey;
    const sessionId = res.sessionId;
    const token = res.token;
    const streamName = res.streamName;
    initializeSession(apiKey, sessionId, token, streamName);
  })
  .catch(handleCallback);

function initializeSession(apiKey, sessionId, token, streamName) {
  // Create a session object with the sessionId
  const session = OT.initSession(apiKey, sessionId);

  // Create a publisher
  const publisher = OT.initPublisher(
    "publisher",
    {
      insertMode: "append",
      width: "100%",
      height: "100%",
      name: streamName
    },
    handleCallback
  );

  // Connect to the session
  session.connect(token, error => {
    // If the connection is successful, initialize the publisher and publish to the session
    if (error) {
      handleCallback(error);
    } else {
      session.publish(publisher, handleCallback);
    }
  });

  // Subscribe to a newly created stream
  session.on("streamCreated", event => {
    session.subscribe(
      event.stream,
      "subscriber",
      {
        insertMode: "append",
        width: "100%",
        height: "100%",
        name: event.stream.name
      },
      handleCallback
    );
  });
}

// Callback handler
function handleCallback(error) {
  if (error) {
    console.log("error: " + error.message);
  } else {
    console.log("callback success");
  }
}

Tratamento de rotas no lado do servidor

A última etapa antes de tudo se encaixar é o server.js arquivo, onde as rotas são definidas. Precisaremos configurar as rotas para atender o página do (viewer.html) e também a página (index.html), respectivamente.

app.get("/session/participant/:room", (request, response) => {
  response.sendFile(__dirname + "/views/index.html");
});

app.get("/session/viewer/:room", (request, response) => {
  response.sendFile(__dirname + "/views/viewer.html");
});

Em vez da função remixada generateToken() , usaremos duas funções diferentes para as duas funções.

function generatePublisherToken(roomName, streamName, response) {
  // Configure token options
  const tokenOptions = {
    role: "publisher",
    data: `roomname=${roomName}?streamname=${streamName}`
  };
  // Generate token with the OpenTok SDK
  let token = OT.generateToken(
    sessions[roomName],
    tokenOptions
  );
  // Send the required credentials back to to the client
  // as a response from the fetch request
  response.status(200);
  response.send({
    sessionId: sessions[roomName],
    token: token,
    apiKey: process.env.API_KEY,
    streamName: streamName
  });
}

function generateSubscriberToken(roomName, response) {
  // Configure token options
  const tokenOptions = {
    role: "subscriber",
    data: `roomname=${roomName}`
  };
  // Generate token with the OpenTok SDK
  let token = OT.generateToken(
    sessions[roomName],
    tokenOptions
  );
  // Send the required credentials back to to the client
  // as a response from the fetch request
  response.status(200);
  response.send({
    sessionId: sessions[roomName],
    token: token,
    apiKey: process.env.API_KEY
  });
}

Para os espectadores, assim que a página do Viewer for carregada, o nome da sala será enviado ao servidor por meio de uma POST solicitação. Isso será processado pela seguinte rota:

app.post("/session/viewer/:room", (request, response) => {
  const roomName = request.params.room;
  // Check if the session already exists
  if (sessions[roomName]) {
    // Generate the token
    generateSubscriberToken(roomName, response);
  } else {
    // If the session does not exist, create one
    OT.createSession((error, session) => {
      if (error) {
        console.log("Error creating session:", error);
      } else {
        // Store the session in the sessions object
        sessions[roomName] = session.sessionId;
        // Generate the token
        generateSubscriberToken(roomName, response);
      }
    });
  }
});

Da mesma forma, para os participantes, assim que a página do Participante for carregada, o nome da sala e o nome de usuário serão enviados ao servidor por meio de uma POST solicitação, e a rota correspondente é tratada da seguinte forma:

// Middleware to read the body of the request
app.use(express.json());

app.post("/session/participant/:room", (request, response) => {
  const roomName = request.params.room;
  const streamName = request.body.username;
  // Check if the session already exists
  if (sessions[roomName]) {
    // Generate the token
    generatePublisherToken(roomName, streamName, response);
  } else {
    // If the session does not exist, create one
    OT.createSession((error, session) => {
      if (error) {
        console.log("Error creating session:", error);
      } else {
        // Store the session in the sessions object
        sessions[roomName] = session.sessionId;
        // Generate the token
        generatePublisherToken(roomName, streamName, response);
      }
    });
  }
});

E, com isso, os espectadores poderão assistir às transmissões dos participantes em uma única página, enquanto os participantes estiverem conversando por Video entre si.

Screenshot of viewer page

Screenshot of participant page

Não deixe de conferir o código final em Glitch ou no GitHub e fique à vontade para remixar ou clonar o código e brincar com ele por conta própria.

E agora?

Existem funcionalidades adicionais que podemos desenvolver com a Video API da Vonage, que serão abordadas em tutoriais futuros; mas, enquanto isso, você pode saber mais em nosso site de documentação completa. Se você tiver algum problema ou dúvida, entre em contato conosco na nossa Slack da Comunidade. Obrigado pela leitura!

Compartilhar:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing ChenEx-funcionários da Vonage

Hui Jing é Developer Advocate na Nexmo. Ela tem um amor imenso por CSS e tipografia e, de modo geral, é apaixonada por tudo que diz respeito à web.