Skip to content

Instantly share code, notes, and snippets.

@noonat
Last active April 11, 2022 18:02
Show Gist options
  • Save noonat/91286152bf1882c658bf to your computer and use it in GitHub Desktop.
Save noonat/91286152bf1882c658bf to your computer and use it in GitHub Desktop.
WebRTC Data Channels
// This is a bare bones example of creating a data channel between two WebRTC
// peers. Let's imagine two peers trying to connect to each other. We'll call
// one the "offer peer", and the other the "answer peer". The offer peer will
// be the one initiating a connection, and the answer peer will be the one
// responding to it.
//
// The two peers must use a separate connection to negotiate their connection.
// A websocket connection to a shared server is often used for this negotiation.
// They will exchange offers and answers using the websocket. Each peer will
// also attempt to discover more details about themselves (ICE), such as their
// public and private IP addresses. A candidate will be generated for each
// potential way the other peer could connect to them, and that candidate must
// also be sent to the other peer over the websocket.
//
// Eventually, the two will hopefully establish a connection, at which time
// they can begin communicating with each other directly and forget about the
// websocket connection.
//
// In more detail:
//
// 1. Offer peer creates a peer connection.
// 2. Offer peer listens for ICE candidates on its connection. The handler will
// be called for each candidate it discovers, and those candidates should be
// sent to answer peer over some other transport, like a websocket
// connection to a shared server.
// 3. Offer peer uses that connection to create a data channel. Note that only
// the offering side actually creates a data channel.
// 4. Offer peer creates an offer object (which includes information about the
// data channel).
// 5. Offer peer sets the local description for its connection to the offer
// it created.
// 6. Offer peer sends that offer to answer peer via a websocket connection.
// 7. Answer peer creates a peer connection.
// 8. Answer peer listens for ICE candidates, and sends each candidate to offer
// peer via a websocket connection.
// 9. Answer peer receives the offer via a websocket connection.
// 10. Answer peer sets the remote description for its connection to the offer
// it received.
// 11. Answer peer creates a corresponding answer object.
// 12. Answer peer sets the local description for its connection to the answer
// it created.
// 13. Answer peer sends its answer to offer peer via a websocket connection.
// 14. A data channel event is triggered on answer peer's connection. The
// channel on the event should be used to communicate with offer peer.
// 14. Offer peer recieves the answer via a websocket connection.
// 15. Offer peer sets the remote description for its connection to the answer
// it received.
// 16. When the connection is established, an open event will be triggered for
// both data channels, and the peers can start sending direct messages.
var RTCPeerConnection = (window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection);
var RTCSessionDescription = (window.RTCSessionDescription ||
window.mozRTCSessionDescription ||
window.webkitRTCSessionDescription);
// This configuration is passed when we create a new peer connection object.
// It provides a set of servers used to establish a connection. STUN servers
// are used to discover our external IP address, and TURN servers (none listed
// here) are used to proxy a connection when a peer is behind a restrictive
// firewall that prevents a direct connection.
var peerConnectionConfig = {
'iceServers': [
{'url': 'stun:stun.l.google.com:19302'},
{'url': 'stun:stun1.l.google.com:19302'},
{'url': 'stun:stun2.l.google.com:19302'},
{'url': 'stun:stun3.l.google.com:19302'},
{'url': 'stun:stun4.l.google.com:19302'}
]
};
// Think of offerPeer as our local client. It's going to create a data channel
// and an offer and send them to the other peer.
var offerPeer = new RTCPeerConnection(peerConnectionConfig);
offerPeer.onicecandidate = onOfferPeerIceCandidate;
// Think of answerPeer as our remote server. It's going to accept an offer
// from offerPeer, and create a corresponding answer.
var answerPeer = new RTCPeerConnection(peerConnectionConfig);
answerPeer.ondatachannel = onAnswerPeerDataChannel;
answerPeer.onicecandidate = onAnswerPeerIceCandidate;
// Create the offered data channel. Note that this must happen before the
// offer is created, so that it is included in the offer.
var offerDataChannel = offerPeer.createDataChannel('dataChannel', {
// This configuration basically makes it act like a UDP connection, which is
// useful for games. You can specify different options to a get more TCP-like
// connection instead.
maxRetransmits: 0,
reliable: false
});
addDataChannelListeners(offerDataChannel, 'offerPeer');
// Create an offer to send to answerPeer. Like most operations with the WebRTC
// API, this is asynchronous. Our callback will be invoked once an offer has
// been created.
offerPeer.createOffer(onCreatedOffer, handleError('error creating offer'));
// This is used to attach generic logging handlers for data channels.
function addDataChannelListeners(channel, label) {
channel.onclose = function(event) {
console.log(label + ' data channel close', event);
};
channel.onerror = function(err) {
console.log(label + ' data channel error', event);
};
channel.onmessage = function(event) {
console.log(label + ' data channel message', event);
};
channel.onopen = function(event) {
console.log(label + ' data channel open', event);
channel.send('hello from ' + label);
};
}
// Generic error handler to just log a message.
function handleError(message) {
return function onError(err) {
console.log(message);
};
}
// This will be called for each offer candidate. A candidate is a potential
// address that the other peer can attempt to connect to. Note that
// event.candidate can be null, so we must guard against that. The two peers
// will exchange candidates until they find a connection that works.
function onOfferPeerIceCandidate(event) {
console.log('offerPeer ice candidate', event);
if (event && event.candidate) {
// These would normally be sent to answerPeer over some other transport,
// like a websocket, but since this is local we can just set it here.
answerPeer.addIceCandidate(event.candidate);
}
}
// This will be called when answerPeer receives the offer, since a data channel
// was included in the offer. event.channel will be answerPeer's end of the
// channel. Note that answerPeer does not create a data channel directly, only
// uses this one created as a side effect of the offer.
function onAnswerPeerDataChannel(event) {
console.log('answerPeer data channel', event);
addDataChannelListeners(event.channel, 'answerPeer');
}
// The answer peer will also have this method called for each candidate.
function onAnswerPeerIceCandidate(event) {
console.log('answerPeer ice candidate', event);
if (event && event.candidate) {
// These would normally be sent to offerPeer over some other transport,
// like a websocket, but since this is local we can just set it here.
offerPeer.addIceCandidate(event.candidate);
}
}
// This is called once offerPeer has created its offer, so we can set it
// locally and send it over the wire to answerPeer.
function onCreatedOffer(offer) {
console.log('created offer', offer);
offer = new RTCSessionDescription(offer);
offerPeer.setLocalDescription(offer, onSetLocalOffer,
handleError('error setting local description for offerPeer'));
// Normally we would send this over the wire to answerPeer, but since this is
// all local, we can just set it here.
answerPeer.setRemoteDescription(offer, onSetRemoteOffer,
handleError('error setting remote description for answerPeer'));
}
// This is called once our local description has been set for offerPeer.
function onSetLocalOffer() {
console.log('set local description for offerPeer');
}
// This is called once our remote description has been set for answerPeer.
// Once we have a remote offer, we can create a corresponding answer.
function onSetRemoteOffer() {
console.log('set remote description for answerPeer');
answerPeer.createAnswer(onCreatedAnswer,
handleError('error creating answer'));
}
// This is called once answerPeer has created its answer, so we can set it
// locally and send it over the wire to offerPeer -- just like we did with
// the offer, but the other way around.
function onCreatedAnswer(answer) {
console.log('created answer');
answer = new RTCSessionDescription(answer);
answerPeer.setLocalDescription(answer, onSetLocalAnswer,
handleError('error setting answerPeer local description'));
// Normally we would send this over the wire to offerPeer, but since this is
// all local, we can just set it here.
offerPeer.setRemoteDescription(answer, onSetRemoteAnswer,
handleError('error setting offerPeer remote description'));
}
function onSetLocalAnswer() {
console.log('set local description for answerPeer');
}
function onSetRemoteAnswer() {
console.log('set remote description for offerPeer');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment