Application vidéo avec salons de groupe, Twilio Programmable Video, React, TypeScript et Express — Partie 1

May 17, 2021
Rédigé par
Mia Adjei
Twilion
Révisé par

Construire une application vidéo avec des salons de groupe avec Twilio Programmable Video, React, TypeScript et Express — Partie 1

Avez-vous déjà assisté à une réunion vidéo, à une visioconférence ou à un cours en ligne ? Et avez-vous eu l'opportunité de rejoindre une salle de groupe ? Les salles de groupe vidéo sont idéales pour créer un espace de discussion et de collaboration en petits groupes, en parallèle d'un appel vidéo en grand groupe.

Dans ce tutoriel et dans le suivant, vous allez construire une application de chat vidéo, qui vous permettra de créer des salles de groupe en plus de votre salle vidéo principale.

À la fin de ce premier tutoriel, vous aurez les éléments suivants :

  • Un serveur qui gère vos appels d'API vers les API Twilio Video et qui communique avec votre base de données locale.
  • Une base de données configurée pour stocker les associations entre les salles principales et les salles de groupe.

Allez, c'est parti !

Conditions préalables

Vous aurez besoin des éléments ci-dessous :

  • Un compte Twilio gratuit. (Si vous vous inscrivez ici, vous recevrez un crédit Twilio de 10 $ lorsque vous passerez à un compte payant !)
  • Node.js (version 14.16.1 ou supérieure) et npm installé sur votre machine.
  • HTTPie ou cURL.

Obtenir le code de démarrage

La première étape consiste à obtenir le code de départ. Choisissez un emplacement sur votre machine où vous souhaitez configurer le projet. Ouvrez ensuite une fenêtre de terminal et exécutez la commande suivante pour cloner la branche getting-started du répertoire de code :

git clone -b getting-started  https://github.com/adjeim/react-video-breakouts.git

Ensuite, accédez au répertoire racine du projet en exécutant la commande suivante :

cd react-video-breakouts

Installer les dépendances et définir les variables d'environnement

Maintenant que vous avez le code de démarrage, exécutez la commande suivante dans votre fenêtre de terminal pour installer les dépendances requises :

npm install

Si vous avez la curiosité de savoir quelles dépendances ont été installées, vous pouvez les consulter dans le fichier package.json, qui se trouve également à la racine du projet.

Ensuite, vous devez configurer vos variables d'environnement. Exécutez la commande suivante pour copier le fichier .env.template dans un nouveau fichier nommé .env :

cp .env.template .env

Ouvrez .env dans votre éditeur de code. Vous verrez que le fichier contient les variables d'environnement suivantes :

TWILIO_ACCOUNT_SID=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_API_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Vous devrez remplacer le texte de l'espace réservé ci-dessus par vos informations d'identification Twilio réelles, qui se trouvent dans la console Twilio. Connectez-vous à la console Twilio et recherchez le SID de votre compte (Account SID).

Console Twilio indiquant l'emplacement du SID de compte (Account SID)

Copiez et collez la valeur du SID de compte (Account SID) afin de remplacer le texte de l'espace réservé pour TWILIO_ACCOUNT_SID.

Accédez ensuite à la section des clés API de la console et générez une nouvelle clé API. Copiez les valeurs de la clé API pour SID et Secret afin de remplacer le texte de l'espace réservé pour TWILIO_API_KEY et TWILIO_API_SECRET.

Exécuter le serveur Express

Maintenant que vous avez configuré les variables d'environnement, il est temps d'exécuter votre serveur.

Ouvrez le fichier server.ts dans votre éditeur de texte. Vous verrez qu'un serveur Express de base a déjà été configuré pour vous.

Exécutez la commande suivante à partir de la racine de votre projet pour démarrer le serveur :

npm run server

Vous devriez voir le log suivant dans votre terminal, ce qui signifie qu'il fonctionne correctement :

Express server running on port 5000

Super ! Maintenant que votre serveur Express est en cours d'exécution, il est temps de connecter une base de données.

Configurer et connecter une base de données PouchDB

Pour ce projet, vous devez utiliser une base de données nommée PouchDB. PouchDB est une base de données JavaScript open source qui peut s'exécuter dans Node.js ou dans le navigateur. Pour en savoir plus sur PouchDB, vous pouvez consulter les guides ou la documentation API sur le site web approprié.

Pour commencer à utiliser PouchDB, installez le package pouchdb et les types TypeScript à partir de la console, à l'aide des commandes ci-dessous :

npm install pouchdb
npm install --save-dev @types/pouchdb

Ouvrez votre fichier server.ts et importez pochdb dans votre projet en ajoutant la ligne de code suivante sous les autres instructions d'importation :

import twilio, { Twilio } from 'twilio';
import PouchDB from 'pouchdb';

Ajoutez ensuite une interface décrivant la structure des données à stocker. PouchDB est une base de données NoSQL, chaque enregistrement est donc appelé document. Chaque salle vidéo principale que vous créez sera enregistrée dans son propre document.

Pour ce projet, vous allez créer une interface VideoRoom pour les salles vidéo principales. Pour ce faire, ajoutez le code suivant au fichier server.ts, juste en dessous de la ligne app.use(cors(options)); :

app.use(cors(options));

export interface VideoRoom {
  _id: string,
  _rev: string;
  breakouts: string[];
}

Vous pouvez consulter la documentation sur l'API Rooms pour en savoir plus sur les salles vidéo Twilio. Pour l'élément VideoRoom principal, vous stockerez le sid de la salle comme son _id. Vous associerez les salles de groupe à leurs salles principales en stockant leurs sid dans le champ breakouts (salles de groupe) de l'élément VideoRoom.

Mais qu'en est-il du champ _rev ? Le champ _rev est le marqueur de révision. C'est un champ utilisé par PouchDB pour marquer lorsqu'un enregistrement a été créé ou mis à jour. Chaque fois que vous modifiez un enregistrement dans la base de données, cet ID généré de manière aléatoire est également mis à jour.

Maintenant que vous disposez d'une structure pour vos données, créez une nouvelle base de données PouchDB nommée video_rooms en ajoutant la ligne de code suivante au fichier, juste en dessous de l'interface que vous avez créée à l'étape précédente :

export interface VideoRoom {
  _id: string,
  _rev: string;
  breakouts: string[];
}

const db = new PouchDB<VideoRoom>('video_rooms');

Vous pouvez maintenant commencer à créer des routes pour votre API.

Créer les routes API

Pour ce projet, vous allez construire les 4 routes suivantes :

  • createRoom : créer une nouvelle salle principale
  • createBreakoutRoom : créer une nouvelle salle de groupe
  • listActiveRooms : répertorier les salles vidéo actives
  • getToken : obtenir un token d'accès pour une salle vidéo

Cependant, avant de commencer à écrire des routes, créez une nouvelle instance du client Twilio afin de gérer la création et la mise à jour de vos salles vidéo. Ajoutez le code suivant au fichier server.ts, juste en dessous de l'emplacement où vous avez créé la base de données :

const db = new PouchDB<VideoRoom>('video_rooms');

const twilioClient = new Twilio(
  process.env.TWILIO_API_KEY as string,
  process.env.TWILIO_API_SECRET as string,
  { accountSid: process.env.TWILIO_ACCOUNT_SID as string }
);

Vous pouvez vous lancer avec les salles vidéo !

Créer une nouvelle salle vidéo principale

Commencez par créer la fonction createRoom en ajoutant le code suivant au fichier server.ts, juste au-dessus de la ligne app.listen :

/**
 * Create a new main room
 */
const createRoom = async (request: Request, response: Response) => {
  // Get the room name from the request body.
  // If no room name is provided, the name will be set to the room's SID.
  const roomName: string = request.body.roomName || '';

  try {
    // Call the Twilio video API to create the new room.
    const room = await twilioClient.video.rooms.create({
        uniqueName: roomName,
        type: 'group'
      });

    const mainRoom: VideoRoom = {
      _id: room.sid,
      _rev: '',
      breakouts: [],
    }

    try {
      // Save the document in the db.
      await db.put(mainRoom);

      return response.status(200).send({
        message: `New video room ${room.uniqueName} created`,
        room: mainRoom
      });

    } catch (error) {
      return response.status(400).send({
        message: `Error saving new room to db -- room name=${roomName}`,
        error
      });
    }

  } catch (error) {
    // If something went wrong, handle the error.
    return response.status(400).send({
      message: `Unable to create new room with name=${roomName}`,
      error
    });
  }
};

Dans cette fonction, le nom de la salle vidéo provient de la requête entrante. Ensuite, la fonction fait une requête à l'API Twilio Video pour créer la nouvelle salle du côté Twilio. Les détails de la salle créée sont alors enregistrés dans la base de données et les détails de la nouvelle salle sont renvoyés côté client. Toute erreur sera traitée par les instructions catch.

Ajoutez ensuite la nouvelle route juste au-dessus de la ligne app.listen à la fin du fichier server.ts :

app.post('/rooms/main', createRoom);

app.listen(port, () => {
  console.log(`Express server running on port ${port}`);
});

