https://d226lax1qjow5r.cloudfront.net/blog/blogposts/broadcast-video-chat-with-javascript-and-vonage-dr/Blog_Stream-Video_1200x600.png

Diffusion d'un chat vidéo avec Javascript et Vonage

Publié le May 5, 2021

Temps de lecture : 9 minutes

Cette série de tutoriels explorera l'API vidéo de Vonage (anciennement TokBox OpenTok). Video API de Vonage (anciennement TokBox OpenTok) et ce que vous pouvez construire avec. L'API Video est très robuste et hautement personnalisable, et dans chaque article, nous montrerons comment mettre en œuvre une fonctionnalité spécifique à l'aide de l'API. Cette fois-ci, nous allons voir comment diffuser votre chat vidéo à de nombreux spectateurs en ligne.

Comme cette application nécessitera un peu de code côté serveur, nous utiliserons Glitch pour faciliter l'installation. Vous pouvez également télécharger le code de ce projet Glitch et le déployer sur le serveur ou la plateforme d'hébergement de votre choix (il faudra probablement modifier la configuration en fonction des exigences de votre plateforme).

Nous n'utiliserons pas de frameworks frontaux pour cette série, mais uniquement du Javascript vanille afin de nous concentrer sur l'API Video elle-même. À la fin de ce tutoriel, vous devriez être en mesure de diffuser votre chat vidéo en direct à un large public à l'aide de la diffusion en direct HTTP (HLS) ou d'un flux RTMP.

Screenshot of broadcast page

Le code final de cette application peut être trouvé dans ce dépôt GitHub ou remixé sur Glitch.

Conditions préalables

Avant de commencer, vous aurez besoin d'un compte Video API de Vonage, que vous pouvez créer gratuitement ici. Vous aurez également besoin de Node.js installé (si vous n'utilisez pas Glitch).

Ce tutoriel s'appuie sur le premier billet d'introduction de la série : Construire un chat vidéo de base. Si vous utilisez l'API Video pour la première fois, nous vous conseillons vivement de lire cet article car il couvre la configuration de base suivante :

  • Créer un projet Video API de Vonage

  • S'installer sur Glitch

  • Structure de base du projet

  • Initialisation d'une session

  • Connexion à la session, abonnement et publication

  • Styles de mise en page de base pour un chat vidéo

Diffusions avec l'API Video de Vonage

La plateforme prend en charge deux types de diffusion, les diffusions vidéo interactives en direct et les diffusions en continu en direct. Les deux types de diffusion nécessitent l'utilisation d'une session acheminée (une session qui utilise le routeur média de l'API vidéo de Vonage). routeur média Video API de Vonage). Ce sujet est abordé plus en profondeur dans la section suivante.

Les diffusions vidéo interactives en direct permettent à de nombreux clients de publier et de s'abonner aux flux audio-vidéo les uns des autres en temps réel. Les sessions routées peuvent prendre en charge des diffusions vidéo interactives en direct pour un maximum de 3 000 flux entre clients.

La diffusion en direct vous permettent de partager un flux flux HTTP en direct (HLS) ou un flux flux RTMP à un grand nombre de Numbers. Le flux HLS ou RTMP est une vidéo unique composée des différents flux publiés dans la session. Pour ce tutoriel, c'est ce type de diffusion que nous utiliserons.

HTTP Live Streaming (HLS) est un protocole de diffusion en continu de médias qui vise à diffuser de manière fiable des vidéos continues et de longue durée sur Internet. Il a été développé par Apple et publié en 2009.

HLS utilise le CDN pour la diffusion et est une diffusion traditionnelle avec une latence élevée (15-20 secondes) et aucune interaction. Un spectateur HLS recevra le contenu avec une latence de 15 à 20 secondes, et ne se prête donc pas directement à des cas d'utilisation interactifs.

Le protocole de messagerie en temps réel (RTMP) est un protocole basé sur le protocole TCP conçu pour la transmission d'audio, de vidéo et de données. Développé à l'origine en tant que protocole propriétaire par Macromedia, il s'agit désormais d'une spécification ouverte publiée par Adobe.

Bien que le RTMP ait une latence plus faible (environ 5 secondes) que le HLS, il ne se prête pas aux cas d'utilisation qui requièrent de l'interactivité. Vous utiliseriez RTMP pour envoyer du contenu créé avec l'API Video de Vonage vers des plateformes vidéo de médias sociaux, telles que Facebook ou YouTube Live.

Configuration initiale

Comme nous construisons sur un chat vidéo de base, commencez par remixer le projet pour le chat vidéo de base construit dans le tutoriel précédent. Cliquez sur le gros bouton Remix ci-dessous pour le faire. 👇

remix this

La structure de votre dossier devrait ressembler à ceci :

Folder structure of the project

Comme nous l'avons mentionné au début, TokBox OpenTok est maintenant Video API de Vonage. Nous n'avons pas modifié les noms de nos paquets, de sorte que vous ferez toujours référence à OpenTok dans votre code.

Si vous avez remixé le projet Glitch, votre fichier server.js devrait déjà ressembler à ceci :

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

Pour mettre en place le chat vidéo, rendez-vous dans le fichier .env et indiquez la clé API et le secret de votre projet, que vous trouverez dans le tableau de bord. Une fois cela fait, nous travaillerons sur le code côté client pour faire fonctionner le chat textuel avant de revisiter le fichier server.js à nouveau.

Ajouter le balisage requis

Notre application sera composée de trois pages : une page de renvoi permettant aux utilisateurs de créer ou de rejoindre une session, une page de chat vidéo pour les participants au chat vidéo et une page affichant le flux de diffusion.

Nous devons créer une page supplémentaire pour l'émission. Ajoutons un fichier broadcast.html au dossier views en cliquant sur le bouton Nouveau fichier dans la barre latérale gauche. Nommez le fichier views/broadcast.html et collez la balise suivante dans la page.

Add a broadcast.html to the views folder

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

Il n'y a pas grand-chose à faire ici, la partie clé est l'élément video qui hébergera le flux HLS lorsque la diffusion commencera.

Nous ajouterons également quelques balises liées à la diffusion à la page index.html comme les boutons pour démarrer et arrêter la diffusion, ainsi que pour générer un lien HLS partageable.

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

Style des contrôles de diffusion

Ensuite, mettons en place quelques styles pour les nouvelles balises ajoutées. Rien de bien compliqué ici, juste un peu de positionnement et aussi des états de boutons, dont nous parlerons lorsque nous commencerons à travailler sur le démarrage et l'arrêt de la diffusion.

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

Lorsque vous démarrez la session, votre interface doit ressembler à ceci :

Styling the broadcast controls to the bottom-right corner of the screen

Il ne s'agit pas du style final, mais il suffira pour l'instant de développer la fonctionnalité de diffusion de l'application.

Démarrage/arrêt d'une diffusion

La clé de la diffusion avec l'API Video de Vonage est la méthode startBroadcast() et la méthode stopBroadcast() et la méthode Ces méthodes seront appelées à partir du fichier server.js fichier. La méthode startBroadcast() prend en compte trois paramètres : l'identifiant de la session, les options de diffusion et une fonction de rappel. Nous obtiendrons l'identifiant de la session du côté client via une POST requête. Configurons la route pour cela.

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

Il existe d'autres propriétés facultatives que vous pourriez inclure dans les options de diffusion, telles que la résolution, la mise en page et ainsi de suite, mais pour l'instant, nous utiliserons les valeurs par défaut. Reportez-vous à la référence API pour plus de détails.

Mettons également en place l'itinéraire pour arrêter une diffusion. La méthode stopBroadcast() nécessite l'identifiant de diffusion, que nous obtiendrons également du côté client.

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

Il y a quelques modifications à apporter au fichier client.js pour prendre en compte cette nouvelle fonctionnalité. Dans le fichier client.js créez une variable globale session une variable globale.

+ let session;
function initializeSession(apiKey, sessionId, token) {
-  const session = OT.initSession(apiKey, sessionId);
+ session = OT.initSession(apiKey, sessionId);
  // more code below
}

Nous devons également changer le mode média de la session en mode routé au lieu du mode relayé par défaut.

Avant :

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

Nous devons également déclarer une variable broadcast qui contiendra des informations sur la diffusion et qui sera utilisée pour arrêter la diffusion. Pour l'instant, enregistrons également les réponses dans la console afin de pouvoir vérifier que les choses fonctionnent comme prévu.

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 vous ouvrez votre console lors du démarrage et de l'arrêt de la diffusion, vous devriez voir ce qui suit :

Console log messages when broadcast starts and stops

En théorie, nous pourrions nous arrêter ici, car nous avons maintenant accès à un lien HLS pour diffuser le Video Chat vers un lecteur qui prend en charge ce format. Et si vous avez déjà quelque chose qui gère les flux HLS, n'hésitez pas à faire les choses à votre manière. Le reste de ce tutoriel couvre une implémentation de base afin que vous puissiez voir à quoi ressemble le flux de diffusion.

État des boutons de commande

Mais d'abord, un peu de style supplémentaire pour les états des boutons. Si vous avez remarqué, il y a un certain décalage entre le moment où vous appuyez sur le bouton Lancer la diffusion et la réponse qui est enregistrée dans la console. Pour améliorer l'expérience de l'utilisateur, nous voulons lui indiquer que sa demande a bien été envoyée au serveur.

Le flux fonctionnerait de la manière suivante :

Button state flow

Au lieu d'afficher les deux boutons de démarrage et d'arrêt, nous n'affichons qu'un seul bouton à la fois. De plus, une fois qu'un bouton est cliqué, nous ne voulons pas que les utilisateurs cliquent plusieurs fois dessus pendant que le traitement est en cours. Ajoutons quelques classes CSS pour gérer les états caché et désactivé.

/* These are for the button states */
.hidden {
  display: none;
}

.disabled {
  cursor: not-allowed;
  opacity: 0.5;
  pointer-events: none;
}

Étant donné que les boutons "start" et "stop" ont le même flux, les classes CSS requises pour les changements d'état seraient les mêmes pour les deux boutons, mais appliquées de manière alternée. Nous pouvons abstraire ces changements dans une fonction qui prend la chaîne "start" ou "stop" et cible le bouton approprié.

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

Intégrons ces fonctions dans nos requêtes de récupération pour démarrer et arrêter la diffusion.

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

Créer un lien HLS partageable

L'objet Diffusion renvoyé par le SDK contient une URL de diffusion HLS qui peut être consommée par tous les lecteurs vidéo prenant en charge HLS. Utilisons-la pour créer un lien vers une page de diffusion. Nous avons déjà créé un fichier broadcast.html au début, alors acheminons notre diffusion vers cette page. Mettons en place une route dans le fichier server.js pour cela.

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

Nous allons ajouter une autre route qui vérifie l'existence de la session à diffuser. Si c'est le cas, la réponse de succès transmettra l'URL de la diffusion et son statut.

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

Sur notre page index.html ajouter ce qui suit aux contrôles de diffusion 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>

Et quelques CSS supplémentaires pour styles.css:

.invisible {
  position: absolute;
  opacity: 0;
  z-index: -1;
}

Le résultat de ces changements sera un lien qui ouvrira la page de diffusion dans un onglet ou une fenêtre séparée. Un bouton s'affiche qui, lorsqu'il est cliqué, copie le lien vers la page de diffusion pour que les personnes puissent le partager. Nous devrons récupérer l'URL HLS de la réponse à la diffusion, ainsi que le nom de la salle (à partir de l'URL) pour composer le lien de la page de diffusion.

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

