
Compartir:
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.
Transmisión de Video Chat con Javascript y Vonage
Tiempo de lectura: 8 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 veremos cómo transmitir tu videochat a muchos espectadores en línea.
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, usted debe ser capaz de transmitir su video chat en vivo a una gran audiencia utilizando HTTP live streaming (HLS) o un flujo RTMP.

El código final de 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
Transmisiones con la API de Video de Vonage
La plataforma admite dos tipos de emisiones, emisiones de Video interactivo en directo y retransmisiones en directo. Ambos tipos de transmisión requieren que utilices una sesión enrutada (una sesión que utiliza la enrutador de medios de la Video API de Vonage). En la próxima sección se tratará este tema con mayor profundidad.
Transmisiones de Video interactivas en directo permiten a muchos clientes publicar y suscribirse a los flujos de audio y vídeo de los demás en tiempo real. Las sesiones enrutadas pueden soportar emisiones de vídeo interactivo en directo de hasta 3.000 flujos entre clientes.
Retransmisiones en directo le permiten compartir un transmisión en directo HTTP (HLS) o un flujo RTMP con un gran número de espectadores. El flujo HLS o RTMP es un único vídeo compuesto por los flujos individuales publicados en la sesión. Para este tutorial, este es el tipo de emisión que utilizaremos.
Streaming en directo HTTP (HLS) es un protocolo de transmisión de contenidos multimedia cuyo objetivo es ofrecer vídeo continuo y de larga duración a través de Internet de forma fiable. Fue desarrollado por Apple y lanzado en 2009.
HLS utiliza CDN para la entrega y es una emisión tradicional con alta latencia (15-20 segundos) y sin interacción. Un espectador de HLS recibirá el contenido con una latencia de 15-20 segundos, por lo que no se presta directamente a casos de uso interactivo.
Protocolo de mensajería en tiempo real (RTMP) es un protocolo basado en TCP diseñado para la transmisión de audio, Video y datos. Desarrollado originalmente por Macromedia como protocolo propietario, en la actualidad es una especificación abierta publicada por Adobe.
Aunque RTMP tiene una latencia más baja (alrededor de 5 segundos) que HLS, tampoco se presta a casos de uso que requieren interactividad. Puedes usar RTMP para enviar contenido creado con la Video API de Vonage a plataformas de video de redes sociales, como Facebook o YouTube Live.
Configuración inicial
Como estamos construyendo sobre un videochat básico, empieza por remezclar el proyecto para el videochat básico construido en el tutorial anterior. Haga clic en el botón grande Remix abajo para hacer eso. 👇

Su estructura de carpetas debe parecerse a algo como esto:

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 puedes encontrar en el panel de control. Una vez hecho esto, vamos a trabajar en el código del lado del cliente para obtener el chat de texto de trabajo antes de volver a visitar el server.js de nuevo.
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, una página de chat de vídeo para los participantes en el chat de vídeo y una página que muestre el flujo de emisión.
Necesitaremos crear una página adicional para la emisión. Vamos a añadir un broadcast.html archivo a la carpeta views haciendo clic en el botón Nuevo archivo de la barra lateral izquierda. Nombre del archivo views/broadcast.html y pegue la siguiente marca en la página.

