Maîtriser WebRTC : Communications Audio, Vidéo et Données en Temps Réel sur le Web
Maîtriser WebRTC : Communications Audio, Vidéo et Données en Temps Réel sur le Web

Accéder aux Flux Média Locaux avec getUserMedia

Introduction au Contexte WebRTC

Bienvenue dans ce module de notre cours "Maîtriser WebRTC : Communications Audio, Vidéo et Données en Temps Réel sur le Web". Avant de pouvoir établir une communication en temps réel entre deux navigateurs ou plus, il est essentiel de pouvoir capturer les flux audio et vidéo locaux de l'utilisateur. C'est précisément le rôle de l'API getUserMedia().

Dans cette leçon, nous allons explorer en profondeur getUserMedia() : son fonctionnement, les permissions qu'elle requiert, comment spécifier les médias souhaités (caméra, microphone, résolution, etc.), et comment gérer les succès et les écheurs. À la fin de cette leçon, vous serez capable d'accéder aux périphériques médias de l'utilisateur et d'afficher leurs flux dans une application web.

Qu'est-ce que getUserMedia ?

getUserMedia() est une API JavaScript qui fait partie de l'interface MediaDevices de l'API MediaDevices (accessible via navigator.mediaDevices). Elle permet à une application web de demander l'accès aux périphériques d'entrée multimédia de l'utilisateur, tels que les caméras vidéo et les microphones audio.

Lorsque cette fonction est appelée, le navigateur affiche une boîte de dialogue de permission à l'utilisateur, lui demandant s'il autorise l'accès à ses périphériques. Si l'utilisateur accorde la permission, getUserMedia() retourne une Promise qui, une fois résolue, fournit un objet MediaStream.

Le MediaStream : L'Élément Central

Un MediaStream (ou simplement "flux") est un concept clé en WebRTC. Il représente un flux de données multimédia. Un MediaStream peut contenir :

  • Des pistes audio (MediaStreamTrack de type "audio").
  • Des pistes vidéo (MediaStreamTrack de type "video").

Ces pistes proviennent généralement d'une source locale (comme un microphone ou une caméra via getUserMedia), mais elles peuvent aussi provenir d'une source distante (reçue via RTCPimereConnection) ou même d'un écran partagé (via getDisplayMedia).

Une fois que vous avez un MediaStream, vous pouvez :

  • L'assigner à un élément <video> ou <audio> pour l'afficher ou l'écouter localement.
  • L'envoyer à un pair distant via une RTCPeerConnection pour la communication en temps réel.
  • Le traiter (par exemple, avec une API comme Web Audio API ou Canvas API).

Demande de Permissions et Sécurité

La confidentialité des utilisateurs est primordiale. C'est pourquoi getUserMedia() est soumis à des règles de sécurité strictes :

  1. Demande Explicite de l'Utilisateur : Le navigateur doit afficher une boîte de dialogue de permission. L'utilisateur a le contrôle total et peut accepter ou refuser l'accès.
  2. Contexte Sécurisé (HTTPS) : Pour des raisons de sécurité, getUserMedia() ne peut être appelé que depuis un contexte sécurisé, c'est-à-dire une page servie via HTTPS ou depuis localhost. Tenter de l'appeler sur une page HTTP déclenchera une SecurityError.
  3. Indicateur Visuel : La plupart des navigateurs affichent un indicateur visuel (par exemple, une icône dans l'onglet de la barre d'adresse) pour montrer que la caméra ou le microphone est en cours d'utilisation.

Si l'utilisateur refuse la permission, getUserMedia() rejettera la Promise avec une NotAllowedError. Il est crucial de gérer cette erreur gracieusement dans votre application.

Utilisation de getUserMedia : La Syntaxe de Base

L'API getUserMedia() est accessible via navigator.mediaDevices. Sa syntaxe de base est la suivante :