Ajoutons ces nouvelles fonctions aux demandes de récupération pour démarrer/arrêter la diffusion :

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

Il existe de nombreux lecteurs Video capables de gérer les flux HLS et d'offrir différents niveaux de personnalisation de l'interface du lecteur, mais pour rester basique, ce tutoriel charge les éléments suivants hls.js pour lire le flux. Créez un fichier broadcast.js dans le dossier public dans le dossier

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 dernière partie de ce tutoriel est une implémentation native de la copie vers le presse-papiers. N'hésitez pas à utiliser une bibliothèque comme clipboard.js pour une API plus 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();
}

Après tout cela, vous devriez finalement obtenir quelque chose comme ceci pour la page de chat vidéo et la page de diffusion respectivement :

Video chat page

Broadcast page

Quelle est la prochaine étape ?

Le code final de Glitch et sur GitHub contient tout ce que nous avons couvert dans ce long article, mais réorganisé, de sorte que le code est plus propre et plus facile à maintenir. N'hésitez pas à remixer ou cloner le code et à vous amuser avec.

Il existe d'autres fonctionnalités que nous pouvons créer avec l'API Video de Vonage, qui seront abordées dans de futurs tutoriels, mais en attendant, vous pouvez en savoir plus sur notre site de documentation complet. Si vous rencontrez des problèmes ou si vous avez des questions, contactez-nous sur notre Communauté Slack. Merci pour votre lecture !

Partager:

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

Hui Jing est développeuse chez Nexmo. Elle a un amour immodéré pour le CSS et la typographie, et est généralement passionnée par tout ce qui touche au web.