Utiliser les Canaux de Données (Data Channels) pour le Partage de Fichiers et de Messages
Bienvenue dans cette leçon dédiée aux Data Channels de WebRTC, un composant puissant souvent éclipsé par les fonctionnalités audio et vidéo. Alors que WebRTC est célèbre pour les communications multimédias en temps réel, ses Data Channels ouvrent la voie à des interactions peer-to-peer riches et variées, permettant le transfert de n'importe quel type de données.
Introduction : Au-delà de l'Audio et de la Vidéo
Traditionnellement, WebRTC est associé aux appels vidéo et vocaux. Cependant, sa véritable force réside dans sa capacité à établir des connexions directes peer-to-peer (P2P) entre navigateurs, éliminant ainsi le besoin de serveurs intermédiaires pour le flux de données. Les Data Channels exploitent cette même architecture P2P pour transférer des données arbitraires, qu'il s'agisse de messages textuels, de fichiers binaires, d'états de jeu, ou de toute autre information structurée ou non.
Imaginez pouvoir partager un fichier de manière sécurisée et directe avec un ami, sans passer par un service de stockage cloud. Ou encore, créer un tableau blanc collaboratif en temps réel, ou un jeu multijoueur où les interactions sont instantanées. C'est précisément ce que les Data Channels rendent possible.
Pourquoi les Data Channels sont-ils cruciaux ?
- P2P Direct : Évite les serveurs relai pour le transfert de données, réduisant la latence et la charge serveur.
- Sécurité Intégrée : Toutes les communications via WebRTC, y compris les Data Channels, sont chiffrées par défaut grâce à DTLS (Datagram Transport Layer Security).
- Flexibilité : Peut transporter tout type de données (texte, binaire).
- Contrôle Fin : Permet de configurer la fiabilité (garantie de livraison) et l'ordonnancement des messages.
- Intégration WebRTC : S'intègrent parfaitement dans le même
RTCPeerConnectionque vos flux audio/vidéo.
Prérequis
Pour tirer le meilleur parti de cette leçon, une compréhension de base de WebRTC est recommandée, notamment des concepts suivants :
RTCPeerConnection: L'API principale pour établir une connexion P2P.- Signalisation : Le processus d'échange d'informations (SDP et ICE candidates) entre les pairs pour établir la connexion. Bien que nous simplifions la signalisation pour l'exemple, il est crucial de comprendre son rôle.
- JavaScript et HTML : Les exemples de code seront basés sur ces technologies.
Les Canaux de Données (Data Channels) : Une Vue d'Ensemble Approfondie
Un RTCDataChannel peut être vu comme un tube bidirectionnel sécurisé entre deux pairs connectés via WebRTC. Il est construit sur SCTP (Stream Control Transmission Protocol) qui s'exécute sur DTLS (Datagram Transport Layer Security), lui-même encapsulé dans UDP.
Caractéristiques Clés
Les RTCDataChannel offrent une flexibilité impressionnante grâce à leurs options de configuration :
-
Fiabilité (
ordered,maxRetransmits,maxPacketLifeTime)- Fiable (Reliable) : Par défaut (
ordered: true,maxRetransmits: null,maxPacketLifeTime: null). Garantit que chaque message est livré et dans l'ordre où il a été envoyé. Idéal pour le transfert de fichiers ou des messages critiques. Similaire à TCP. - Non Fiable (Unreliable) : Les messages peuvent être perdus ou arriver dans le désordre. Idéal pour des données de jeu en temps réel, la synchronisation de curseurs, ou toute donnée où la fraîcheur est plus importante que la garantie de livraison. Similaire à UDP.
ordered: false: Les messages peuvent arriver dans n'importe quel ordre.maxRetransmits: Nombre maximal de tentatives de retransmission avant d'abandonner un message.maxPacketLifeTime: Durée maximale (en ms) pendant laquelle les messages peuvent être retransmis. Après ce délai, ils sont abandonnés.
- Fiable (Reliable) : Par défaut (
-
Ordre (
ordered)true(par défaut) : Les messages sont livrés dans l'ordre où ils ont été envoyés.false: Les messages peuvent arriver dans n'importe quel ordre.
-
Identifiant (
id)- Un identifiant numérique unique pour le canal au sein de la
RTCPeerConnection.
- Un identifiant numérique unique pour le canal au sein de la
-
Libellé (
label)- Une chaîne de caractères descriptive pour identifier le canal. Utile lorsqu'une
RTCPeerConnectiona plusieurs canaux de données.
- Une chaîne de caractères descriptive pour identifier le canal. Utile lorsqu'une
-
Protocol (
protocol)- Une chaîne de caractères facultative permettant de spécifier un sous-protocole, similaire à celui de WebSocket.
Cycle de Vie d'un Data Channel
Un RTCDataChannel passe par plusieurs états au cours de sa vie :
connecting: Le canal est en cours d'établissement.open: Le canal est prêt à envoyer et recevoir des données.closing: Le canal est en cours de fermeture.closed: Le canal est fermé et ne peut plus être utilisé.
Vous pouvez écouter les événements onopen, onclose, et onerror pour gérer ces états.
Création et Gestion des Canaux de Données
La création d'un RTCDataChannel est asymétrique : un pair le crée, l'autre pair le reçoit.
Côté Créateur (Offerer)
Le pair qui initie la connexion (createOffer) est généralement celui qui crée le RTCDataChannel en utilisant la méthode createDataChannel() sur son RTCPeerConnection.
// Supposons que 'peerConnection' est votre objet RTCPeerConnection
const dataChannel = peerConnection.createDataChannel("file-transfer-channel", {
ordered: true, // Les messages seront livrés dans l'ordre
maxRetransmits: 0 // Aucune retransmission (pour des raisons de performance ou si l'ordre n'est pas critique)
});
dataChannel.onopen = (event) => {
console.log("Data Channel ouvert !");
// Le canal est prêt à envoyer des données
};
dataChannel.onmessage = (event) => {
console.log("Message reçu :", event.data);
};
dataChannel.onclose = (event) => {
console.log("Data Channel fermé.");
};
dataChannel.onerror = (error) => {
console.error("Erreur Data Channel :", error);
};
// N'oubliez pas d'ajouter les gestionnaires de glace et de description SDP
// ... et d'envoyer l'offre SDP au pair distant
Côté Récepteur (Answerer)
Le pair qui reçoit l'offre SDP et y répond (createAnswer) ne crée pas le canal directement. Au lieu de cela, il écoute l'événement ondatachannel sur son RTCPeerConnection.
// Supposons que 'peerConnection' est votre objet RTCPeerConnection
peerConnection.ondatachannel = (event) => {
const dataChannel = event.channel;
console.log(`Nouveau Data Channel reçu : ${dataChannel.label}`);
dataChannel.onopen = (event) => {
console.log("Data Channel ouvert côté récepteur !");
};
dataChannel.onmessage = (event) => {
console.log("Message reçu sur le canal reçu :", event.data);
};
dataChannel.onclose = (event) => {
console.log("Data Channel fermé côté récepteur.");
};
dataChannel.onerror = (error) => {
console.error("Erreur Data Channel côté récepteur :", error);
};
// Vous pouvez stocker ce dataChannel si vous voulez l'utiliser plus tard
// globalDataChannel = dataChannel;
};
// N'oubliez pas d'ajouter les gestionnaires de glace et de description SDP
// ... et de répondre avec l'answer SDP au pair distant
Une fois que les deux côtés ont leur référence au RTCDataChannel et que son état est open, ils peuvent commencer à communiquer.
Partage de Messages Textuels
Envoyer et recevoir des messages textuels est la forme la plus simple d'utilisation des Data Channels.
Envoyer un message
La méthode send() du RTCDataChannel est utilisée pour envoyer des données. Elle peut prendre des chaînes de caractères (String), des Blob, des ArrayBuffer ou des ArrayBufferView (comme Uint8Array).
if (dataChannel.readyState === 'open') {
dataChannel.send("Bonjour, ceci est un message WebRTC !");
} else {
console.warn("Impossible d'envoyer le message, Data Channel non ouvert.");
}
Recevoir un message
L'événement onmessage est déclenché chaque fois qu'un message est reçu sur le canal. La donnée est accessible via event.data.
dataChannel.onmessage = (event) => {
console.log("Message reçu :", event.data);
// Afficher le message dans l'interface utilisateur
};
Partage de Fichiers (Avancé)
Le partage de fichiers via Data Channels est un cas d'utilisation puissant mais nécessite une gestion plus sophistiquée, principalement en raison de la taille potentielle des fichiers. Les Data Channels n'ont pas de limite stricte sur la taille totale d'un fichier, mais ils ont des limites sur la taille des messages individuels qu'ils peuvent envoyer (typiquement autour de 64KB, mais cela peut varier selon les implémentations de navigateur). Par conséquent, les fichiers volumineux doivent être divisés en "morceaux" (chunks) et envoyés séquentiellement.
Défis du Partage de Fichiers
- Découpage (Chunking) : Diviser le fichier source en petits morceaux.
- Transmission : Envoyer chaque morceau via
dataChannel.send(). - Réassemblage : Côté récepteur, collecter tous les morceaux et les assembler dans l'ordre correct.
- Gestion des Métadonnées : Envoyer des informations sur le fichier (nom, type, taille) avant les morceaux pour que le récepteur sache à quoi s'attendre.
- Suivi de Progression : Fournir un retour visuel à l'utilisateur sur l'avancement du transfert.
Approche pour le Transfert de Fichiers
- Initiation : L'expéditeur envoie un message JSON contenant les métadonnées du fichier (nom, taille, type MIME).
- Préparation du Fichier : L'expéditeur lit le fichier (généralement via l'API
FileReader) enArrayBuffer. - Boucle de Découpage : L'expéditeur itère sur l'
ArrayBuffer, extrait desBlobouArrayBufferViewde tailleCHUNK_SIZEet les envoie. - Réception des Morceaux : Le récepteur écoute les messages. Si le premier message est des métadonnées, il initialise le processus de réception. Les messages suivants sont des morceaux de données.
- Réassemblage : Les morceaux sont stockés (par exemple, dans un tableau d'ArrayBuffers ou Blobs) dans l'ordre.
- Reconstruction : Une fois tous les morceaux reçus, ils sont combinés en un seul
Blob, puis un URL d'objet est créé pour permettre le téléchargement du fichier.
Mise en Œuvre Pratique : Chat et Partage de Fichiers
Nous allons créer un exemple minimaliste avec un HTML et un JavaScript pour illustrer ces concepts. La signalisation sera simulée/simplifiée pour se concentrer sur les Data Channels. Dans une application réelle, vous utiliseriez un serveur de signalisation (WebSocket, etc.).
Structure HTML (index.html)
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebRTC Data Channel: Chat & Fichier</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; }
.container { display: flex; gap: 20px; }
.peer-box {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
flex: 1;
}
h2 { color: #333; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
textarea { width: calc(100% - 22px); height: 100px; padding: 10px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 4px; resize: vertical; }
input[type="text"], input[type="file"], button {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
button { background-color: #007bff; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
button:hover { background-color: #0056b3; }
#messagesA, #messagesB {
border: 1px solid #eee;
padding: 10px;
height: 200px;
overflow-y: scroll;
margin-top: 10px;
background-color: #f9f9f9;
}
.progress-bar {
width: 100%;
background-color: #e0e0e0;
border-radius: 5px;
margin-top: 5px;
height: 20px;
overflow: hidden;
}
.progress {
height: 100%;
background-color: #4CAF50;
width: 0%;
text-align: center;
line-height: 20px;
color: white;
font-size: 0.8em;
transition: width 0.3s ease-in-out;
}
.status-message { font-style: italic; color: #666; margin-bottom: 10px; }
.message-row { margin-bottom: 5px; }
.message-row strong { color: #333; }
.message-row span { color: #555; }
</style>
</head>
<body>
<h1>WebRTC Data Channel: Chat et Partage de Fichiers</h1>
<p class="status-message">Ouvrez cette page dans deux onglets différents pour simuler deux pairs.</p>
<div class="container">
<div class="peer-box">
<h2>Pair A (Offerer)</h2>
<button id="startButtonA">Démarrer Connexion A</button>
<p>Statut de la connexion : <span id="connectionStatusA">Déconnecté</span></p>
<h3>Chat</h3>
<div id="messagesA"></div>
<textarea id="messageInputA" placeholder="Votre message..."></textarea>
<button id="sendMessageButtonA">Envoyer Message</button>
<h3>Partage de Fichiers</h3>
<input type="file" id="fileInputA">
<button id="sendFileButtonA">Envoyer Fichier</button>
<div class="progress-bar"><div class="progress" id="progressBarA">0%</div></div>
</div>
<div class="peer-box">
<h2>Pair B (Answerer)</h2>
<button id="startButtonB">Démarrer Connexion B</button>
<p>Statut de la connexion : <span id="connectionStatusB">Déconnecté</span></p>
<h3>Chat</h3>
<div id="messagesB"></div>
<textarea id="messageInputB" placeholder="Votre message..."></textarea>
<button id="sendMessageButtonB">Envoyer Message</button>
<h3>Partage de Fichiers</h3>
<input type="file" id="fileInputB" disabled>
<button id="sendFileButtonB" disabled>Envoyer Fichier</button>
<div class="progress-bar"><div class="progress" id="progressBarB">0%</div></div>
<p>Fichier reçu : <a id="downloadLinkB" style="display: none;">Télécharger</a></p>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
Logique JavaScript (script.js)
// Définition des variables globales pour les deux pairs
let pcA, pcB;
let dcA, dcB; // Data Channels
let fileReaderA, fileReaderB; // Pour la lecture des fichiers
// Taille des morceaux pour le transfert de fichiers (en octets)
const CHUNK_SIZE = 16 * 1024; // 16KB
// Variables pour le transfert de fichiers (côté récepteur)
let receivedBuffersB = [];
let receivedFileSizeB = 0;
let expectedFileSizeB = 0;
let fileNameB = '';
let fileTypeB = '';
let receivedBuffersA = [];
let receivedFileSizeA = 0;
let expectedFileSizeA = 0;
let fileNameA = '';
let fileTypeA = '';
// --- Éléments du DOM pour le Pair A ---
const startButtonA = document.getElementById('startButtonA');
const connectionStatusA = document.getElementById('connectionStatusA');
const messagesA = document.getElementById('messagesA');
const messageInputA = document.getElementById('messageInputA');
const sendMessageButtonA = document.getElementById('sendMessageButtonA');
const fileInputA = document.getElementById('fileInputA');
const sendFileButtonA = document.getElementById('sendFileButtonA');
const progressBarA = document.getElementById('progressBarA');
// --- Éléments du DOM pour le Pair B ---
const startButtonB = document.getElementById('startButtonB');
const connectionStatusB = document.getElementById('connectionStatusB');
const messagesB = document.getElementById('messagesB');
const messageInputB = document.getElementById('messageInputB');
const sendMessageButtonB = document.getElementById('sendMessageButtonB');
const fileInputB = document.getElementById('fileInputB');
const sendFileButtonB = document.getElementById('sendFileButtonB');
const progressBarB = document.getElementById('progressBarB');
const downloadLinkB = document.getElementById('downloadLinkB');
// --- Gestionnaires d'événements pour les boutons de démarrage ---
startButtonA.onclick = startPeerA;
startButtonB.onclick = startPeerB;
// --- Gestionnaires d'événements pour les boutons d'envoi de message ---
sendMessageButtonA.onclick = () => sendMessage(dcA, messageInputA, messagesA, 'Pair A');
sendMessageButtonB.onclick = () => sendMessage(dcB, messageInputB, messagesB, 'Pair B');
// --- Gestionnaires d'événements pour l'envoi de fichiers ---
sendFileButtonA.onclick = () => sendFile(dcA, fileInputA, progressBarA);
// Côté B, le bouton d'envoi de fichier est désactivé par défaut.
// Pour simuler un échange bidirectionnel, il faudrait que les deux pairs puissent initier des envois.
// Pour cette démo, on se concentre sur A -> B pour le fichier.
// --- Fonctions d'initialisation des Pairs ---
async function startPeerA() {
startButtonA.disabled = true;
connectionStatusA.textContent = 'Initialisation...';
// Configuration ICE (servers STUN/TURN)
const servers = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
// Création de RTCPeerConnection pour le Pair A
pcA = new RTCPeerConnection(servers);
pcA.onicecandidate = e => {
if (e.candidate) {
// Dans un vrai scénario, envoyez e.candidate au Pair B via le serveur de signalisation
console.log('ICE Candidate A:', e.candidate);
if (pcB) pcB.addIceCandidate(e.candidate); // Simulation de signalisation
}
};
pcA.onconnectionstatechange = () => {
connectionStatusA.textContent = pcA.connectionState;
if (pcA.connectionState === 'connected') {
enableChatAndFileTransfer(messageInputA, sendMessageButtonA, fileInputA, sendFileButtonA);
} else {
disableChatAndFileTransfer(messageInputA, sendMessageButtonA, fileInputA, sendFileButtonA);
}
};
// Création du Data Channel côté Pair A (l'Offerer)
dcA = pcA.createDataChannel('chat-file-channel', { ordered: true, maxRetransmits: 0 }); // Canal fiable
setupDataChannelEvents(dcA, 'A', messagesA, progressBarA, downloadLinkB); // dcA est l'expéditeur pour le fichier A->B, messagesA est l'UI du chat A, progressBarA est l'expéditeur de la barre. downloadLinkB est pour le download du fichier reçu par B.
// Création de l'offre SDP
const offer = await pcA.createOffer();
await pcA.setLocalDescription(offer);
// Dans un vrai scénario, envoyez l'offre SDP au Pair B via le serveur de signalisation
console.log('SDP Offer A:', offer.sdp);
// Simulation de signalisation: pcB reçoit l'offre et la traite
if (pcB) {
await pcB.setRemoteDescription(offer);
const answer = await pcB.createAnswer();
await pcB.setLocalDescription(answer);
console.log('SDP Answer B:', answer.sdp);
await pcA.setRemoteDescription(answer); // pcA reçoit la réponse de pcB
}
}
async function startPeerB() {
startButtonB.disabled = true;
connectionStatusB.textContent = 'Initialisation...';
const servers = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] };
pcB = new RTCPeerConnection(servers);
pcB.onicecandidate = e => {
if (e.candidate) {
// Dans un vrai scénario, envoyez e.candidate au Pair A via le serveur de signalisation
console.log('ICE Candidate B:', e.candidate);
if (pcA) pcA.addIceCandidate(e.candidate); // Simulation de signalisation
}
};
pcB.onconnectionstatechange = () => {
connectionStatusB.textContent = pcB.connectionState;
if (pcB.connectionState === 'connected') {
enableChatAndFileTransfer(messageInputB, sendMessageButtonB, fileInputB, sendFileButtonB);
} else {
disableChatAndFileTransfer(messageInputB, sendMessageButtonB, fileInputB, sendFileButtonB);
}
};
// Le Pair B (Answerer) écoute l'événement 'ondatachannel'
pcB.ondatachannel = event => {
dcB = event.channel;
console.log(`Data Channel '${dcB.label}' reçu par le Pair B.`);
setupDataChannelEvents(dcB, 'B', messagesB, progressBarB, downloadLinkB);
};
// Si le Pair A n'a pas encore démarré, attendez son offre
if (!pcA) {
console.log("En attente de l'offre du Pair A...");
} else {
// Si le Pair A a déjà démarré et envoyé son offre, le Pair B peut déjà traiter
// Cette partie serait gérée par le serveur de signalisation dans un vrai scénario.
// Ici, pcA.setLocalDescription (l'offre) est déjà fait dans startPeerA.
// On attend que pcA.setRemoteDescription (l'answer) soit fait pour pcA
// et que pcB.setRemoteDescription (l'offre) soit fait pour pcB.
// C'est pourquoi la simulation de signalisation dans startPeerA est importante.
}
}
// --- Fonctions génériques pour les Data Channels ---
function setupDataChannelEvents(dc, peerName, chatDisplay, progressBar, downloadLink) {
dc.onopen = () => {
console.log(`Data Channel ${peerName} ouvert !`);
addMessage(chatDisplay, `[Système] Data Channel ouvert pour ${peerName}.`);
if (peerName === 'A') { // A est l'initiateur
// Active les contrôles A si le canal est ouvert
enableChatAndFileTransfer(messageInputA, sendMessageButtonA, fileInputA, sendFileButtonA);
} else { // B est le récepteur
enableChatAndFileTransfer(messageInputB, sendMessageButtonB, fileInputB, sendFileButtonB);
}
};
dc.onmessage = async (event) => {
if (typeof event.data === 'string') {
try {
const message = JSON.parse(event.data);
if (message.type === 'file-metadata') {
// C'est un message de métadonnées de fichier
console.log(`Receiving file: ${message.name} (${message.size} bytes)`);
addMessage(chatDisplay, `[Fichier] Réception de "${message.name}" (${(message.size / (1024 * 1024)).toFixed(2)} Mo)...`);
if (peerName === 'B') {
expectedFileSizeB = message.size;
fileNameB = message.name;
fileTypeB = message.type;
receivedBuffersB = [];
receivedFileSizeB = 0;
updateProgressBar(progressBar, 0);
downloadLinkB.style.display = 'none'; // Cacher le lien précédent
} else { // Si B envoie à A
expectedFileSizeA = message.size;
fileNameA = message.name;
fileTypeA = message.type;
receivedBuffersA = [];
receivedFileSizeA = 0;
updateProgressBar(progressBar, 0); // La barre de A est pour l'envoi, pas la réception dans ce cas.
}
} else if (message.type === 'chat') {
addMessage(chatDisplay, `<strong>Pair ${peerName === 'A' ? 'B' : 'A'} :</strong> ${message.content}`);
}
} catch (e) {
// C'est un message textuel simple (non JSON)
addMessage(chatDisplay, `<strong>Pair ${peerName === 'A' ? 'B' : 'A'} :</strong> ${event.data}`);
}
} else if (event.data instanceof ArrayBuffer || event.data instanceof Blob) {
// C'est un morceau de fichier binaire
const buffer = event.data instanceof Blob ? await event.data.arrayBuffer() : event.data;
if (peerName === 'B') {
receivedBuffersB.push(buffer);
receivedFileSizeB += buffer.byteLength;
updateProgressBar(progressBar, (receivedFileSizeB / expectedFileSizeB) * 100);
if (receivedFileSizeB === expectedFileSizeB) {
// Fichier entièrement reçu
const receivedBlob = new Blob(receivedBuffersB, { type: fileTypeB });
const fileURL = URL.createObjectURL(receivedBlob);
downloadLink.href = fileURL;
downloadLink.download = fileNameB;
downloadLink.textContent = `Télécharger "${fileNameB}"`;
downloadLink.style.display = 'inline';
addMessage(chatDisplay, `[Fichier] "${fileNameB}" reçu entièrement !`);
receivedBuffersB = []; // Réinitialiser pour le prochain fichier
receivedFileSizeB = 0;
}
} else { // Si A reçoit un fichier de B (bidirectionnel)
receivedBuffersA.push(buffer);
receivedFileSizeA += buffer.byteLength;
updateProgressBar(progressBar, (receivedFileSizeA / expectedFileSizeA) * 100);
if (receivedFileSizeA === expectedFileSizeA) {
const receivedBlob = new Blob(receivedBuffersA, { type: fileTypeA });
const fileURL = URL.createObjectURL(receivedBlob);
// Ici, il faudrait un lien de téléchargement pour A aussi
addMessage(chatDisplay, `[Fichier] "${fileNameA}" reçu entièrement par A !`);
receivedBuffersA = [];
receivedFileSizeA = 0;
}
}
}
};
dc.onclose = () => {
console.log(`Data Channel ${peerName} fermé.`);
addMessage(chatDisplay, `[Système] Data Channel fermé pour ${peerName}.`);
if (peerName === 'A') {
disableChatAndFileTransfer(messageInputA, sendMessageButtonA, fileInputA, sendFileButtonA);
} else {
disableChatAndFileTransfer(messageInputB, sendMessageButtonB, fileInputB, sendFileButtonB);
}
};
dc.onerror = (error) => {
console.error(`Erreur Data Channel ${peerName}:`, error);
addMessage(chatDisplay, `[Erreur] Data Channel ${peerName} : ${error.message}`);
};
}
// --- Fonctions de Chat ---
function sendMessage(dataChannel, inputElement, displayElement, senderName) {
const message = inputElement.value;
if (message.trim() === '') return;
if (dataChannel && dataChannel.readyState === 'open') {
dataChannel.send(JSON.stringify({ type: 'chat', content: message }));
addMessage(displayElement, `<strong>Moi (${senderName}) :</strong> ${message}`);
inputElement.value = '';
} else {
addMessage(displayElement, `[Erreur] Data Channel non ouvert pour envoyer le message.`);
}
}
function addMessage(displayElement, messageHtml) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message-row');
messageDiv.innerHTML = messageHtml;
displayElement.appendChild(messageDiv);
displayElement.scrollTop = displayElement.scrollHeight; // Scroll vers le bas
}
// --- Fonctions de Transfert de Fichiers ---
async function sendFile(dataChannel, fileInput, progressBar) {
const file = fileInput.files[0];
if (!file) {
alert('Veuillez sélectionner un fichier à envoyer.');
return;
}
if (!dataChannel || dataChannel.readyState !== 'open') {
alert('Le Data Channel n\'est pas ouvert. Impossible d\'envoyer le fichier.');
return;
}
console.log(`Envoi du fichier: ${file.name} (${file.size} bytes)`);
// Envoyer les métadonnées du fichier
dataChannel.send(JSON.stringify({
type: 'file-metadata',
name: file.name,
size: file.size,
fileType: file.type // Utilisez file.type, pas message.type
}));
// Lire et envoyer les morceaux du fichier
let offset = 0;
fileReaderA = new FileReader(); // Utiliser fileReaderA pour le pair A
fileReaderB = new FileReader(); // Utiliser fileReaderB pour le pair B (si B devait aussi envoyer)
fileReaderA.onload = async (e) => {
dataChannel.send(e.target.result); // e.target.result est un ArrayBuffer
offset += e.target.result.byteLength;
updateProgressBar(progressBar, (offset / file.size) * 100);
if (offset < file.size) {
readNextChunk();
} else {
console.log('Fichier envoyé avec succès !');
addMessage(messagesA, `[Fichier] "${file.name}" envoyé entièrement.`);
progressBar.style.width = '0%'; // Réinitialiser la barre après envoi
progressBar.textContent = '0%';
}
};
fileReaderA.onerror = (error) => {
console.error('Erreur de lecture de fichier:', error);
addMessage(messagesA, `[Erreur] Lecture de fichier échouée.`);
};
const readNextChunk = () => {
const chunk = file.slice(offset, offset + CHUNK_SIZE);
fileReaderA.readAsArrayBuffer(chunk);
};
readNextChunk(); // Démarrer la lecture du premier morceau
}
function updateProgressBar(progressBarElement, percentage) {
const p = Math.min(100, Math.max(0, percentage)); // S'assurer que le pourcentage est entre 0 et 100
progressBarElement.style.width = p + '%';
progressBarElement.textContent = p.toFixed(0) + '%';
}
// --- Fonctions d'activation/désactivation des contrôles ---
function enableChatAndFileTransfer(messageInput, sendMessageButton, fileInput, sendFileButton) {
messageInput.disabled = false;
sendMessageButton.disabled = false;
fileInput.disabled = false;
sendFileButton.disabled = false;
}
function disableChatAndFileTransfer(messageInput, sendMessageButton, fileInput, sendFileButton) {
messageInput.disabled = true;
sendMessageButton.disabled = true;
fileInput.disabled = true;
sendFileButton.disabled = true;
}
// Désactiver les contrôles au démarrage
disableChatAndFileTransfer(messageInputA, sendMessageButtonA, fileInputA, sendFileButtonA);
disableChatAndFileTransfer(messageInputB, sendMessageButtonB, fileInputB, sendFileButtonB);
Explication du Code
-
RTCPeerConnectionet Signalisation Simplifiée :- Les fonctions
startPeerA()etstartPeerB()initialisent les objetsRTCPeerConnectionpour chaque pair. - Les
onicecandidateenvoient directement les ICE candidates à l'autreRTCPeerConnection(c'est une simulation de signalisation pour que l'exemple fonctionne sur une seule page). Dans une vraie application, ces candidats seraient envoyés via un serveur de signalisation. - L'échange de SDP (Offer/Answer) est également simulé pour établir la connexion entre les deux pairs sur la même page.
- Les fonctions
-
Création et Réception du
DataChannel:- Le Pair A (l'Offerer) utilise
pcA.createDataChannel('chat-file-channel', { ordered: true, maxRetransmits: 0 });pour créer le canal. Les options indiquent un canal ordonné sans retransmissions illimitées (plus léger pour de gros transferts, mais un peu moins fiable si le réseau est très mauvais). - Le Pair B (l'Answerer) n'appelle pas
createDataChannel(). Il écoute l'événementpcB.ondatachannelqui est déclenché lorsque le Pair A propose un canal.
- Le Pair A (l'Offerer) utilise
-
Gestion des Événements du
DataChannel:- La fonction
setupDataChannelEvents()est appelée pour chaque canal (côté émetteur et récepteur) pour configurer les écouteurs d'événements :onopen: Indique que le canal est prêt.onclose: Indique que le canal est fermé.onerror: Gère les erreurs.onmessage: Le plus important. C'est ici que les données reçues sont traitées.
- La fonction
-
Envoi/Réception de Messages Textuels :
- Lorsque
sendMessage()est appelée, elle envoie le contenu dutextareaviadataChannel.send(). - Côté réception (
onmessage), sievent.dataest une chaîne de caractères, elle est affichée comme un message de chat. On tente deJSON.parsepour distinguer les messages de chat des métadonnées de fichier.
- Lorsque
-
Transfert de Fichiers : La Logique de Chunking
- Envoi (
sendFile) :- Lorsque l'utilisateur sélectionne un fichier et clique sur "Envoyer", les métadonnées du fichier (nom, taille, type) sont d'abord envoyées sous forme de message JSON.
- Le fichier est ensuite lu morceau par morceau (
CHUNK_SIZE) en utilisantFileReader.readAsArrayBuffer(). - Chaque
ArrayBufferde morceau est envoyé viadataChannel.send(e.target.result). - Une barre de progression est mise à jour.
- Réception (
onmessagepour les données binaires) :- Lorsque
onmessagereçoit unArrayBufferouBlob, il est traité comme un morceau de fichier. - Ces morceaux sont stockés dans un tableau (
receivedBuffers). - Le
receivedFileSizeest mis à jour et la barre de progression du récepteur est rafraîchie. - Une fois que
receivedFileSizecorrespond àexpectedFileSize(reçu via les métadonnées), tous lesArrayBuffersont combinés en unBlobfinal. URL.createObjectURL(receivedBlob)crée un lien de téléchargement temporaire dans le navigateur pour le fichier reçu.
- Lorsque
- Envoi (
Cette implémentation utilise un seul RTCDataChannel pour les messages textuels et les transferts de fichiers. Dans des applications plus complexes, vous pourriez vouloir des canaux séparés pour différents types de données (e.g., un canal "fiable" pour les fichiers, un canal "non fiable" pour les données de jeu en temps réel).
Considérations et Bonnes Pratiques
- Gestion des Erreurs : Toujours implémenter les gestionnaires
onerrorpour lesRTCPeerConnectionet lesRTCDataChannelpour déboguer et informer l'utilisateur des problèmes de connexion. - Contrôle de Flux : Pour les transferts de fichiers volumineux, la surcharge de données peut être un problème. L'événement
bufferedamountlowduRTCDataChannelpeut être utilisé pour mettre en pause et reprendre l'envoi de morceaux si la mémoire tampon du canal atteint une certaine limite, évitant ainsi la saturation. - Sécurité : Comme mentionné, les Data Channels sont sécurisés par DTLS. Cependant, la sécurité de votre application dépendra aussi de la façon dont vous gérez l'authentification des pairs et la validation des données reçues.
- Persistance et Reconnexion : WebRTC ne gère pas la persistance des sessions ou la reconnexion automatique après une déconnexion. Ces fonctionnalités doivent être implémentées au niveau de l'application.
- Limites de Taille des Messages : Bien que
send()puisse accepter de grosBlobouArrayBuffer, les navigateurs les diviseront en interne en paquets de taille limitée (souvent 64KB). C'est pourquoi le chunking est essentiel pour un contrôle manuel et un suivi de progression précis.
Conclusion
Les RTCDataChannel sont un pilier fondamental de WebRTC, étendant ses capacités bien au-delà des communications audio et vidéo. Ils permettent le développement d'applications P2P riches et performantes, offrant un contrôle granulaire sur la fiabilité et l'ordonnancement des données. Que ce soit pour un chat instantané, le partage sécurisé de fichiers, la synchronisation d'états d'applications ou le multijoueur en temps réel, les Data Channels ouvrent la voie à une nouvelle génération d'interactions web décentralisées et directes. En maîtrisant leur utilisation, vous débloquez un potentiel immense pour vos futures applications WebRTC.