https://d226lax1qjow5r.cloudfront.net/blog/blogposts/stream-a-video-chat-with-vonage-video-api-dr/Blog_Stream-Video_1200x600.png

Transmite un Video Chat con Vonage Video API

Publicado el May 5, 2021

Tiempo de lectura: 6 minutos

Esta serie de tutoriales explorará la Video API de Vonage (anteriormente TokBox OpenTok) y lo que puedes crear con ella. La Video API es muy robusta y altamente personalizable, y en cada post mostraremos cómo implementar una función específica usando la API. Esta vez vamos a ver cómo transmitir su video chat a un público que no está en el chat.

Como esta aplicación requerirá algo de código del lado del servidor, utilizaremos Glitch para facilitar la configuración. También puede descargar el código de este proyecto Glitch y desplegarlo en su servidor o plataforma de alojamiento de elección (probablemente puede requerir algunos ajustes de configuración en función de los requisitos de su plataforma).

No vamos a utilizar ningún marco de front-end para esta serie, sólo Javascript vainilla para mantener el foco en la propia Video API. Al final de este tutorial, su aplicación de chat de vídeo también debe proporcionar una opción para simplemente ver el flujo de chat de vídeo.

Screenshot of viewer page

El código final para esta aplicación se puede encontrar en este repositorio GitHub o en remezclado en Glitch.

Requisitos previos

Antes de comenzar, necesitarás una cuenta de Video API de Vonage, que puedes crear gratis aquí. También necesitarás Node.js instalado (si no estás usando Glitch).

Este tutorial se basa en el primer post introductorio de la serie: Creación de un Video Chat Básico. Si es la primera vez que utilizas la Video API, te recomendamos que lo leas porque cubre la siguiente configuración básica:

  • Crear un proyecto de Video API de Vonage

  • Instalación en Glitch

  • Estructura básica del proyecto

  • Iniciar una sesión

  • Conectarse a la sesión, suscribirse y publicar

  • Estilos básicos de diseño para un chat de vídeo

Creación de tokens y funciones

Cada usuario que se conecta a una sesión necesita autenticarse con un token. A cada token se le asigna un rol, que determina lo que el cliente puede hacer cuando está conectado. Hay tres roles disponibles, Abonado, Editor y Moderador. En este tutorial sólo utilizaremos los dos primeros.

Un editor puede conectarse a sesiones, publicar flujos de audio y vídeo en la sesión y suscribirse a las sesiones de otros clientes. Un suscriptor puede conectarse a sesiones y suscribirse a las sesiones de otros clientes pero no puede publicar a la sesión.

Crude illustration of subscribers and publishers

Para este tutorial, proporcionaremos a los participantes fichas de editor, mientras que los espectadores obtendrán fichas de suscriptor.

Encontrará más información sobre los tokens en la documentación.

Configuración inicial

Como estamos construyendo un videochat básico, empieza por remezclar el proyecto del videochat básico construido en el tutorial anterior. Haga clic en el botón grande Remix abajo para hacer eso. 👇

remix this

Su estructura de carpetas debe parecerse a algo como esto:

Folder structure of the project

Como mencionamos al principio, TokBox OpenTok es ahora Vonage Video API. No hemos hecho ningún cambio en los nombres de nuestros paquetes, por lo que seguirás haciendo referencia a OpenTok en tu código.

Si has remezclado el proyecto Glitch, tu archivo server.js debería tener este aspecto:

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 poner en marcha el chat de vídeo, vaya al archivo .env e introduce la clave API y el secreto de tu proyecto, que encontrarás en el panel de control. Una vez hecho esto, vamos a hacer algunas adiciones al proyecto para proporcionar una interfaz para los espectadores.

Añadir el marcado obligatorio

Nuestra aplicación constará de tres páginas: una página de inicio para que los usuarios creen o se unan a una sesión, así como para que seleccionen si quieren ser espectadores o participantes, y las dos páginas de chat de vídeo para cada rol respectivamente.

Necesitaremos crear una página adicional para el visor. Añadamos un archivo viewer.html archivo a la carpeta views haciendo clic en el botón Nuevo archivo de la barra lateral izquierda. Nombre del archivo views/viewer.html y pegue la siguiente marca en la página. Esta página es casi exactamente igual que el archivo index.html salvo que no tiene un div para el 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>

