Fermer

juin 30, 2025

Comment utiliser NESTJS comme serveur de signalisation pour le chat vidéo WebrTC

Comment utiliser NESTJS comme serveur de signalisation pour le chat vidéo WebrTC


Découvrez WebBrTC et voyez comment construire une application de chat vidéo entre pairs dans NESTJS.

Dans cet article, nous créerons une application de chat vidéo entre pairs avec WebBrTC pour la communication du navigateur direct et NESTJS en tant que serveur de signalisation. Vous apprendrez comment les navigateurs établissent des connexions directes et le rôle des serveurs de signalisation dans ce processus.

Notre application de chat vidéo aura trois fonctionnalités clés:

  • Les deux navigateurs se connecteront directement sans passer des données vidéo via le serveur.
  • NESTJS (notre serveur de signalisation) aidera uniquement les navigateurs à se retrouver et à établir des connexions.
  • Aucun plugins ou API externes ne sera utilisé.

Pourquoi webrtc?

WeBrTC, ou communication Web en temps réel, est un projet gratuit et open-source qui facilite la communication directe entre les navigateurs. Cela élimine le besoin de serveurs intermédiaires, ce qui entraîne des processus plus rapides et plus rentables. Il est soutenu par les normes Web et s’intègre en douceur à JavaScript. Il peut gérer la vidéo, l’audio et d’autres formes de données.

En outre, il propose des outils intégrés qui aident les utilisateurs à se connecter sur divers réseaux. Ces fonctionnalités en font un choix idéal pour les applications Web en temps réel.

Voici un diagramme montrant le flux de la façon dont notre chat vidéo sera créé.

Diagramme de débit webrtc

Le processus de connexion peut être divisé en quatre étapes:

  • Initiation des appels: l’appelant envoie d’abord une offre WebBrTC au serveur de signalisation. Le serveur alerte ensuite les destinataires potentiels en diffusant cette offre.
  • Acceptation d’appel: lorsqu’une Callee choisit de répondre, il renvoie une réponse WebBrTC via le serveur. Le serveur complète la poignée de main en avançant l’appelant d’origine («ils ont répondu»).
  • Préparation du réseau: les deux appareils partagent ensuite leurs détails de réseau avec le serveur, qui les relaie à l’autre partie.
  • Connexion directe: une fois les détails du réseau suffisants sont échangés, WebBrTC établit une connexion entre pairs et le travail du serveur de signalisation est effectué.

Pourquoi avons-nous besoin d’un serveur de signalisation?

La signalisation est nécessaire pour établir des connexions entre les pairs, car il permet le partage des détails de session, tels que les offres, les réponses et les métadonnées du réseau. Sans cela, les navigateurs ne seraient pas en mesure de se localiser ou de négocier les protocoles requis, car WebBrTC ne fournit pas de capacités de messagerie en soi. À l’aide de NESTJS, la signalisation est gérée en toute sécurité via les lignes Web, de sorte que toutes les communications sont cryptées. Il est facile de négliger, mais la signalisation est cruciale pour permettre des interactions entre pairs.

Configuration du projet

C’est à quoi ressemblera la structure du dossier de notre projet.

Diagramme de structure du projet

Configuration du backend

Exécutez la commande suivante dans votre terminal pour configurer un projet NESTJS:

nest new signaling-server
cd signaling-server

Ensuite, installez les dépendances du projet:

npm install @nestjs/websockets @nestjs/platform-socket.io socket.io

Comme indiqué dans le squelette du projet ci-dessus, créez un module de signalisation avec un signaling.gateway.ts fichier et un offer.interface.ts déposer.

Webrtc nécessite HTTPS

WeBrTC nécessite HTTPS pour un accès sécurisé aux caméras, aux microphones et aux connexions par les pairs directes. Pendant le développement, nous pouvons contourner les blocs de sécurité car nous sommes dans un environnement contrôlé. Pour faciliter cela, nous utilisons MKCERT, un outil pour créer des certificats SSL. Nous configurons notre frontend et notre backend pour utiliser ces certificats, nous permettant de tester en toute sécurité sans la surcharge complète de HTTPS.

