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

Diffuser une conversation vidéo avec l'API Video de Vonage

Publié le May 5, 2021

Temps de lecture : 6 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 à un public qui n'est pas dans le chat.

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 pour nous concentrer sur l'API Video elle-même. À la fin de ce tutoriel, votre application de chat vidéo devrait également offrir une option permettant de regarder simplement le flux du chat vidéo.

Screenshot of viewer page

Le code final pour 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

Création de jetons et rôles

Chaque utilisateur qui se connecte à une session doit être authentifié par un jeton. Chaque jeton se voit attribuer un rôle, qui détermine ce que le client peut faire lorsqu'il est connecté. Trois rôles sont disponibles, Abonné, Éditeur et Modérateur. Nous n'utiliserons que les deux premiers pour ce tutoriel.

Un éditeur peut se connecter à des sessions, publier des flux audio-vidéo dans la session et s'abonner aux sessions d'autres clients. Un abonné peut se connecter à des sessions et s'abonner aux sessions d'autres clients, mais ne peut pas ne peut pas publier à la session.

Crude illustration of subscribers and publishers

Pour ce tutoriel, nous fournirons aux participants des jetons d'éditeur, tandis que les spectateurs recevront des jetons d'abonné.

Vous trouverez plus d'informations sur les jetons dans la documentation.

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 que c'est fait, nous ferons quelques ajouts au projet pour fournir une interface pour les spectateurs.

Ajouter le balisage requis

Notre application sera composée de trois pages : une page d'accueil permettant aux utilisateurs de créer ou de rejoindre une session et de choisir s'ils veulent être spectateur ou participant, et les deux pages de chat vidéo pour chaque rôle respectivement.

Nous allons devoir créer une page supplémentaire pour le spectateur. Ajoutons un fichier viewer.html au dossier views en cliquant sur le bouton Nouveau fichier dans la barre latérale gauche. Nommez le fichier views/viewer.html et collez les balises suivantes dans la page. Cette page est presque exactement la même que le fichier index.html sauf qu'elle n'a pas de div pour l'éditeur.

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>

Les viewer.html et le fichier index.html utiliseront des fichiers de script différents, car leur mise en œuvre est légèrement différente en raison de leur rôle respectif dans les jetons, comme expliqué dans la section ci-dessus.

Ensuite, nous allons apporter quelques modifications au formulaire de la page landing.html afin d'y inclure une option permettant aux utilisateurs de sélectionner leur rôle à l'aide de boutons radio. S'ils sélectionnent Téléspectateurils seront dirigés vers la page qui leur montrera un flux de la discussion vidéo. S'il sélectionne Participantune autre entrée de texte s'affichera pour le nom d'utilisateur, qui sera utilisé pour identifier leur flux.

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

Style du formulaire de la page d'atterrissage

Ajoutons aux styles existants les nouveaux champs et boutons 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

Refonte du Javascript côté client

Travaillons d'abord sur la landing.html en premier lieu. Pour l'affichage/masquage conditionnel du champ du nom de l'utilisateur, nous pouvons ajouter un écouteur d'événements qui vérifie la valeur du bouton radio sélectionné et bascule les styles en conséquence.

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

Nous devons également modifier la logique qui permet d'envoyer nos utilisateurs vers les bonnes pages, selon qu'ils ont choisi ou non l'option spectateur ou participant. Les spectateurs seront envoyés vers /session/viewer/ROOM_NAME tandis que les participants seront envoyés à /session/participant/ROOM_NAME?username=USER_NAME. Nous utilisons la chaîne de requête de l'URL pour transmettre le nom de l'utilisateur au serveur.

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

Ensuite, nous allons créer le fichier viewer.js pour la page viewer.html page. Comme nous l'avons fait pour la page viewer.htmlcliquez sur Nouveau fichier mais cette fois, ajoutez les fichiers Javascript dans le dossier public à la place.

Votre dossier de projet devrait maintenant ressembler à ceci :

Project structure after all files are added

Le fichier viewer.js est légèrement plus court que le fichier client.js car il n'inclut pas la création d'un éditeur. Nous faisons une POST demande à /session/viewer/ROOM_NAME et recevons les données de réponse nécessaires pour nous connecter à une session.

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

Nous devons également apporter quelques modifications mineures au fichier client.js car nous voulons étiqueter le flux de chaque participant avec le nom d'utilisateur qu'il a saisi sur la page d'accueil.

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 fonction initializeSession() prendra désormais un paramètre supplémentaire pour streamName et utilisé dans la méthode initPublisher() et la méthode subscribe() . Les deux méthodes acceptent un argument facultatif de propriétés, qui nous permet de passer des options de personnalisation pour les flux.

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

Vos fichiers finaux client.js ressembleront à ceci :

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

Gérer les itinéraires du côté du serveur

La dernière partie avant que tout ne soit réuni est le fichier server.js où les routes sont définies. Nous devrons gérer les routes pour servir le visionneuse (viewer.html) ainsi que la page Participant (index.html) respectivement.

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

Au lieu de la fonction remixée generateToken() nous utiliserons deux fonctions différentes pour les deux rôles.

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

Pour les visionneuses, une fois la page de la visionneuse chargée, le nom de la salle sera envoyé au serveur par le biais d'une POST demande. Cette requête sera traitée par la route suivante :

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

De même, pour les participants, une fois la page Participant chargée, le nom de la salle et le nom de l'utilisateur sont envoyés au serveur par le biais d'une requête POST et la route correspondante est gérée comme suit :

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

Ainsi, les téléspectateurs pourront voir les flux des participants sur une seule page, tandis que les participants auront un chat vidéo entre eux.

Screenshot of viewer page

Screenshot of participant page

Consultez le code final sur Glitch ou sur GitHub et n'hésitez pas à remixer ou cloner le code et à vous amuser avec.

Quelle est la prochaine étape ?

Il existe d'autres fonctionnalités que nous pouvons créer avec l'API Video de Vonage et qui seront abordées dans de futurs tutoriels, mais en attendant, vous pouvez en savoir plus en consultant 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.