Revoir le stream !

Pour un Youtubeur à 500k abonnés, je réalise un quiz, avec Angular, où les participants répondent en temps réel. C'est l'occasion, en direct, de réaliser et de voir ensemble des développements d'Angular: Streaming Resources, gestion du temps réel, utilisation de l'IA pour générer l'UI, etc.

Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

WebRTC avec Angular

La communication en temps réel est devenue essentielle dans nos applications web modernes. Imaginez que vous développez une application de téléconsultation médicale : les patients doivent pouvoir parler directement à leur médecin via vidéo, sans que les données sensibles ne transitent par un serveur. C'est exactement ce que permet WebRTC !

Qu'est-ce que WebRTC ?

WebRTC (Web Real-Time Communication) est une technologie qui permet aux navigateurs web de communiquer directement entre eux (peer-to-peer), sans passer par un serveur intermédiaire. C'est comme si deux personnes se parlaient directement plutôt que de passer par un intermédiaire pour transmettre leurs messages.

AVANTAGES

  • Communication directe et rapide
  • Sécurisé par défaut (chiffrement bout en bout)
  • Parfait pour l'audio, la vidéo et le partage de données
  • Réduit la charge serveur

Implémentation dans Angular

Commençons par créer un service qui gérera notre connexion WebRTC :

ts
import { Injectable, signal } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class WebRTCService {
  // Signal pour gérer l'état de la connexion
  private connectionState = signal<'disconnected' | 'connecting' | 'connected'>('disconnected');
  
  // Configuration des serveurs STUN pour la négociation de connexion
  private configuration: RTCConfiguration = {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' }
    ]
  };

  // Instance de la connexion peer-to-peer
  private peerConnection: RTCPeerConnection | null = null;

  // Flux local de la webcam
  private localStream = signal<MediaStream | null>(null);

  constructor() {
    this.initializePeerConnection();
  }

  /**
   * Initialise la connexion WebRTC avec la configuration STUN
   */
  private initializePeerConnection() {
    this.peerConnection = new RTCPeerConnection(this.configuration);
    
    // Gestion des changements d'état de la connexion
    this.peerConnection.onconnectionstatechange = () => {
      this.connectionState.set(this.peerConnection?.connectionState as any);
    };
  }

  /**
   * Démarre la capture vidéo locale
   */
  async startLocalStream() {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true
      });
      
      this.localStream.set(stream);
      
      // Ajout des tracks au peer connection
      stream.getTracks().forEach(track => {
        this.peerConnection?.addTrack(track, stream);
      });
      
      return stream;
    } catch (error) {
      console.error('Erreur lors de l\'accès à la webcam:', error);
      throw error;
    }
  }
}
ts
import { Component } from '@angular/core';
import { WebRTCService } from './webrtc.service';

@Component({
  selector: 'app-video-chat',
  template: `
    <div class="video-container">
      @if (localVideoStream()) {
        <video #localVideo [srcObject]="localVideoStream()" autoplay playsinline></video>
      }
      
      @if (remoteVideoStream()) {
        <video #remoteVideo [srcObject]="remoteVideoStream()" autoplay playsinline></video>
      }
      
      <div class="controls">
        <button (click)="startCall()">Démarrer l'appel</button>
        <button (click)="endCall()">Terminer l'appel</button>
      </div>
    </div>
  `,
  styles: [`
    .video-container {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
      padding: 1rem;
    }
    
    video {
      width: 100%;
      border-radius: 8px;
      background: #000;
    }
    
    .controls {
      grid-column: 1 / -1;
      display: flex;
      justify-content: center;
      gap: 1rem;
    }
  `]
})
export class VideoChatComponent {
  private webRTCService = inject(WebRTCService);
  
  localVideoStream = signal<MediaStream | null>(null);
  remoteVideoStream = signal<MediaStream | null>(null);

  async startCall() {
    try {
      const stream = await this.webRTCService.startLocalStream();
      this.localVideoStream.set(stream);
    } catch (error) {
      console.error('Erreur lors du démarrage de l\'appel:', error);
    }
  }

  endCall() {
    this.localVideoStream()?.getTracks().forEach(track => track.stop());
    this.localVideoStream.set(null);
  }
}

Établissement de la connexion

Pour établir une connexion WebRTC entre deux pairs, nous devons suivre un processus appelé "signaling". C'est comme si deux personnes voulaient se rencontrer et avaient besoin d'un intermédiaire pour échanger leurs coordonnées.

IMPORTANT

WebRTC ne fournit pas le mécanisme de signaling. Vous devez implémenter votre propre solution (WebSocket, HTTP, etc.) pour échanger les informations de connexion entre les pairs.

Voici comment fonctionne le processus :

  1. Le pair A crée une offre
  2. Le pair A envoie cette offre au pair B via le serveur de signaling
  3. Le pair B reçoit l'offre et crée une réponse
  4. Le pair B envoie sa réponse au pair A
  5. Les deux pairs échangent des candidats ICE pour établir la meilleure connexion possible

https://excalidraw.com/#json=fPn62ObxdpM-DxBMrl4k0,9eVZdzCiAXhJL7loI-on4A

Voici comment implémenter ce processus :

ts
/**
 * Crée une offre de connexion
 */
async createOffer() {
  if (!this.peerConnection) return;
  
  try {
    const offer = await this.peerConnection.createOffer();
    await this.peerConnection.setLocalDescription(offer);
    return offer;
  } catch (error) {
    console.error('Erreur lors de la création de l\'offre:', error);
    throw error;
  }
}

/**
 * Traite une offre reçue et crée une réponse
 */
async handleOffer(offer: RTCSessionDescriptionInit) {
  if (!this.peerConnection) return;
  
  try {
    await this.peerConnection.setRemoteDescription(offer);
    const answer = await this.peerConnection.createAnswer();
    await this.peerConnection.setLocalDescription(answer);
    return answer;
  } catch (error) {
    console.error('Erreur lors du traitement de l\'offre:', error);
    throw error;
  }
}

Gestion des événements

WebRTC génère plusieurs événements importants que nous devons gérer :

ts
// Réception d'une nouvelle piste média
this.peerConnection.ontrack = (event) => {
  // Mise à jour du flux vidéo distant
  const [remoteStream] = event.streams;
  this.remoteStream.set(remoteStream);
};

// Gestion des candidats ICE
this.peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    // Envoyer le candidat à l'autre pair via le serveur de signaling
  }
};

CONSEIL

Pour une meilleure expérience utilisateur, ajoutez des indicateurs visuels pour :

  • L'état de la connexion
  • La qualité du réseau
  • Les problèmes de périphériques

Bonnes pratiques

  1. Gestion des erreurs : Implémentez une gestion robuste des erreurs pour les cas où :

    • L'utilisateur refuse l'accès à la caméra/micro
    • La connexion échoue
    • Un pair se déconnecte
  2. Adaptabilité : Utilisez les contraintes média pour s'adapter aux capacités du dispositif :

ts
const constraints = {
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { max: 30 }
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true
  }
};
  1. Sécurité : Bien que WebRTC soit sécurisé par défaut, assurez-vous de :
    • Utiliser HTTPS pour votre serveur de signaling
    • Valider les pairs avant d'établir une connexion
    • Implémenter un système d'authentification

POUR ALLER PLUS LOIN

  • Implémentez le partage d'écran avec getDisplayMedia()
  • Ajoutez des filtres vidéo avec WebGL
  • Utilisez les DataChannels pour le partage de fichiers