Pour connecter notre ordinateur portable et notre téléphone portable, nous utiliserons notre adresse IP locale et les deux appareils devraient être connectés au même réseau WiFi.

Maintenant, exécutez les commandes suivantes pour générer les fichiers de certificat.

npm install -g mkcert
mkcert create-ca
mkcert create-cert

Ensuite, mettez à jour votre main.ts dossier avec les éléments suivants:

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import * as fs from "fs";
import { IoAdapter } from "@nestjs/platform-socket.io";

async function bootstrap() {
  const httpsOptions = {
    key: fs.readFileSync("./cert.key"),
    cert: fs.readFileSync("./cert.crt"),
  };

  const app = await NestFactory.create(AppModule, { httpsOptions });
  app.useWebSocketAdapter(new IoAdapter(app));

  
  const localIp = "YOUR-LOCAL-IP-ADDRESS";
  app.enableCors({
    origin: [`https://${localIp}:3000`, "https://localhost:3000"],
    credentials: true,
  });

  await app.listen(8181);
  console.log(`Signaling server running on https://${localIp}:8181`);
}

bootstrap();

Dans le code ci-dessus, nous avons configuré notre serveur de signalisation à l’aide de HTTPS et WebSockets. Tout d’abord, nous définissons un httpsOptions Objet en utilisant notre cert.key et cert.crt fichiers, que nous utilisons lors de la création de notre application dans le NestFactory.create méthode. Ensuite, nous configurons l’application avec le IoAdapterqui permet la prise en charge de la communication WebSocket via Socket.io.

Pour autoriser uniquement les clients frontals à partir d’origine spécifique à l’accès à notre backend, nous permons les COR avec les paramètres d’origine personnalisés et la prise en charge des informations d’identification. Enfin, nous commençons le serveur sur le port 8181 et enregistrons un message pour confirmer qu’il est en cours d’exécution et prêt à gérer la communication sécurisée en temps réel.

Configuration du frontend

Pour configurer notre frontend, exécutez la commande ci-dessous:

cd .. && mkdir webrtc-client && cd webrtc-client && touch index.html scripts.js styles.css socketListeners.js package.json

Ensuite, copiez le cert.key et cert.crt les fichiers du projet NESTJS dans le webrtc-client dossier.

Logique backend

Mettez à jour votre offer.interface.ts fichier avec le code ci-dessous:

export interface ConnectedSocket {
  socketId: string;
  userName: string;
}

export interface Offer {
  offererUserName: string;
  offer: any;
  offerIceCandidates: any[];
  answererUserName: string | null;
  answer: any | null;
  answererIceCandidates: any[];
  socketId: string;
  answererSocketId?: string;
}

Le signaling.gateway.ts Le fichier écoute les événements de WebBrTC et relie les pairs tout en gérant l’état pour les sessions et les candidats, fournissant une coordination efficace sans perturber les flux multimédias.

Installons le cœur de notre passerelle de signalisation, puis parcourons les méthodes nécessaires par la suite.

import {
  WebSocketGateway,
  WebSocketServer,
  OnGatewayConnection,
  OnGatewayDisconnect,
  SubscribeMessage,
} from "@nestjs/websockets";
import { Server, Socket } from "socket.io";
import { Offer, ConnectedSocket } from "./interfaces/offer.interface";

@WebSocketGateway({
  cors: {
    origin: ["https://localhost:3000", "https://YOUR-LOCAL-IP-ADDRESS:3000"],
    methods: ["GET", "POST"],
    credentials: true,
  },
})
export class SignalingGateway
  implements OnGatewayConnection, OnGatewayDisconnect
{
  @WebSocketServer() server: Server;
  private offers: Offer[] = [];
  private connectedSockets: ConnectedSocket[] = [];
}

Le @WebSocketGateway Le décorateur comprend des paramètres COR qui restreignent l’accès à des origines clients spécifiques. Paramètre credentials à true Permet d’envoyer des cookies, des en-têtes d’autorisation ou des certificats clients TLS avec les demandes.

Le SignalingGateway La classe gère automatiquement les connexions et les déconnexions du client en implémentant OnGatewayConnection et OnGatewayDisconnect.

À l’intérieur de la classe, @WebSocketServer() donne accès à l’actif Socket.io instance de serveur, et le offers Les magasins Arrays WeBrTC proposent des objets, qui incluent des descriptions de session et des candidats ICE.

Le connectedSockets Array maintient une liste d’utilisateurs connectés, identifiés par leur ID de socket et leur nom d’utilisateur, permettant au serveur de diriger correctement les messages de signalisation.

Que sont les candidats à la glace?

Les candidats ICE (Interactive Connectivity Iassemment) sont des informations sur le réseau (comme les adresses IP, les ports et les protocoles) qui aident les pairs à trouver le moyen le plus efficace d’établir une connexion directe à peer-to-peer. Ils sont échangés après la négociation de l’offre / réponse et sont essentiels pour naviguer dans les NAT et les pare-feu. Sans eux, la communication WebrTC peut échouer en raison d’obstacles de réseau.

Ensuite, nous implémenterons le handleConnection et handleDisconnect Méthodes pour authentifier les utilisateurs, les enregistrer en mémoire et supprimer proprement leurs données lorsqu’ils se déconnectent.

Mettez à jour votre signaling.gateway.ts dossier avec les éléments suivants:


handleConnection(socket: Socket) {
  const userName = socket.handshake.auth.userName;
  const password = socket.handshake.auth.password;

  if (password !== 'x') {
    socket.disconnect(true);
    return;
  }

  this.connectedSockets.push({ socketId: socket.id, userName });
  if (this.offers.length) socket.emit('availableOffers', this.offers);
}

handleDisconnect(socket: Socket) {
  this.connectedSockets = this.connectedSockets.filter(
    (s) => s.socketId !== socket.id,
  );
  this.offers = this.offers.filter((o) => o.socketId !== socket.id);
}

Le handleConnection la méthode obtient le userName et password à partir des données d’authentification du client. Si le mot de passe est incorrect, la connexion est terminée, mais si elle est correcte, l’utilisateur socketId et userName sera ajouté au connectedSockets tableau.

S’il y a des offres qui n’ont pas encore été gérées, le serveur les envoie à l’utilisateur nouvellement connecté via le availableOffers événement.

Le handleDisconnect la méthode supprime la prise déconnectée des deux connectedSockets tableau et le offers liste. Ce nettoyage empêche les données périmées de s’accumuler et ne conserve que les connexions actives conservées.

La logique de filtrage conserve toutes les entrées qui ne correspondent pas à l’ID de la prise déconnectée.

Ensuite, nous implémenterons des méthodes pour gérer des événements WebSocket spécifiques: offres, réponses et candidats ICE, qui sont essentiels pour créer des connexions entre pairs dans WebBrTC.

Mettez à jour votre signaling.gateway.ts dossier avec les éléments suivants:


@SubscribeMessage('newOffer')
handleNewOffer(socket: Socket, newOffer: any) {
  const userName = socket.handshake.auth.userName;
  const newOfferEntry: Offer = {
    offererUserName: userName,
    offer: newOffer,
    offerIceCandidates: [],
    answererUserName: null,
    answer: null,
    answererIceCandidates: [],
    socketId: socket.id,
  };

  this.offers = this.offers.filter((o) => o.offererUserName !== userName);
  this.offers.push(newOfferEntry);
  socket.broadcast.emit('newOfferAwaiting', [newOfferEntry]);
}

@SubscribeMessage('newAnswer')
async handleNewAnswer(socket: Socket, offerObj: any) {
  const userName = socket.handshake.auth.userName;
  const offerToUpdate = this.offers.find(
    (o) => o.offererUserName === offerObj.offererUserName,
  );

  if (!offerToUpdate) return;

  
  socket.emit('existingIceCandidates', offerToUpdate.offerIceCandidates);

  
  offerToUpdate.answer = offerObj.answer;
  offerToUpdate.answererUserName = userName;
  offerToUpdate.answererSocketId = socket.id;

  
  this.server
    .to(offerToUpdate.socketId)
    .emit('answerResponse', offerToUpdate);
  socket.emit('answerConfirmation', offerToUpdate);
}

@SubscribeMessage('sendIceCandidateToSignalingServer')
handleIceCandidate(socket: Socket, iceCandidateObj: any) {
  const { didIOffer, iceUserName, iceCandidate } = iceCandidateObj;

  
  const offer = this.offers.find((o) =>
    didIOffer
      ? o.offererUserName === iceUserName
      : o.answererUserName === iceUserName,
  );

  if (offer) {
    if (didIOffer) {
      offer.offerIceCandidates.push(iceCandidate);
    } else {
      offer.answererIceCandidates.push(iceCandidate);
    }
  }

  
  const targetUserName = didIOffer
    ? offer?.answererUserName
    : offer?.offererUserName;
  const targetSocket = this.connectedSockets.find(
    (s) => s.userName === targetUserName,
  );

  if (targetSocket) {
    this.server
      .to(targetSocket.socketId)
      .emit('receivedIceCandidateFromServer', iceCandidate);
  }
}

Gestionnaire d’offre

Cette méthode traite les offres de WebBrTC entrantes à partir des appelants, créant un nouvel objet d’offre avec le nom d’utilisateur de l’appelant, la description de session et les tableaux de candidats de glace vides. Le serveur supprime toutes les offres existantes du même utilisateur pour empêcher les doublons, puis stocke et diffuse la nouvelle offre à tous les clients connectés pour que Callees puisse répondre.

Gestionnaire de réponses

Après avoir reçu une réponse à une offre, le serveur localise l’offre d’origine. Il envoie des candidats ICE existants de l’appelant au client de réponse pour accélérer la connexion. Le serveur met à jour l’objet de l’offre avec les détails de réponse, y compris le nom d’utilisateur et l’ID de socket de la Callee. Les deux parties obtiennent des notifications: l’appelant d’origine reçoit la réponse et le client répondant obtient une confirmation.

Gestionnaire de candidats de glace

Cette méthode traite les candidats ICE à partir de pairs. Il détermine si chaque candidat provient d’un offrande ou d’un répondeur en utilisant le didIOffer Flag, le stockant dans le tableau approprié dans l’objet Offre. Le serveur relaie chaque candidat au pair correspondant en recherchant leur ID de socket et continue jusqu’à ce que les pairs établissent une connexion directe.

Ensuite, exécutez cette commande pour démarrer le serveur:

npm run start:dev

Logique du frontend

Mettez à jour votre Index.html dossier avec les éléments suivants:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>WebRTC with NestJS Signaling</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
    <script>
      
      document.addEventListener("DOMContentLoaded", async () => {
        try {
          const stream = await navigator.mediaDevices.getUserMedia({
            video: { facingMode: "user" }, 
            audio: false,
          });
          stream.getTracks().forEach((track) => track.stop());
        } catch (err) {
          console.log("Pre-permission error:", err);
        }
      });
    </script>
  </head>
  <body>
    <div class="container">
      <div class="row mb-3 mt-3 justify-content-md-center">
        <div id="user-name" class="col-12 text-center mb-2"></div>
        <button id="call" class="btn btn-primary col-3">Start Call</button>
        <div id="answer" class="col-6"></div>
      </div>
      <div id="videos">
        <div id="video-wrapper">
          <div id="waiting">Waiting for answer...</div>
          <video
            class="video-player"
            id="local-video"
            autoplay
            playsinline
            muted
          ></video>
        </div>
        <video
          class="video-player"
          id="remote-video"
          autoplay
          playsinline
        ></video>
      </div>
    </div>
    
    <script src="https://cdn.socket.io/4.7.4/socket.io.min.js"></script>
    <script src="scripts.js"></script>
    <script src="socketListeners.js"></script>
  </body>
</html>

Puis mettez à jour votre styles.css dossier avec les éléments suivants:

#videos {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2em;
}
.video-player {
  background-color: black;
  width: 100%;
  height: 300px;
  border-radius: 8px;
}
#video-wrapper {
  position: relative;
}
#waiting {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
  width: 200px;
  height: 40px;
  background: rgba(0, 0, 0, 0.7);
  color: white;
  text-align: center;
  line-height: 40px;
  border-radius: 5px;
}
#answer {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}
#user-name {
  font-weight: bold;
  font-size: 1.2em;
}