En viewer.html y el archivo index.html utilizarán archivos de script diferentes ya que su implementación es ligeramente diferente debido a sus respectivos roles de token como se explica en la sección anterior.

A continuación, realizaremos algunas modificaciones en el formulario de la página landing.html para incluir una opción para que los usuarios seleccionen sus roles mediante botones de radio. Si seleccionan Espectadorserán enviados a la página que les muestra una secuencia del chat de vídeo. Si seleccionan Participanteaparecerá otra entrada de texto para el nombre de usuario, que se utilizará para identificar su secuencia.

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

Estilizar el formulario de la página de destino

Vamos a añadir a los estilos existentes para atender a la nueva fieldset y botones de radio.

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

Refactorizar el Javascript del lado del cliente

Trabajemos primero en la landing.html primero. Para mostrar/ocultar condicionalmente el campo del nombre de usuario, podemos añadir un receptor de eventos que compruebe si se selecciona el valor del botón de opción y cambie los estilos en consecuencia.

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

También tenemos que modificar la lógica para enviar a nuestros usuarios a las páginas correctas en función de si eligieron visor o participante. Los espectadores serán enviados a /session/viewer/ROOM_NAME mientras que los participantes serán enviados a /session/participant/ROOM_NAME?username=USER_NAME. Estamos haciendo uso de la cadena de consulta en la URL para pasar el nombre de usuario al 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 continuación, crearemos el archivo viewer.js para la página viewer.html página. De forma similar a lo que hicimos para el viewer.htmlhaga clic en Nuevo archivo pero esta vez añada los archivos Javascript a la carpeta public en su lugar.

Tu carpeta de proyecto debería tener ahora este aspecto:

Project structure after all files are added

El archivo viewer.js es ligeramente más corto que el archivo client.js porque no incluye la creación de un editor. Estamos haciendo una POST solicitud a /session/viewer/ROOM_NAME y recibiendo los datos de respuesta necesarios para conectarnos a una sesión.

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

Tenemos que hacer algunos ajustes menores en el archivo client.js porque queremos etiquetar el flujo de cada participante con el nombre de usuario que introdujo en la página de destino.

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

La función initializeSession() tomará ahora un parámetro más para streamName y se utilizará en el método initPublisher() y en el método subscribe() método. Ambos métodos aceptan un argumento opcional properties, que nos permite pasar opciones de personalización para los flujos.

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

Sus archivos client.js tendrán este aspecto:

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

Gestión de rutas en el servidor

La última parte antes de que todo se una es el archivo server.js donde se definen las rutas. Necesitaremos manejar las rutas para servir al Visor (viewer.html), así como la página Participante (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");
});

En lugar de la función generateToken() utilizaremos dos funciones diferentes para los dos papeles.

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 los espectadores, una vez cargada la página del Visor, el nombre de la sala se enviará al servidor mediante una POST solicitud. Esto será manejado por la siguiente ruta:

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

Del mismo modo, en el caso de los participantes, una vez cargada la página Participante, el nombre de la sala y el nombre de usuario se enviarán al servidor mediante una POST y su correspondiente ruta se gestiona de la siguiente manera:

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

De este modo, los espectadores podrán ver los streams de los participantes en una sola página, mientras éstos mantienen un videochat entre sí.

Screenshot of viewer page

Screenshot of participant page

Consulte el código final en Glitch o en GitHub y siéntete libre de remezclar o clonar el código y jugar con él tú mismo.

¿Y ahora qué?

Existen funcionalidades adicionales que podemos crear con la Video API de Vonage que se tratarán en futuros tutoriales, pero mientras tanto, puedes obtener más información en nuestro sitio de documentación integral. Si tienes algún problema o alguna pregunta, comunícate con nosotros en nuestra Slack de la comunidad. Gracias por leernos.

Compartir:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing ChenAntiguos alumnos de Vonage

Hui Jing es defensora de los desarrolladores en Nexmo. Tiene un amor desmesurado por CSS y la tipografía, y en general es una apasionada de todo lo relacionado con la web.