Pour essayer de créer une nouvelle salle vidéo à partir de la ligne de commande, assurez-vous que le serveur Express est toujours en cours d'exécution, puis ouvrez une deuxième fenêtre de terminal et exécutez l'une des commandes suivantes en sélectionnant cURL ou HTTPie. Spécifiez un élément roomName. Dans cet exemple, le nom de la salle sera Music Chat :

// cURL
curl -X POST localhost:5000/rooms/main \
    -d '{"roomName":"Music Chat"}' \
    -H "Content-Type: application/json"

// HTTPie
http POST localhost:5000/rooms/main roomName="Music Chat"

Une réponse contenant la nouvelle salle créée s'affiche :

{
    "message": "New video room Music Chat created",
    "room": {
        "_id": "<ROOM_SID>",
        "_rev": "",
        "breakouts": []
    }
}

Créer une nouvelle salle vidéo de groupe

Ensuite, construisez la fonction createBreakoutRoom. Elle est similaire à la fonction createRoom, mais elle présente quelques différences. En plus d'un roomName provenant de la requête, la requête entrante inclut le parentSid, qui correspond à l'élément _id de la salle vidéo principale à laquelle cette salle de groupe sera associée.

Ajoutez la fonction suivante au fichier server.ts, juste en dessous de la fonction createRoom que vous avez ajoutée à l'étape précédente :

/**
 * Create a new breakout room
 */
const createBreakoutRoom = async (request: Request, response: Response) => {
  // Get the roomName and parentSid from the request body.
  const roomName: string = request.body.roomName || '';

  // If no parent was provided, return an error message.
  if (!request.body.parentSid) {
    return response.status(400).send({
      message: `No parentSid provided for new breakout room with name=${roomName}`,
    });
  }

  const parentSid: string = request.body.parentSid;

  try {
    // Call the Twilio video API to create the new room.
    const breakoutRoom = await twilioClient.video.rooms.create({
        uniqueName: roomName,
        type: 'group'
      });

    try {
      // Save the new breakout room on its parent's record (main room).
      const mainRoom: VideoRoom = await db.get(parentSid);
      mainRoom.breakouts.push(breakoutRoom.sid);
      await db.put(mainRoom);

      // Return the full room details in the response.
      return response.status(200).send({
        message: `Breakout room ${breakoutRoom.uniqueName} created`,
        room: mainRoom
      });

    } catch (error) {
      return response.status(400).send({
        message: `Error saving new breakout room to db -- breakout room name=${roomName}`,
        error
      });
    }

  } catch (error) {
    // If something went wrong, handle the error.
    return response.status(400).send({
      message: `Unable to create new breakout room with name=${roomName}`,
      error
    });
  }
};

Dans la fonction createBreakoutRoom, vous enregistrez la nouvelle salle de groupe dans son document parent. Si aucun parentSid n'est fourni, un message d'erreur s'affiche. Inutile de créer une salle de groupe si elle n'est associée à aucune salle principale.

Ajoutez cette nouvelle route juste en dessous de la route createRoom que vous avez ajoutée dans la section précédente :

app.post('/rooms/main', createRoom);
app.post('/rooms/breakout', createBreakoutRoom);

Essayez de créer une nouvelle salle vidéo principale et une nouvelle salle de groupe à partir de la ligne de commande. Tout d'abord, créez une nouvelle salle principale, en utilisant cURL ou HTTPie :

// cURL
curl -X POST localhost:5000/rooms/main \
    -d '{"roomName":"Music Chat 2"}' \
    -H "Content-Type: application/json"

// HTTPie
http POST localhost:5000/rooms/main roomName="Music Chat 2"

Pour créer une salle de groupe nommée « Jazz », copiez l'élément _id de la salle principale que vous venez de créer et transmettez-le à la requête createBreakoutRoom en utilisant cURL ou HTTPie :

// cURL
curl -X POST localhost:5000/rooms/breakout \
    -d '{"roomName":"Jazz", "parentSid":"<ROOM_SID>"}' \
    -H "Content-Type: application/json"

// HTTPie
http POST localhost:5000/rooms/breakout roomName="Jazz" parentSid="<ROOM_SID>"

Vous verrez une réponse contenant la salle principale mise à jour, avec une salle de groupe dans le tableau breakouts :

{
    "message": "Breakout room Jazz created",
    "room": {
        "_id": "<ROOM_SID>",
        "_rev": "<REV_ID>",
        "breakouts": [
            "<BREAKOUT_ROOM_SID>"
        ]
    }
}

Super ! Il y a désormais une salle de groupe dans Music Chat 2, nommée Jazz, où tous vos amis qui aiment le jazz peuvent parler de leurs chansons préférées.