Nous diviserons le code pour le script.js Fichier en deux parties: initialisation et configuration et fonctionnalité de base et écouteur d’événements.

Mettez à jour votre script.js fichier avec le code pour l’initialisation et la configuration:

const userName = "User-" + Math.floor(Math.random() * 1000);
const password = "x";
document.querySelector("#user-name").textContent = userName;
const localIp = "YOUR-LOCAL-IP-ADDRESS";
const socket = io(`https://${localIp}:8181`, {
  auth: { userName, password },
  transports: ["websocket"],
  secure: true,
  rejectUnauthorized: false,
});

const localVideoEl = document.querySelector("#local-video");
const remoteVideoEl = document.querySelector("#remote-video");
const waitingEl = document.querySelector("#waiting");

const peerConfiguration = {
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  iceTransportPolicy: "all",
};

let localStream;
let remoteStream;
let peerConnection;
let didIOffer = false;

Le code crée une connexion WebSocket sécurisée au serveur de signalisation NESTJS. La configuration de WebBrTC comprend des serveurs de glace essentiels pour la traversée du réseau et vise une connectivité maximale. Il initialise également les variables pour gérer les flux multimédias et suivre la connexion homologue active.

Maintenant, ajoutons les fonctions de base et l’écouteur d’événements:


const startCall = async () => {
  try {
    await getLocalStream();
    await createPeerConnection();
    const offer = await peerConnection.createOffer();
    await peerConnection.setLocalDescription(offer);
    didIOffer = true;
    socket.emit("newOffer", offer);
    waitingEl.style.display = "block";
  } catch (err) {
    console.error("Call error:", err);
  }
};
const answerCall = async (offerObj) => {
  try {
    await getLocalStream();
    await createPeerConnection(offerObj);
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    
    const offerIceCandidates = await new Promise((resolve) => {
      socket.emit(
        "newAnswer",
        {
          ...offerObj,
          answer,
          answererUserName: userName,
        },
        resolve
      );
    });
    
    offerIceCandidates.forEach((c) => {
      peerConnection
        .addIceCandidate(c)
        .catch((err) => console.error("Error adding ICE candidate:", err));
    });
  } catch (err) {
    console.error("Answer error:", err);
  }
};
const getLocalStream = async () => {
  const constraints = {
    video: {
      facingMode: "user",
      width: { ideal: 1280 },
      height: { ideal: 720 },
    },
    audio: false,
  };
  try {
    localStream = await navigator.mediaDevices.getUserMedia(constraints);
    localVideoEl.srcObject = localStream;
    localVideoEl.play().catch((e) => console.log("Video play error:", e));
  } catch (err) {
    alert("Camera error: " + err.message);
    throw err;
  }
};
const createPeerConnection = async (offerObj) => {
  peerConnection = new RTCPeerConnection(peerConfiguration);
  remoteStream = new MediaStream();
  remoteVideoEl.srcObject = remoteStream;
  
  localStream.getTracks().forEach((track) => {
    peerConnection.addTrack(track, localStream);
  });
  
  peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      socket.emit("sendIceCandidateToSignalingServer", {
        iceCandidate: event.candidate,
        iceUserName: userName,
        didIOffer,
      });
    }
  };
  
  peerConnection.ontrack = (event) => {
    event.streams[0].getTracks().forEach((track) => {
      if (!remoteStream.getTracks().some((t) => t.id === track.id)) {
        remoteStream.addTrack(track);
      }
    });
    waitingEl.style.display = "none";
  };
  
  peerConnection.onconnectionstatechange = () => {
    console.log("Connection state:", peerConnection.connectionState);
    if (peerConnection.connectionState === "failed") {
      alert("Connection failed! Please try again.");
    }
  };
  
  if (offerObj) {
    await peerConnection
      .setRemoteDescription(offerObj.offer)
      .catch((err) => console.error("setRemoteDescription error:", err));
  }
};

document.querySelector("#call").addEventListener("click", startCall);

Cette section gère l’intégralité du processus d’appel WebBrTC. Il configure une connexion par les pairs, crée des descriptions de session et travaille avec le serveur de signalisation pour partager les paquets SDP de l’offre / répond.

