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

# Implémenter le Partage d'Écran avec `getDisplayMedia`

## Introduction

Bienvenue dans cette leçon dédiée à l'implémentation du partage d'écran, une fonctionnalité essentielle pour de nombreuses applications WebRTC modernes, telles que les visioconférences, les démonstrations de produits, le support technique à distance, ou encore le gaming en ligne. Dans le cadre de notre cours "Maîtriser WebRTC", nous avons déjà exploré comment capturer des flux audio et vidéo à partir de la caméra et du microphone de l'utilisateur avec `getUserMedia()`. Aujourd'hui, nous allons franchir une nouvelle étape en découvrant l'API `getDisplayMedia()`, qui permet de capturer le contenu d'un écran, d'une fenêtre spécifique ou d'un onglet de navigateur.

Le partage d'écran est un défi technique intéressant, car il implique la capture de *ce que l'utilisateur voit* plutôt que de ce qu'il est. Cette API a été conçue pour offrir un contrôle granulaire à l'utilisateur sur ce qu'il souhaite partager, tout en garantissant sa vie privée et sa sécurité.

## Qu'est-ce que `getDisplayMedia` ?

`getDisplayMedia()` est une méthode de l'interface `MediaDevices` (accessible via `navigator.mediaDevices`) qui permet à une application web de demander à l'utilisateur de partager une partie de son écran. Contrairement à `getUserMedia()`, qui est principalement destinée à la capture de médias *personnels* (caméra, micro), `getDisplayMedia()` se concentre sur la capture de *contenu visuel* affiché à l'écran.

Lorsque cette méthode est appelée, le navigateur affiche une boîte de dialogue à l'utilisateur, lui proposant de choisir ce qu'il souhaite partager :
*   **L'intégralité de son écran** (si l'utilisateur a plusieurs écrans, il pourra choisir lequel).
*   **Une fenêtre d'application spécifique** (par exemple, un éditeur de texte, une présentation PowerPoint).
*   **Un onglet du navigateur actuel** (utile pour des démos d'autres sites web ou d'applications web).

Une fois le choix fait et la permission accordée, `getDisplayMedia()` retourne une `Promise` qui, en cas de succès, se résout avec un objet `MediaStream`. Cet objet `MediaStream` contient la piste vidéo du contenu partagé (et potentiellement une piste audio si l'utilisateur a choisi de partager l'audio système).

## Cas d'Usage et Avantages

L'intégration du partage d'écran ouvre la porte à une multitude de fonctionnalités puissantes :

*   **Vidéoconférences enrichies** : Permet aux participants de partager des présentations, des documents ou des démonstrations logicielles.
*   **Support technique à distance** : Un agent peut guider un utilisateur en voyant directement son écran.
*   **Collaboration en temps réel** : Les équipes peuvent travailler ensemble sur des documents ou du code en partageant leurs écrans.
*   **Streaming de jeux ou de contenu éducatif** : Les créateurs de contenu peuvent diffuser leur écran de jeu ou leurs tutoriels.
*   **Prototypage et tests d'utilisabilité** : Recueillir des retours en temps réel en observant comment les utilisateurs interagissent avec une interface.

Les principaux avantages de `getDisplayMedia()` sont :
*   **Simplicité d'utilisation** : L'API est intuitive et s'intègre bien avec le modèle `MediaStream` existant.
*   **Contrôle utilisateur renforcé** : L'utilisateur conserve le contrôle total sur ce qu'il partage, avec une interface native du navigateur.
*   **Sécurité et Confidentialité** : Le partage est explicitement consenti et visible par l'utilisateur (par exemple, une bordure bleue autour de l'écran partagé).
*   **Prise en charge de l'audio** : Possibilité de capturer l'audio du système, idéal pour partager des vidéos ou des applications avec du son.

## Comprendre l'API `getDisplayMedia`

L'utilisation de `getDisplayMedia()` est similaire à celle de `getUserMedia()`, mais avec des spécificités liées à la nature de la capture.

### La Promesse du Partage

La méthode `getDisplayMedia()` retourne une `Promise` qui doit être gérée :

```javascript
navigator.mediaDevices.getDisplayMedia(constraints)
  .then(stream => {
    // Le partage a réussi, 'stream' contient la piste vidéo (et potentiellement audio)
    // On peut maintenant utiliser ce stream, par exemple l'afficher dans une balise <video>
  })
  .catch(error => {
    // Une erreur est survenue (utilisateur a annulé, permission refusée, etc.)
    console.error('Erreur lors du partage d\'écran :', error);
  });

Les Contraintes (Constraints)

Comme pour getUserMedia(), l'API getDisplayMedia() accepte un objet constraints optionnel. Cet objet permet de spécifier les types de médias que l'on souhaite obtenir. Cependant, les contraintes pour getDisplayMedia() sont plus limitées et se concentrent sur la vidéo et l'audio système.

Contraintes Vidéo

La contrainte la plus courante est video: true pour demander explicitement une piste vidéo. Vous pouvez également spécifier des résolutions préférées, bien que le navigateur puisse les ignorer si l'utilisateur choisit une surface de partage plus petite.

const constraints = {
  video: {
    cursor: "always" || "never" || "motion", // Comment afficher le curseur de la souris
    displaySurface: "monitor" || "window" || "browser", // Suggérer le type de surface à partager
  },
  audio: true // Demander la capture audio système
};

Propriétés spécifiques aux contraintes vidéo pour getDisplayMedia :

  • cursor : Indique si le curseur de la souris doit être inclus dans la capture vidéo.
    • "always" : Le curseur est toujours visible.
    • "never" : Le curseur n'est jamais visible.
    • "motion" (par défaut) : Le curseur est visible uniquement lorsqu'il est en mouvement.
  • displaySurface : Suggère au navigateur le type de surface de display à privilégier dans la boîte de dialogue de sélection. Ce n'est qu'une suggestion, l'utilisateur a le dernier mot.
    • "monitor" : Suggère de partager un écran complet.
    • "window" : Suggère de partager une fenêtre d'application.
    • "browser" : Suggère de partager un onglet du navigateur.
  • logicalSurface : Un booléen qui indique si la capture doit inclure des "surfaces logiques" (comme les zones hors écran des onglets). Par défaut false.
  • preferCurrentTab : (Chrome-specific) Un booléen qui, si true, fait en sorte que le navigateur mette en évidence l'onglet courant comme option de partage par défaut.
  • selfBrowserSurface : (Chrome-specific) Un booléen qui indique si le navigateur doit afficher l'option de partager l'onglet courant (celui qui appelle getDisplayMedia). Utile pour éviter l'effet "miroir infini" si l'utilisateur partage l'onglet lui-même.

Contraintes Audio

Pour inclure l'audio du système (par exemple, le son d'une vidéo YouTube jouée dans l'onglet partagé ou le son d'une application), vous devez définir audio: true.

const constraints = {
  video: true,
  audio: true // Capture l'audio du système
};

Important : La capture audio du système est soumise à des restrictions de sécurité et de navigateur. Tous les navigateurs ne la supportent pas de la même manière, et l'utilisateur doit généralement donner une permission explicite pour cela. Sur certains systèmes d'exploitation, l'audio système ne peut être capturé que si l'on partage un onglet spécifique.

L'Objet MediaStream

Lorsque getDisplayMedia() se résout avec succès, elle renvoie un objet MediaStream. Cet objet est le même type d'objet que celui retourné par getUserMedia(). Il contient :

  • Une piste vidéo (VideoTrack) représentant le contenu de l'écran/fenêtre/onglet partagé.
  • Optionnellement, une piste audio (AudioTrack) si l'audio système a été demandé et accordé.

Vous pouvez interagir avec ce MediaStream de la même manière que vous le feriez avec un stream de caméra/micro :

  • L'assigner à l'attribut srcObject d'une balise <video> pour l'afficher localement.
  • L'ajouter à un RTCPeerConnection pour l'envoyer à un pair distant.
  • Ajouter des gestionnaires d'événements pour ended sur les pistes afin de détecter quand le partage est arrêté par l'utilisateur.

Gestion des Erreurs

Les erreurs les plus courantes lors de l'appel à getDisplayMedia() sont :

  • NotAllowedError : L'utilisateur a refusé la permission de partager l'écran ou a annulé la boîte de dialogue de sélection.
  • NotFoundError : Aucune source d'affichage disponible (extrêmement rare pour le partage d'écran).
  • NotReadableError : Le système d'exploitation n'a pas pu accéder à la source d'affichage.
  • AbortError : L'opération a été avortée pour une raison inconnue.

Il est crucial de gérer ces erreurs pour fournir une bonne expérience utilisateur.

Mise en Pratique : Partager son Écran

Nous allons créer un exemple simple où l'utilisateur peut démarrer et arrêter le partage de son écran, et visualiser le flux partagé dans un élément <video>.

La Structure HTML

Commençons par une structure HTML minimale avec une balise <video> pour afficher le flux et des boutons pour contrôler le partage.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Partage d'Écran avec getDisplayMedia</title>
    <style>
        body {
            font-family: sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-top: 50px;
            background-color: #f0f2f5;
        }
        .container {
            background-color: #ffffff;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            text-align: center;
        }
        video {
            width: 80vw; /* Utiliser 80% de la largeur de la fenêtre */
            max-width: 800px; /* Limiter la largeur maximale */
            height: auto;
            border: 2px solid #ccc;
            border-radius: 8px;
            margin-top: 20px;
            background-color: #000; /* Fond noir pour la vidéo */
        }
        button {
            padding: 12px 25px;
            margin: 10px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 1rem;
            transition: background-color 0.3s ease;
        }
        #startButton {
            background-color: #28a745;
            color: white;
        }
        #startButton:hover {
            background-color: #218838;
        }
        #stopButton {
            background-color: #dc3545;
            color: white;
        }
        #stopButton:hover {
            background-color: #c82333;
        }
        #statusMessage {
            margin-top: 15px;
            color: #555;
            font-size: 0.9em;
        }
        .controls {
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Démo de Partage d'Écran</h1>
        <p>Cliquez sur "Démarrer le partage" pour sélectionner ce que vous souhaitez partager.</p>
        <div class="controls">
            <button id="startButton">Démarrer le partage</button>
            <button id="stopButton" disabled>Arrêter le partage</button>
        </div>
        <p id="statusMessage">Aucun partage en cours.</p>
        <video id="sharedScreenVideo" autoplay playsinline controls></video>
    </div>

    <script src="script.js"></script>
</body>
</html>

Le Code JavaScript

Maintenant, écrivons le code JavaScript (script.js) pour gérer le partage d'écran.

// script.js

let currentStream; // Variable pour stocker le MediaStream actuel
const videoElement = document.getElementById('sharedScreenVideo');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const statusMessage = document.getElementById('statusMessage');

// Initialiser l'état des boutons
stopButton.disabled = true;

/**
 * Démarre le partage d'écran en utilisant getDisplayMedia.
 */
async function startScreenShare() {
    statusMessage.textContent = 'Demande de permission de partage d\'écran...';
    startButton.disabled = true; // Désactiver le bouton de démarrage pendant la demande

    try {
        // Demander le stream de l'écran avec la vidéo et l'audio système (si disponible)
        // Les contraintes peuvent être ajustées : { video: true, audio: true }
        // Ou plus spécifiques, par exemple pour le curseur :
        // { video: { cursor: "always" }, audio: true }
        const constraints = {
            video: true,
            audio: true // Tente de capturer l'audio système
        };

        currentStream = await navigator.mediaDevices.getDisplayMedia(constraints);

        // Afficher le stream dans l'élément vidéo
        videoElement.srcObject = currentStream;

        // Gérer l'événement lorsque l'utilisateur arrête le partage via le navigateur
        currentStream.getVideoTracks()[0].onended = () => {
            console.log('Le partage d\'écran a été arrêté par l\'utilisateur.');
            stopScreenShare(); // Appeler notre fonction d'arrêt
        };

        statusMessage.textContent = 'Partage d\'écran en cours.';
        stopButton.disabled = false; // Activer le bouton d'arrêt

    } catch (error) {
        console.error('Erreur lors du partage d\'écran :', error);
        if (error.name === 'NotAllowedError') {
            statusMessage.textContent = 'Partage d\'écran refusé par l\'utilisateur ou annulé.';
        } else {
            statusMessage.textContent = `Erreur : ${error.message}`;
        }
        // Réinitialiser le stream et l'état des boutons en cas d'erreur
        videoElement.srcObject = null;
        currentStream = null;
    } finally {
        startButton.disabled = false; // Réactiver le bouton de démarrage
    }
}

/**
 * Arrête le partage d'écran en arrêtant toutes les pistes du stream.
 */
function stopScreenShare() {
    if (currentStream) {
        currentStream.getTracks().forEach(track => {
            track.stop(); // Arrêter chaque piste (vidéo, audio)
        });
        currentStream = null; // Réinitialiser la variable du stream
        videoElement.srcObject = null; // Vider l'élément vidéo
        statusMessage.textContent = 'Partage d\'écran arrêté.';
        stopButton.disabled = true; // Désactiver le bouton d'arrêt
    }
}

// Ajouter les écouteurs d'événements aux boutons
startButton.addEventListener('click', startScreenShare);
stopButton.addEventListener('click', stopScreenShare);

// Optionnel: Gérer la déconnexion si l'utilisateur quitte la page
window.addEventListener('beforeunload', () => {
    stopScreenShare();
});

Explication du code :

  1. Variables Globales :

    • currentStream: Une variable pour stocker le MediaStream obtenu. C'est important car nous aurons besoin d'y accéder pour arrêter le partage.
    • Références aux éléments HTML (videoElement, startButton, stopButton, statusMessage).
  2. startScreenShare() (Asynchrone) :

    • Affiche un message de statut et désactive le bouton startButton pour éviter les clics multiples.
    • Utilise un bloc try...catch pour gérer les promesses et les erreurs.
    • Appelle navigator.mediaDevices.getDisplayMedia({ video: true, audio: true }).
      • video: true : Indique que nous voulons une piste vidéo (essentiel pour le partage d'écran).
      • audio: true : Tente de capturer l'audio du système. N'oubliez pas que cela dépend du navigateur et de la sélection de l'utilisateur.
    • Si la promesse est résolue (.then ou await), le MediaStream est assigné à currentStream.
    • videoElement.srcObject = currentStream; : Le stream est directement lié à l'élément <video> pour être affiché.
    • Gestion de l'arrêt par l'utilisateur : currentStream.getVideoTracks()[0].onended est un événement crucial. Si l'utilisateur arrête le partage directement via la barre d'outils du navigateur (par exemple, en cliquant sur le bouton "Arrêter le partage"), cet événement se déclenche. Nous utilisons cet événement pour appeler notre propre fonction stopScreenShare() afin de synchroniser l'état de notre application.
    • Met à jour le message de statut et active le bouton stopButton.
    • En cas d'erreur (.catch), un message d'erreur est affiché, et l'état des boutons est réinitialisé.
  3. stopScreenShare() :

    • Vérifie si un currentStream existe.
    • currentStream.getTracks().forEach(track => track.stop()); : Itère sur toutes les pistes (vidéo et audio) du stream et appelle track.stop(). C'est la méthode standard pour libérer les ressources matérielles et arrêter la capture.
    • Réinitialise currentStream à null et videoElement.srcObject à null pour vider l'affichage.
    • Met à jour le message de statut et désactive le bouton stopButton.
  4. Écouteurs d'événements :

    • Les événements click sont ajoutés aux boutons startButton et stopButton pour appeler les fonctions correspondantes.
    • Un écouteur beforeunload est ajouté au window pour s'assurer que le partage est arrêté si l'utilisateur ferme ou quitte la page, libérant ainsi les ressources.

Considérations Avancées et Bonnes Pratiques

Sécurité et Confidentialité

  • Consentement Utilisateur : getDisplayMedia() nécessite toujours le consentement explicite de l'utilisateur via une boîte de dialogue native du navigateur. Votre application ne peut pas forcer le partage.
  • Indicateur Visuel : La plupart des navigateurs affichent un indicateur visuel clair (par exemple, une bordure colorée autour de l'écran partagé) pour informer l'utilisateur que son écran est en cours de capture.
  • Permissions Contextuelles : L'API est conçue pour être utilisée dans un contexte sécurisé (HTTPS).

Performance

Le partage d'écran est une opération gourmande en ressources, surtout si l'on capture un écran entier en haute résolution.

  • Taux de rafraîchissement : Le navigateur peut ajuster le taux de rafraîchissement du flux capturé pour optimiser les performances, surtout si la bande passante est limitée.
  • Compression : Lorsque le stream est envoyé via WebRTC, il est encodé et compressé, ce qui aide à réduire l'utilisation de la bande passante.
  • Optimisations spécifiques : Les navigateurs tentent d'optimiser la capture en ne transmettant que les zones de l'écran qui ont changé (détection de zones sales).

Partage de l'Audio Système

Comme mentionné, la capacité à capturer l'audio du système est très utile mais soumise à des limitations :

  • Navigateur/OS : La disponibilité et la mise en œuvre varient entre les navigateurs et les systèmes d'exploitation. Chrome supporte bien la capture audio pour les onglets et parfois les systèmes. Firefox a une implémentation légèrement différente.
  • Consentement explicite : L'utilisateur doit souvent cocher une case spécifique dans la boîte de dialogue de partage pour inclure l'audio.

Effet "Miroir Infini" (Self-capture)

Si l'utilisateur choisit de partager l'onglet où votre application WebRTC est en cours d'exécution, et que votre application affiche ce stream dans un <video> élément, cela peut créer un effet "miroir infini" (ou "hall of mirrors"). C'est une expérience étrange mais inoffensive. Les navigateurs modernes offrent des options comme selfBrowserSurface: "exclude" dans les contraintes pour suggérer de ne pas offrir l'onglet courant comme option de partage, mais l'utilisateur a toujours le dernier mot.

Conclusion

L'API getDisplayMedia() est un ajout puissant à la boîte à outils WebRTC, permettant de créer des applications web interactives et collaboratives avec des capacités de partage d'écran. En comprenant son fonctionnement, ses contraintes et en gérant correctement les flux et les erreurs, vous pouvez implémenter cette fonctionnalité de manière robuste et conviviale.

N'oubliez pas l'importance du consentement et de la sécurité. Le partage d'écran expose le contenu de l'utilisateur, il est donc essentiel que l'interface soit claire et que l'utilisateur ait toujours le contrôle total sur ce qu'il partage et quand il l'arrête.

Dans la prochaine leçon, nous verrons comment intégrer ce MediaStream de partage d'écran dans une RTCPeerConnection pour l'envoyer à un pair distant, permettant ainsi de construire des applications de visioconférence complètes avec cette fonctionnalité clé.