Répertorier les salles principales actives

Ensuite, créez une fonction pour répertorier les salles principales actives. Cela sera utile pour votre application côté client, car vos utilisateurs devront pouvoir rejoindre uniquement les salles ouvertes et en cours.

Pour consulter l'état actuel des salles, vous devez utiliser twilioClient afin d'appeler l'API Rooms et obtenir la liste des salles actuellement actives. Ensuite, vous pouvez filtrer les documents de la base de données en fonction de cette liste et renvoyer uniquement les salles principales actives et leurs salles de groupe du côté client de votre application.

Juste en dessous de l'interface que vous avez créée précédemment pour enregistrer les salles VideoRoom dans la base de données, ajoutez les nouvelles interfaces suivantes qui décrivent les données de la salle principale et de la salle de groupe que vous allez renvoyer du côté client de votre application :

export interface VideoRoom {
  _id: string,
  _rev: string;
  breakouts: string[];
}

interface MainRoomItem {
  _id: string,
  name: string;
  breakouts: BreakoutRoomItem[]
}

interface BreakoutRoomItem {
  _id: string;
  name: string;
}

Les données que vous allez récupérer à partir de l'API Rooms Twilio incluront les noms des salles. L'élément name est donc inclus dans ces interfaces. Cela vous permettra d'afficher les noms des salles côté client de votre application.
Ensuite, juste en dessous de votre fonction pour `createBreakoutRoom`, ajoutez le code ci-dessous au fichier server.ts pour créer la fonction `listActiveRooms` :

/**
* List active video rooms
*/
const listActiveRooms = async (request: Request, response: Response) => {
  try {
    // Get the last 20 rooms that are still currently in progress.
    const rooms = await twilioClient.video.rooms.list({status: 'in-progress', limit: 20});

    // Get a list of active room sids.
    let activeRoomSids = rooms.map((room) => room.sid);

    try {
      // Retrieve the room documents from the database.
      let dbRooms = await db.allDocs({
        include_docs: true,
      });

      // Filter the documents to include only the main rooms that are active.
      let dbActiveRooms = dbRooms.rows.filter((mainRoomRecord) => {
        return activeRoomSids.includes(mainRoomRecord.id) && mainRoomRecord
      });

    // Create a list of MainRoomItem that will associate a room's id with its name and breakout rooms.
    let videoRooms: MainRoomItem[] = [];

    // For each of the active rooms from the db, get the details for that main room and its breakout rooms.
    // Then pass that data into an array to return to the client side.
    if (dbActiveRooms) {
      dbActiveRooms.forEach((row) => {
        // Find the specific main room in the list of rooms returned from the Twilio Rooms API.
        const activeMainRoom = rooms.find((mainRoom) => {
          return mainRoom.sid === row.doc._id;
        })

        // Get the list of breakout rooms from this room's document.
        const breakoutSids = row.doc.breakouts;

        // Filter to select only the breakout rooms that are active according to
        // the response from the Twilio Rooms API.
        const activeBreakoutRooms = rooms.filter((breakoutRoom) => {
          return breakoutSids.includes(breakoutRoom.sid);
        });

        // Create a list of BreakoutRoomItems that will contain each breakout room's name and id.
        let breakouts: BreakoutRoomItem[] = [];

        // Get the names of each breakout room from the API response.
        activeBreakoutRooms.forEach((breakoutRoom) => {
          breakouts.push({
            _id: breakoutRoom.sid,
            name: breakoutRoom.uniqueName
          })
        });

        const videoRoom: MainRoomItem = {
          _id: activeMainRoom.sid,
          name: activeMainRoom.uniqueName,
          breakouts: breakouts
        };
        // Add this room to the list of rooms to return to the client side.
        videoRooms.push(videoRoom);
      });
    }

    // Return the list of active rooms to the client side.
    return response.status(200).send({
      rooms: videoRooms,
    });

    } catch (error) {
      return response.status(400).send({
        message: `Error retrieving video rooms from db`,
        error
      });
    }

  } catch (error) {
    return response.status(400).send({
      message: `Unable to list active rooms`,
      error
    });
  }
};

Ajoutez ensuite la route listActiveRooms à la liste des autres routes que vous avez créées :

app.post('/rooms/main', createRoom);
app.post('/rooms/breakout', createBreakoutRoom);
app.get('/rooms/', listActiveRooms);

Vous pouvez maintenant lister les salles vidéo actives avec une requête cURL ou HTTPie :

// cURL
curl -X GET localhost:5000/rooms/

// HTTPie
http GET localhost:5000/rooms/

S'il n'y a pas de salle vidéo active, vous verrez une réponse comme celle-ci :