Le processus de réponse synchronise les candidats ICE pour échanger les détails du chemin du réseau. Il suit également les ajouts, génère des candidats ICE, surveille les états de connexion et met à jour l’interface utilisateur.

Ensuite, ajoutez ce qui suit à votre fichier socketListeners.js:


socket.on("availableOffers", (offers) => {
  console.log("Received available offers:", offers);
  createOfferElements(offers);
});

socket.on("newOfferAwaiting", (offers) => {
  console.log("Received new offers awaiting:", offers);
  createOfferElements(offers);
});

socket.on("answerResponse", (offerObj) => {
  console.log("Received answer response:", offerObj);
  peerConnection
    .setRemoteDescription(offerObj.answer)
    .catch((err) => console.error("setRemoteDescription failed:", err));
  waitingEl.style.display = "none";
});

socket.on("receivedIceCandidateFromServer", (iceCandidate) => {
  console.log("Received ICE candidate:", iceCandidate);
  peerConnection
    .addIceCandidate(iceCandidate)
    .catch((err) => console.error("Error adding ICE candidate:", err));
});

socket.on("existingIceCandidates", (candidates) => {
  console.log("Receiving existing ICE candidates:", candidates);
  candidates.forEach((c) => {
    peerConnection
      .addIceCandidate(c)
      .catch((err) =>
        console.error("Error adding existing ICE candidate:", err)
      );
  });
});

function createOfferElements(offers) {
  const answerEl = document.querySelector("#answer");
  answerEl.innerHTML = ""; 
  offers.forEach((offer) => {
    const button = document.createElement("button");
    button.className = "btn btn-success";
    button.textContent = `Answer ${offer.offererUserName}`;
    button.onclick = () => answerCall(offer);
    answerEl.appendChild(button);
  });
}

Ce fichier gère le côté client du processus de signalisation WebTC en utilisant Socket.io événements. Il écoute les offres d’appels entrantes («disponibles» et «newOfferawaiting») et génère dynamiquement des boutons «Réponse» qui permettent à l’utilisateur de répondre et d’établir une connexion.

Lorsqu’une réponse est reçue («réponderResponse»), la description de session de pairs distante est définie et l’indicateur d’attente est masqué.

Les candidats à la glace sont manipulés en deux parties:

  • Candidats en temps réel par «reçus en matière de services de service»
  • Précédemment échangé des candidats par des «échecs existants»

Les deux sont ajoutés à la RTCPeerConnection actuelle, avec une gestion des erreurs incluse.

Enfin, mettez à jour votre package.json dossier avec les éléments suivants:

{
  "name": "webrtc-client",
  "version": "1.0.0",
  "scripts": {
    "start": "http-server -S -C cert.crt -K cert.key -p 3000"
  },
  "dependencies": {
    "http-server": "^14.1.1"
  }
}

Puis installer et exécuter:

npm install
npm start

Essai

Sur les deux navigateurs, ouvrez https: // votre Local-ip-address: 3000, puis commencez un appel à l’un et répondez-y de l’autre.

Image montrant un chat vidéo réussi

Problèmes communs

Assurez-vous que votre appareil photo fonctionne en permettant à la permission du navigateur d’y accéder et vérifiez que vous utilisez HTTPS. Certains réseaux peuvent bloquer les serveurs Stun, empêchant les connexions directes de peer-to-peer. Si cela se produit, vous devrez peut-être implémenter un serveur de virage pour la connexion. Si les ID de socket ne sont pas les mêmes aux deux extrémités, cela peut entraîner des problèmes de signalisation, alors vérifiez cela lors du dépannage.

Conclusion

Nous avons créé un serveur de signalisation de base et un client pour les appels vidéo peer-to-peer, couvrant des fonctions clés telles que la négociation des offres / réponses et l’échange de candidats ICE. Cette configuration montre que les concepts WEBRTC fondamentaux et la façon dont les serveurs de signalisation aident à établir des connexions directes sans gérer les flux de supports, optimisant ainsi les performances et la confidentialité. Pour améliorer l’application, envisagez d’ajouter du chat texte via les canaux de données WebTC et d’activer le partage d’écran.




Source link