<!DOCTYPE html>
<html lang="en">
<head>
<title>Broadcast Video Chat</title>
<meta
name="description"
content="Broadcast your video chat to a large audience 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>Video broadcast</h1>
</header>
<main>
<video id="video" class="broadcast-video"></video>
</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://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script src="/broadcast.js"></script>
</body>
</html>
No hay mucho que hacer aquí, la parte clave es el elemento video que albergará el flujo HLS cuando comience la emisión.
También añadiremos a la página algunas marcas relacionadas con la emisión, como los botones para iniciar y detener la emisión, así como para generar un enlace HLS que se pueda compartir. index.html como los botones para iniciar y detener la emisión, así como para generar un enlace HLS compartible.
<main>
<div id="subscriber" class="subscriber"></div>
<div id="publisher" class="publisher"></div>
<!-- Add the broadcast controls -->
<div class="broadcast">
<button id="startBroadcast">Start Broadcast</button>
<button id="stopBroadcast" class="hidden">Stop Broadcast</button>
</div>
</main> Estilo de los controles de emisión
A continuación, vamos a poner en algunos estilos para el nuevo marcado añadido. Nada demasiado elegante aquí, sólo un poco de posicionamiento y también los estados de los botones, que vamos a hablar cuando empezamos a trabajar en iniciar / detener la emisión.
/* To position the controls in the bottom-right corner */
.broadcast {
position: absolute;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.broadcast a,
.broadcast button {
margin-bottom: 1em;
}
/* This is to centre the broadcast video */
.broadcast-video {
margin: auto;
}Ahora, cuando inicies la sesión, tu interfaz debería tener este aspecto:

Este no es el estilo final, pero servirá por ahora mientras desarrollamos la funcionalidad de emisión de la aplicación.
Iniciar/detener una emisión
La clave para transmitir con la Video API de Vonage es el método startBroadcast() y el método stopBroadcast() método. Estos métodos se llamarán desde el server.js archivo. El método startBroadcast() recibe tres parámetros: el identificador de sesión de la sesión, las opciones para la emisión y una función de devolución de llamada. Obtendremos el ID de sesión del lado del cliente a través de una POST solicitud. Configuremos la ruta para ello.
// Required to read the body of a POST request
app.use(express.json());
// Declare an object to store the broadcast information returned by the SDK
let broadcastData = {};
app.post("/broadcast/start", (request, response) => {
const sessionId = request.body.sessionId;
const broadcastOptions = {
outputs: {
hls: {},
},
};
OT.startBroadcast(sessionId, broadcastOptions, (error, broadcast) => {
if (error) {
console.log(error);
response.status(503);
response.send({ error });
}
// Assign the response from the SDK to the broadcastData object
broadcastData = broadcast;
response.status(200);
response.send({ broadcast: broadcast });
});
});
Hay más propiedades opcionales que podría incluir como opciones de emisión, como la resolución, el diseño, etc., pero por ahora utilizaremos las predeterminadas. Consulte la referencia API para más detalles.
Configuremos también la ruta para detener una emisión. El método stopBroadcast() requiere el ID de la emisión, que también obtendremos del lado del cliente.
app.post("/broadcast/stop", (request, response) => {
const broadcastId = request.body.broadcastId;
OT.stopBroadcast(broadcastId, (error, broadcast) => {
if (error) console.log(error);
response.status(200);
response.send({
status: broadcast.status
});
});
});
Hay que hacer algunos ajustes en el archivo client.js para adaptarlo a esta nueva funcionalidad. En el archivo client.js haga session una variable global.
+ let session;
function initializeSession(apiKey, sessionId, token) {
- const session = OT.initSession(apiKey, sessionId);
+ session = OT.initSession(apiKey, sessionId);
// more code below
}También debemos cambiar el modo de medios de la sesión a un modo enrutado en lugar del modo retransmitido por defecto.
Antes:
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) => {
+ // Set the media mode to routed here
+ OT.createSession({ mediaMode: "routed" }, (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);
}
});
}
});
También debemos declarar una variable broadcast para guardar información sobre la emisión que también se utilizará para detenerla. Por ahora, también vamos a registrar las respuestas en la consola para que podamos verificar que las cosas están funcionando como se esperaba.
let broadcast;
const startBroadcastBtn = document.getElementById("startBroadcast");
startBroadcastBtn.addEventListener("click", startBroadCast, false);
function startBroadCast() {
fetch("/broadcast/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: session.sessionId })
})
.then(res => {
return res.json();
})
.then(res => {
broadcast = res.broadcast;
console.log(res);
})
.catch(handleCallback);
}
const stopBroadcastBtn = document.getElementById("stopBroadcast");
stopBroadcastBtn.addEventListener("click", stopBroadCast, false);
function stopBroadCast() {
fetch("/broadcast/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ broadcastId: broadcast.id })
})
.then(res => {
return res.json();
})
.then(res => {
console.log(res);
})
.catch(handleCallback);
}
Si abre la consola al iniciar y detener la emisión, debería ver lo siguiente:

En teoría, podríamos parar aquí, porque ahora tenemos acceso a un enlace HLS para transmitir el chat de vídeo a un reproductor que soporte el formato. Y si ya tienes algo que maneje streams HLS, siéntete libre de hacer las cosas a tu manera. El resto de este tutorial cubre una implementación básica para que puedas echar un vistazo a cómo se ve el flujo de transmisión.
Manejar los estados de los botones
Pero primero, algunos estilos adicionales para los estados de los botones. Si te has dado cuenta, hay un lapso de tiempo entre que pulsas el botón Iniciar emisión y la respuesta que se registra en la consola. Para mejorar la experiencia del usuario, queremos proporcionar alguna indicación al usuario de que su solicitud fue enviada al servidor.
El flujo funcionaría más o menos así:

En lugar de mostrar los botones de inicio y parada, sólo mostramos un botón relevante cada vez. Además, una vez que se pulsa un botón, no queremos que los usuarios lo pulsen varias veces mientras se está procesando. Añadamos algunas clases CSS para tratar los estados oculto y desactivado.
/* These are for the button states */
.hidden {
display: none;
}
.disabled {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
}Dado que start y stop tienen el mismo flujo, las clases CSS necesarias para los cambios de estado serían las mismas para ambos botones, sólo que aplicadas de forma alterna. Podemos abstraer esos cambios en una función que tome la cadena "start" o "stop" y apunte al botón apropiado.
// Button state while awaiting response from server
function pendingBtnState(statusString) {
const btn = document.getElementById(statusString + "Broadcast");
btn.classList.add("disabled");
btn.setAttribute("data-original", btn.textContent);
btn.textContent = "Processing…";
}
// Switch button state once server responds
function activeBtnState(statusString) {
const activeBtn =
statusString === "start"
? document.getElementById("startBroadcast")
: document.getElementById("stopBroadcast");
const inactiveBtn =
statusString === "stop"
? document.getElementById("startBroadcast")
: document.getElementById("stopBroadcast");
inactiveBtn.classList.remove("disabled");
inactiveBtn.textContent = inactiveBtn.getAttribute("data-original");
inactiveBtn.removeAttribute("data-original");
inactiveBtn.classList.add("hidden");
activeBtn.classList.remove("hidden");
}Incorporemos estas funciones a nuestras solicitudes de obtención para iniciar y detener la difusión.
function startBroadCast() {
// To put the Start button into the pending state
pendingBtnState("start");
fetch("/broadcast/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: session.sessionId })
})
.then(res => {
return res.json();
})
.then(res => {
broadcast = res.broadcast;
// To hide the Start button and show the Stop button
activeBtnState("stop");
})
.catch(handleCallback);
}
function stopBroadCast() {
// To put the Stop button into the pending state
pendingBtnState("stop");
fetch("/broadcast/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ broadcastId: broadcast.id })
})
.then(res => {
return res.json();
})
.then(res => {
// To hide the Stop button and show the Start button
activeBtnState("start");
})
.catch(handleCallback);
}
Crear enlace HLS compartible
El objeto Broadcast devuelto por el SDK contiene una URL de emisión HLS que puede ser consumida por cualquier reproductor de vídeo que soporte HLS. Hagamos uso de esto para crear un enlace a una página Broadcast. Ya hemos creado un archivo broadcast.html al principio, así que vamos a canalizar nuestra emisión en esa página. Configuremos una ruta en el archivo server.js para ello.
app.get("/broadcast/:room", (request, response) => {
response.sendFile(__dirname + "/views/broadcast.html");
});
Vamos a añadir otra ruta que compruebe la existencia de la sesión a emitir. Si es así, la respuesta de éxito pasará sobre la URL para la emisión y su estado.
app.get("/broadcast/hls/:room", (request, response) => {
const roomName = request.params.room;
if (sessions[roomName]) {
response.status(200);
response.send({
hls: broadcastData.broadcastUrls.hls,
status: broadcastData.status
});
} else {
response.status(204);
}
});
En nuestra index.html página, añada lo siguiente a los controles de emisión div:
<div class="broadcast">
<!-- Add link to the Broadcast page and a means to copy to clipboard -->
<a class="hidden" id="hlsLink" target="_blank" rel="noopener noreferrer"
>Open Broadcast page</a
>
<p class="invisible" id="hlsCopyTarget"></p>
<button class="hidden" id="copyLink">Copy HLS link</button>
<button id="startBroadcast">Start Broadcast</button>
<button id="stopBroadcast" class="hidden">Stop Broadcast</button>
</div>
Y algunos CSS adicionales para styles.css:
.invisible {
position: absolute;
opacity: 0;
z-index: -1;
}El resultado de estos cambios será un enlace que abre la página de Difusión en una pestaña o ventana aparte. Aparecerá un botón que, al pulsarlo, copiará el enlace a la página de Difusión para que la gente pueda compartirlo. Tendremos que obtener la URL HLS de la respuesta de emisión, así como el nombre de la sala (de la URL) para componer el enlace de la página de emisión.
const url = new URL(window.location.href);
const roomName = url.pathname.split("/")[2];
// To show/hide the HLS links when the broadcast starts/stops
function hlsLinkState(statusString) {
if (statusString === "start") {
document.getElementById("hlsLink").classList.remove("hidden");
document.getElementById("copyLink").classList.remove("hidden");
} else {
document.getElementById("hlsLink").classList.add("hidden");
document.getElementById("copyLink").classList.add("hidden");
}
}
// Create the link to the broadcast page
function composeHlsLink(link) {
hlsLinkState("start");
const hlsLinkUrl =
"https://" + location.host + "/broadcast/" + roomName + "?hls=" + link;
const hlsLink = document.getElementById("hlsLink");
const hlsCopyTarget = document.getElementById("hlsCopyTarget");
hlsLink.href = hlsLinkUrl;
hlsCopyTarget.innerHTML = hlsLinkUrl;
}Vamos a añadir estas nuevas funciones a las peticiones fetch para iniciar/detener también la emisión:
function startBroadCast() {
pendingBtnState("start");
fetch("/broadcast/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: session.sessionId })
})
.then(res => {
return res.json();
})
.then(res => {
broadcast = res.broadcast;
activeBtnState("stop");
// Compose the link to the broadcast page
composeHlsLink(res.broadcast.broadcastUrls.hls);
})
.catch(handleCallback);
}
function stopBroadCast() {
pendingBtnState("stop");
fetch("/broadcast/stop", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ broadcastId: broadcast.id })
})
.then(res => {
return res.json();
})
.then(res => {
activeBtnState("start");
// Hide the links when the broadcast has stopped
hlsLinkState("stop");
})
.catch(handleCallback);
}
Existen numerosos reproductores de Video que pueden manejar streams HLS y ofrecen diferentes niveles de personalización sobre la interfaz del reproductor, pero para mantener las cosas básicas, este tutorial carga hls.js para reproducir el flujo. Crea un archivo broadcast.js en la carpeta public carpeta.
const url = new URL(window.location.href);
const roomName = url.pathname.split("/")[2];
const hlsLink = url.searchParams.get("hls");
fetch("/broadcast/hls/" + roomName)
.then(res => {
return res.json();
})
.then(res => {
playStream(hlsLink);
})
.catch(error => console.error(error));
// Refer to hls.js documentation for more options
function playStream(hlsLink) {
const video = document.getElementById("video");
const videoSrc = hlsLink;
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource(videoSrc);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = videoSrc;
video.addEventListener("loadedmetadata", function() {
video.play();
});
}
}
La parte final de este tutorial es una implementación nativa de copiar al portapapeles. Siéntete libre de usar una librería como clipboard.js para una API más simple.
const copyLinkBtn = document.getElementById("copyLink");
copyLinkBtn.addEventListener("click", copyHlsLink, false);
function copyHlsLink() {
const hlsCopyTarget = document.getElementById("hlsCopyTarget");
const range = document.createRange();
range.selectNode(hlsCopyTarget);
window.getSelection().addRange(range);
try {
const successful = document.execCommand("copy");
const msg = successful ? "successful" : "unsuccessful";
console.log("Copy command was " + msg);
} catch (err) {
console.log("Oops, unable to copy");
}
window.getSelection().removeAllRanges();
}Después de todo esto, deberías obtener finalmente algo como esto para la página de chat de vídeo y la página de emisión respectivamente:


¿Y ahora qué?
El código final de Glitch y GitHub contiene todo lo que cubrimos en este largo post, pero reorganizado, por lo que el código es más limpio y más fácil de mantener. Siéntete libre de remezclar o clonar el código y jugar con él.
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.