WebRTC - Sending Messages
WebRTC - Sending Messages with RTCDataChannel
Section titled “WebRTC - Sending Messages with RTCDataChannel”This example demonstrates sending text messages between two peers using WebRTC’s RTCDataChannel. We’ll assume you have a signaling server running (as described in the ‘WebRTC - Signaling’ chapter) on ws://localhost:9090.
The page will have input fields for a login name, the target username to connect to, and the message to send. Create an index.html file:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>WebRTC DataChannel Demo</title></head><body> <div> <label for="loginInput">Your Username: </label> <input id="loginInput" type="text"/> <button id="loginBtn">Login</button> </div> <hr> <div> <label for="otherUsernameInput">Connect to Username: </label> <input id="otherUsernameInput" type="text"/> <button id="connectToOtherUsernameBtn">Establish Connection</button> </div> <hr> <div> <label for="msgInput">Message: </label> <input id="msgInput" type="text"/> <button id="sendMsgBtn">Send Message</button> </div> <div> <h3>Received Messages:</h3> <div id="messages"></div> </div>
<script src="client.js"></script></body></html>Now, create a client.js file with the following JavaScript. This code uses modern JavaScript features like async/await for handling promises from WebRTC APIs.
const loginInput = document.querySelector('#loginInput');const loginBtn = document.querySelector('#loginBtn');const otherUsernameInput = document.querySelector('#otherUsernameInput');const connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn');const msgInput = document.querySelector('#msgInput');const sendMsgBtn = document.querySelector('#sendMsgBtn');const messagesDiv = document.querySelector('#messages');
let connection;let name = '';let connectedUser = '';let myPeerConnection;let dataChannel;
const iceServers = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } // Add TURN server URIs here if needed for robust NAT traversal // { // urls: 'turn:your.turn.server.com:3478', // username: 'user', // credential: 'password' // }];
function displayMessage(message, sender = 'Me') { const p = document.createElement('p'); p.textContent = `${sender}: ${message}`; messagesDiv.appendChild(p); msgInput.value = ''; // Clear input after sending}
// Connect to the signaling serverfunction connectSignaling() { connection = new WebSocket('ws://localhost:9090');
connection.onopen = () => { console.log('Connected to signaling server'); };
connection.onmessage = async (message) => { console.log('Got message from signaling server:', message.data); const data = JSON.parse(message.data);
switch (data.type) { case 'login': onLogin(data.success); break; case 'offer': await onOffer(data.offer, data.name); break; case 'answer': await onAnswer(data.answer); break; case 'candidate': await onCandidate(data.candidate); break; case 'leave': onLeave(); break; default: break; } };
connection.onerror = (err) => { console.error('Signaling server error:', err); };}
function sendSignalingMessage(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message));}
loginBtn.addEventListener('click', () => { name = loginInput.value.trim(); if (name.length > 0) { if (!connection || connection.readyState !== WebSocket.OPEN) { connectSignaling(); // Wait a bit for connection to establish before sending login setTimeout(() => { sendSignalingMessage({ type: 'login', name: name }); }, 500); } else { sendSignalingMessage({ type: 'login', name: name }); } } else { alert('Please enter a username.'); }});
async function onLogin(success) { if (success === false) { alert('Login failed. Username might be taken.'); } else { console.log('Logged in successfully as', name); // Initialize RTCPeerConnection myPeerConnection = new RTCPeerConnection({ iceServers }); console.log('RTCPeerConnection object created');
myPeerConnection.onicecandidate = (event) => { if (event.candidate) { sendSignalingMessage({ type: 'candidate', candidate: event.candidate }); } };
myPeerConnection.oniceconnectionstatechange = () => { console.log('ICE connection state change:', myPeerConnection.iceConnectionState); if (myPeerConnection.iceConnectionState === 'disconnected' || myPeerConnection.iceConnectionState === 'failed' || myPeerConnection.iceConnectionState === 'closed') { onLeave(); // Handle disconnection } };
// For the receiving peer, this event is fired when the data channel is added. myPeerConnection.ondatachannel = (event) => { console.log('Data channel received'); dataChannel = event.channel; setupDataChannelEvents(); }; }}
connectToOtherUsernameBtn.addEventListener('click', async () => { const otherUsername = otherUsernameInput.value.trim(); if (otherUsername.length > 0 && myPeerConnection) { connectedUser = otherUsername; console.log('Attempting to connect to', connectedUser);
// Create data channel for the initiating peer dataChannel = myPeerConnection.createDataChannel('myDataChannel'); console.log('Data channel created'); setupDataChannelEvents();
try { const offer = await myPeerConnection.createOffer(); await myPeerConnection.setLocalDescription(offer); sendSignalingMessage({ type: 'offer', offer: offer }); console.log('Offer sent'); } catch (error) { console.error('Error creating offer:', error); alert('Error creating offer.'); } } else if (!myPeerConnection) { alert('Please login first.'); } else { alert('Please enter a username to connect to.'); }});
async function onOffer(offer, offeringUserName) { if (myPeerConnection) { connectedUser = offeringUserName; console.log('Received offer from', connectedUser); try { await myPeerConnection.setRemoteDescription(new RTCSessionDescription(offer)); const answer = await myPeerConnection.createAnswer(); await myPeerConnection.setLocalDescription(answer); sendSignalingMessage({ type: 'answer', answer: answer }); console.log('Answer sent'); } catch (error) { console.error('Error handling offer:', error); alert('Error handling offer.'); } } else { console.warn('Received offer but not logged in or peer connection not initialized.'); }}
async function onAnswer(answer) { console.log('Received answer'); try { await myPeerConnection.setRemoteDescription(new RTCSessionDescription(answer)); console.log('Remote description set for answer'); } catch (error) { console.error('Error setting remote description for answer:', error); }}
async function onCandidate(candidate) { console.log('Received ICE candidate'); try { await myPeerConnection.addIceCandidate(new RTCIceCandidate(candidate)); console.log('ICE candidate added'); } catch (error) { console.error('Error adding ICE candidate:', error); }}
function setupDataChannelEvents() { dataChannel.onopen = () => { console.log('Data channel OPEN'); alert('Data channel connection established!'); };
dataChannel.onerror = (error) => { console.error('Data channel error:', error); };
dataChannel.onmessage = (event) => { console.log('Got message:', event.data); displayMessage(event.data, connectedUser); };
dataChannel.onclose = () => { console.log('Data channel CLOSED'); };}
sendMsgBtn.addEventListener('click', () => { const val = msgInput.value; if (val.length > 0 && dataChannel && dataChannel.readyState === 'open') { dataChannel.send(val); displayMessage(val, 'Me'); } else if (!dataChannel || dataChannel.readyState !== 'open'){ alert('Data channel is not open. Please establish a connection first.'); } else { alert('Please type a message.'); }});
function onLeave() { console.log('Disconnecting or peer left.'); connectedUser = null; if (myPeerConnection) { myPeerConnection.close(); myPeerConnection = null; } if (dataChannel) { dataChannel.close(); dataChannel = null; } // Optionally, inform the UI alert('Connection closed or peer left.'); otherUsernameInput.value = ''; // Clear the connected user input}
// Automatically connect to signaling when page loads, or prompt user// For simplicity, we'll rely on the login button to initiate signaling connection.console.log('Client script loaded. Please login to start.');In this updated client.js:
- We use
letandconstfor variable declarations. RTCPeerConnectionis instantiated without vendor prefixes.- The
iceServersconfiguration includes STUN servers. For robust connectivity across various networks, especially those with symmetric NATs, you would also need TURN servers. createOffer(),createAnswer(),setLocalDescription(),setRemoteDescription(), andaddIceCandidate()are all Promise-based, handled withasync/awaitfor cleaner asynchronous code.- The
RTCPeerConnectionconstructor no longer needs special flags likeRtpDataChannels: true; data channels are a standard part of the API. - Error handling is included for WebRTC operations and data channel events.
- A simple display for received messages is added.
oniceconnectionstatechangeis used to monitor the connection status and handle disconnections.- The
onLeavefunction helps in cleaning up resources when a peer disconnects.
To test: 1. Run your signaling server (node server.js). 2. Open index.html in two browser tabs (or two different browsers on the same network). 3. In the first tab, enter a username (e.g., ‘Alice’) and click ‘Login’. 4. In the second tab, enter a different username (e.g., ‘Bob’) and click ‘Login’. 5. In Alice’s tab, enter ‘Bob’ in the ‘Connect to Username’ field and click ‘Establish Connection’. 6. Alerts and console logs should indicate the progress of offer/answer exchange and data channel establishment. 7. Once connected, try sending messages. They should appear in the ‘Received Messages’ section of the other tab.
You would observe messages being exchanged in the console and on the page. This demonstrates that RTCDataChannel is a powerful feature of WebRTC for arbitrary data transfer, suitable for chat, gaming, file sharing, and more.