navigator.mediaDevices.getUserMedia(constraints)
    .then(function(stream) {
        /* Utilisez le MediaStream ici */
    })
    .catch(function(error) {
        /* Gérez l'erreur ici */
    });
  • constraints : Un objet JavaScript qui spécifie les exigences pour les pistes audio et/ou vidéo. C'est le cœur de la configuration.
  • La fonction then() est appelée si l'accès est accordé et que le flux est disponible. Elle reçoit l'objet MediaStream.
  • La fonction catch() est appelée si l'accès est refusé ou si une autre erreur survient. Elle reçoit un objet MediaStreamError.

Les Contraintes (Constraints) : Spécifier le Média Désiré

L'objet constraints est un dictionnaire qui vous permet de spécifier précisément le type et les caractéristiques des médias que vous souhaitez acquérir. Il est composé de deux propriétés principales : audio et video. Chacune peut être un booléen ou un objet plus détaillé.

Booléen Simple

  • audio: true : Demande un microphone.
  • video: true : Demande une caméra.

Exemple :

const constraints = {
    audio: true, // Je veux un microphone
    video: true  // Je veux une caméra
};

Objet Détaillé

Pour plus de contrôle, vous pouvez fournir un objet pour audio et/ou video afin de spécifier des exigences plus précises, telles que la résolution de la vidéo, le facingMode (caméra avant/arrière), l'ID du périphérique, etc.

Contraintes Vidéo

Les contraintes vidéo les plus courantes incluent :

  • width, height : Résolution souhaitée (par exemple, { width: 1280, height: 720 } pour HD).
  • frameRate : Fréquence d'images souhaitée (par exemple, { frameRate: 30 }).
  • facingMode : Permet de choisir la caméra avant ou arrière sur les appareils mobiles.
    • 'user' : Caméra frontale (selfie).
    • 'environment' : Caméra arrière.
    • Peut être spécifié avec exact ou ideal (voir ci-dessous).
  • deviceId : L'ID spécifique d'une caméra ou d'un microphone, obtenu via navigator.mediaDevices.enumerateDevices().

Exemple de contraintes vidéo avancées :

const constraints = {
    video: {
        width: { ideal: 1920 },      // Idéalement 1920 pixels de large
        height: { ideal: 1080 },     // Idéalement 1080 pixels de haut
        frameRate: { ideal: 30 },    // Idéalement 30 images par seconde
        facingMode: { exact: "user" } // Exige la caméra frontale
    },
    audio: true
};

Contraintes Audio

Les contraintes audio sont généralement plus simples :

  • echoCancellation : Booléen, pour activer ou désactiver l'annulation d'écho.
  • noiseSuppression : Booléen, pour activer ou désactiver la suppression de bruit.
  • autoGainControl : Booléen, pour activer ou désactiver le contrôle automatique du gain.
  • deviceId : L'ID spécifique d'un microphone.

Exemple de contraintes audio :

const constraints = {
    audio: {
        echoCancellation: true,
        noiseSuppression: true
    },
    video: true
};

Types de Contraintes : exact et ideal

Pour les valeurs numériques (comme width, height, frameRate) ou certaines chaînes de caractères (facingMode), vous pouvez utiliser des mots-clés spéciaux pour exprimer vos préférences :

  • exact : Exige la valeur spécifiée. Si le navigateur ne peut pas fournir cette valeur exacte, la Promise sera rejetée avec une OverconstrainedError.
  • ideal : Préfère la valeur spécifiée, mais permet au navigateur de choisir la valeur la plus proche et la plus performante disponible. C'est souvent le meilleur choix pour la compatibilité et la flexibilité.

Pour les plages, vous pouvez également utiliser min et max (par exemple, { width: { min: 640, ideal: 1280 } }).

Exemple Pratique : Accéder et Afficher le Flux Vidéo

Mettons en pratique ce que nous avons appris en créant une page web simple qui accède à la caméra de l'utilisateur et affiche le flux vidéo.

Structure HTML (index.html)

