# 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éfautfalse.preferCurrentTab: (Chrome-specific) Un booléen qui, sitrue, 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 appellegetDisplayMedia). 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
srcObjectd'une balise<video>pour l'afficher localement. - L'ajouter à un
RTCPeerConnectionpour l'envoyer à un pair distant. - Ajouter des gestionnaires d'événements pour
endedsur 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 :
-
Variables Globales :
currentStream: Une variable pour stocker leMediaStreamobtenu. 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).
-
startScreenShare()(Asynchrone) :- Affiche un message de statut et désactive le bouton
startButtonpour éviter les clics multiples. - Utilise un bloc
try...catchpour 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 (
.thenouawait), leMediaStreamest 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].onendedest 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 fonctionstopScreenShare()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é.
- Affiche un message de statut et désactive le bouton
-
stopScreenShare():- Vérifie si un
currentStreamexiste. currentStream.getTracks().forEach(track => track.stop());: Itère sur toutes les pistes (vidéo et audio) du stream et appelletrack.stop(). C'est la méthode standard pour libérer les ressources matérielles et arrêter la capture.- Réinitialise
currentStreamànulletvideoElement.srcObjectànullpour vider l'affichage. - Met à jour le message de statut et désactive le bouton
stopButton.
- Vérifie si un
-
Écouteurs d'événements :
- Les événements
clicksont ajoutés aux boutonsstartButtonetstopButtonpour appeler les fonctions correspondantes. - Un écouteur
beforeunloadest ajouté auwindowpour s'assurer que le partage est arrêté si l'utilisateur ferme ou quitte la page, libérant ainsi les ressources.
- Les événements
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é.