{
    "rooms": []
}

Les salles que vous avez créées précédemment et qui sont devenues inactives ont été exclues de cette réponse.

Les salles créées via l'API REST Twilio existent pendant 5 minutes pour permettre aux participants de se connecter, mais si personne ne s'y connecte au cours de ces 5 minutes, la salle expire.

Si vous souhaitez voir à quoi ressemble la réponse lorsque les salles sont actives, essayez de créer une nouvelle salle vidéo en envoyant une autre requête à la route createRoom, puis en essayant d'utiliser listActiveRooms à nouveau. Vous verrez que la réponse contient la salle que vous venez de créer.

Récupérer un token d'accès pour rejoindre une salle vidéo

La dernière route que vous ajouterez à votre serveur dans la Partie 1 de ce tutoriel est celle qui octroie des tokens d'accès (access token) aux utilisateurs qui rejoindront vos salles vidéo côté client. Pour cette requête, le côté client enverra l'élément roomSid de la salle à laquelle l'utilisateur souhaite accéder, ainsi que l'élément identity de cet utilisateur, qui sera probablement un nom ou un nom d'utilisateur.

Pour en savoir plus sur les tokens d'accès, consultez la documentation à ce sujet ici.

Ajoutez la fonction suivante au fichier server.ts juste en dessous de la fonction listActiveRooms :

/**
 * Get a token for a user for a video room
 */
const getToken = (request: Request, response: Response) => {
  const AccessToken = twilio.jwt.AccessToken;
  const VideoGrant = AccessToken.VideoGrant;

  // Get the user's identity and roomSid from the query.
  const { identity, roomSid } = request.body;

// Create the access token.  
const token = new AccessToken(
    process.env.TWILIO_ACCOUNT_SID as string,
    process.env.TWILIO_API_KEY as string,
    process.env.TWILIO_API_SECRET as string,
    { identity: identity as string }
  );

  token.identity = identity;

  // Add a VideoGrant to the token to allow the user of this token to use Twilio Video
  const grant = new VideoGrant({ room: roomSid as string });
  token.addGrant(grant);

  response.json({
    accessToken: token.toJwt()
  });
};

Ensuite, complétez la liste des routes en ajoutant getToken à la liste :

app.post('/rooms/main', createRoom);
app.post('/rooms/breakout', createBreakoutRoom);
app.get('/rooms/', listActiveRooms);
app.post('/token', getToken);

Si vous souhaitez essayer d'obtenir un token d'accès pour l'une de vos salles vidéo, créez une nouvelle salle à l'aide du point de terminaison createRoom que vous avez écrit précédemment. Vous pouvez également ajouter une salle de groupe si vous le souhaitez ! Copiez l'élément _id de la nouvelle salle, puis transmettez-le à votre point de terminaison getToken à l'aide d'une requête cURL ou HTTPie. Par exemple, si vous avez une salle active nommée Flute Player Chat, vous pouvez obtenir l'élément _id de cette salle et demander un token pour un participant nommé Lizzo. Avec cURL ou HTTPie, transmettez l'élément _id de la salle en tant que roomSid et l'élément identity du participant en tant que Lizzo :

// cURL
curl -X POST localhost:5000/token \
    -d '{"roomSid":"<ROOM_SID>", "identity":"Lizzo"}' \
    -H "Content-Type: application/json"

// HTTPie
http POST localhost:5000/token identity="Lizzo" roomSid="<ROOM_SID>"

Le serveur répond avec un token d'accès, comme dans l'exemple suivant :

{
    "accessToken": "<ACCESS_TOKEN>"
}

Lizzo peut désormais rejoindre la salle Flute Player Chat. Super !

Quelle est la prochaine étape pour votre application vidéo avec des salles de groupe ?

Vous avez désormais une excellente configuration de serveur. Vous pouvez aussi créer et gérer des salles vidéo et leurs sessions en petits groupes. Par la suite, vous aurez peut-être envie aussi d'utiliser la fonctionnalité d'appel vidéo dans votre application front-end.

Pour extraire l'intégralité du code de ce premier tutoriel, consultez la branche updated-server de ce répertoire GitHub.

Dans le tutoriel suivant, vous allez construire le côté client de votre application, à l'aide de React. Pour vous lancer, cliquez ici ! J'ai hâte de voir ce que vous allez construire !

Mia Adjei est développeuse de logiciels au sein de l'équipe Developer Voices. Son rôle consiste à aider les développeurs à créer de nouveaux projets et à connaître de grands moments de satisfaction. Vous pouvez contacter Mia à l'adresse : madjei [at] twilio.com.