Nous aurons besoin d'un élément <video> pour afficher le flux et d'un élément pour afficher les messages d'erreur.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Démonstration getUserMedia</title>
    <style>
        body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; margin-top: 50px; }
        video { border: 2px solid #333; max-width: 80%; margin-top: 20px; background-color: #eee; }
        #errorMessage { color: red; margin-top: 20px; font-weight: bold; }
    </style>
</head>
<body>
    <h1>Accéder à votre Caméra avec `getUserMedia`</h1>
    <video id="localVideo" autoplay playsinline controls></video>
    <p id="errorMessage"></p>

    <script src="app.js"></script>
</body>
</html>
  • L'attribut autoplay permet à la vidéo de démarrer automatiquement une fois le flux chargé.
  • playsinline est important pour la lecture automatique sur les appareils iOS.
  • controls est optionnel, mais utile pour le débogage.

Code JavaScript (app.js)

Ce script gérera la demande getUserMedia et l'affichage du flux.

// app.js

document.addEventListener('DOMContentLoaded', () => {
    const localVideo = document.getElementById('localVideo');
    const errorMessage = document.getElementById('errorMessage');

    // Définir les contraintes pour le média souhaité
    const constraints = {
        audio: false, // Pas de microphone pour cet exemple
        video: {
            width: { ideal: 1280 },  // Préférence pour une largeur de 1280px
            height: { ideal: 720 },  // Préférence pour une hauteur de 720px
            facingMode: 'user'       // Essayer d'obtenir la caméra frontale (si disponible)
        }
    };

    /**
     * Fonction asynchrone pour initier l'accès aux médias.
     */
    async function getLocalMediaStream() {
        try {
            // Tenter d'obtenir le flux média en fonction des contraintes
            const stream = await navigator.mediaDevices.getUserMedia(constraints);

            // Si réussi, attacher le MediaStream à l'élément vidéo
            localVideo.srcObject = stream;

            // Optionnel: Ajouter un écouteur pour quand la vidéo est prête à jouer
            localVideo.onloadedmetadata = () => {
                console.log('Flux vidéo chargé avec succès. Dimensions:', 
                            stream.getVideoTracks()[0].getSettings().width, 'x', 
                            stream.getVideoTracks()[0].getSettings().height);
                errorMessage.textContent = ''; // Effacer tout message d'erreur précédent
            };

        } catch (err) {
            // Gérer les erreurs
            console.error('Erreur lors de l\'accès au média:', err);
            let userMessage = 'Une erreur est survenue lors de l\'accès à votre caméra.';

            if (err.name === 'NotAllowedError') {
                userMessage = 'Permission refusée : Vous devez autoriser l\'accès à la caméra pour continuer.';
            } else if (err.name === 'NotFoundError') {
                userMessage = 'Aucune caméra trouvée : Assurez-vous qu\'une caméra est connectée et activée.';
            } else if (err.name === 'NotReadableError') {
                userMessage = 'Caméra occupée : Une autre application utilise peut-être votre caméra. Veuillez la fermer et réessayer.';
            } else if (err.name === 'OverconstrainedError') {
                userMessage = 'Contraintes trop strictes : Impossible de satisfaire les exigences de résolution ou autres pour la caméra. Essayez des contraintes moins spécifiques.';
                console.log('Contraintes actuelles:', constraints.video);
                console.log('Capacités de l\'appareil:', err.constraint); // Affiche la contrainte qui a posé problème
            } else if (err.name === 'SecurityError') {
                userMessage = 'Accès non sécurisé : Cette page doit être servie via HTTPS ou depuis localhost pour accéder à la caméra.';
            } else if (err.name === 'TypeError') {
                 userMessage = 'Erreur de type : Les contraintes de média sont invalides.';
            }
            
            errorMessage.textContent = userMessage;
        }
    }

    // Appeler la fonction pour obtenir le flux média au chargement de la page
    getLocalMediaStream();
});

Explication du Code

  1. Récupération des Éléments HTML : Nous commençons par obtenir des références aux éléments <video> et <p id="errorMessage"> dans le DOM.
  2. Définition des Contraintes : L'objet constraints indique que nous voulons seulement une piste vidéo. Pour la vidéo, nous préférons une résolution HD (1280x720) et tentons d'utiliser la caméra frontale (facingMode: 'user'). L'utilisation de ideal permet au navigateur de choisir la meilleure résolution disponible si 1280x720 n'est pas possible.
  3. Appel à getUserMedia : navigator.mediaDevices.getUserMedia(constraints) est l'appel principal. Il retourne une Promise.
  4. Gestion du Succès (.then() ou await) :
    • Si la Promise est résolue (stream est reçu), nous assignons ce stream à la propriété srcObject de l'élément <video>. C'est la méthode recommandée pour connecter un MediaStream à un élément média HTML5.
    • Le onloadedmetadata est un écouteur optionnel qui se déclenche une fois que le navigateur a suffisamment de métadonnées pour lire la vidéo. Il est utile pour vérifier la résolution réelle du flux obtenu.
  5. Gestion des Erreurs (.catch() ou try...catch) :
    • Si la Promise est rejetée, la fonction catch est exécutée.
    • L'objet err contient des informations sur l'erreur. Le name de l'erreur est crucial pour identifier la cause (par exemple, NotAllowedError si l'utilisateur a refusé).
    • Le code affiche un message d'erreur approprié à l'utilisateur, ce qui est essentiel pour une bonne expérience utilisateur.

Pour faire fonctionner cet exemple, vous devrez le servir via un serveur web local (par exemple, avec Live Server pour VS Code, ou un simple python -m http.server dans votre terminal depuis le dossier du projet) et y accéder via http://localhost:port ou via HTTPS.

Gestion des Erreurs : MediaStreamError

Comprendre les différents types d'erreurs est essentiel pour déboguer et améliorer l'expérience utilisateur. L'objet MediaStreamError renvoyé par le catch peut avoir plusieurs noms :

  • NotAllowedError : L'utilisateur a refusé la permission d'accéder aux périphériques médias.
  • NotFoundError : Aucun périphérique média correspondant aux contraintes spécifiées n'a été trouvé (par exemple, pas de webcam connectée).
  • NotReadableError : Le système d'exploitation ou le matériel n'a pas pu donner accès aux périphériques (par exemple, la caméra est déjà utilisée par une autre application).
  • OverconstrainedError : Les contraintes spécifiées sont trop strictes et aucun périphérique ne peut y satisfaire (par exemple, vous avez demandé une résolution 4K avec exact alors que la caméra ne supporte que le 1080p). L'objet d'erreur contiendra une propriété constraint indiquant quelle contrainte n'a pas pu être satisfaite.
  • SecurityError : L'appel à getUserMedia() a été fait depuis un contexte non sécurisé (non-HTTPS).
  • TypeError : Les contraintes spécifiées sont invalides (par exemple, une propriété inconnue ou une valeur mal typée).

Une gestion robuste des erreurs permet d'informer clairement l'utilisateur sur la raison pour laquelle l'application ne peut pas accéder à ses médias et, si possible, de lui suggérer des solutions.

Conclusion

getUserMedia() est la passerelle essentielle vers le monde de WebRTC. En vous permettant d'accéder et de contrôler les périphériques média locaux de l'utilisateur, elle ouvre la porte à des applications interactives telles que la vidéoconférence, l'enregistrement audio/vidéo et les expériences de réalité augmentée.

Vous avez appris :

  • Le rôle fondamental de getUserMedia() dans la capture de flux audio et vidéo locaux.
  • L'importance du MediaStream comme conteneur pour ces flux.
  • Les impératifs de sécurité et de permission (HTTPS, boîte de dialogue utilisateur).
  • Comment utiliser l'objet constraints pour spécifier avec précision les propriétés des médias désirés (résolution, facingMode, etc.).
  • Comment gérer les succès et, surtout, les différentes erreurs potentielles, pour offrir une expérience utilisateur robuste.

Dans les prochaines leçons, nous verrons comment ces MediaStream peuvent être envoyés et reçus entre pairs en utilisant l'API RTCPeerConnection, le cœur de la communication en temps réel de WebRTC.