Swizec Teller - a geek with a hatswizec.com

    Learning WebRTC peer-to-peer communication, part 2 – connecting 2 browsers on different devices

    This is a Livecoding Recap – an almost-weekly post about interesting things discovered while livecoding. Usually shorter than 500 words. Often with pictures. Livecoding happens almost every Sunday at 2pm PDT on multiple channels. You should subscribe to my Youtube channel to catch me live.

    It worked! We got two browsers on different machines talking to each other without a server.

    Well… after the initial handshake. You still need a server for that.

    Swizec Teller writing a secret book avatarSwizec Teller writing a secret book@Swizec
    OMG IT WORKS! I got two browsers to talk to each other without a server

    Well ... a server is used for the initial handshake. BUT THEN NO SERVER #WebRTC

    Article coming soon
    Tweet media

    That was a fun weekend. Two livecoding sessions. Finally got it working late at night when the cameras weren't rolling. I am nowhere near having it working in time for a true blockchain demo for my WeAreDevelopers talk this Friday. 😅

    You can try it out here. Open that link in two browsers, possibly on different machines.

    You might have to try a couple of times, it's a little finicky. 🤨

    Here's how it works 👇

    How to use WebRTC to connect browsers on different devices

    You can use WebRTC to make browsers talk to each other directly without a server. But because there's no service discovery, you need a signaling server so browsers can find each other.

    The flow goes like this:

    1. Client 1 says hi to the server and registers
    2. Client 2 says hi to the server and registers
    3. Server holds list of identifiers (usernames)
    4. Client 1 tells server to call Client 2
    5. Server tells Client 2 there's a call
    6. Client 2 answers the call
    7. Client 1 and Client 2 are now talking directly

    We followed this WebRTC chat example from MDN to model our code.

    Signaling WebSocket server

    Signaling is the handshake process between two browsers. Our implementation uses WebSockets to do that.

    The server part is the same as the MDN example. Pure copy pasta.

    We made some changes to make it work with now.sh. Namely, we removed all SSL stuff. Zeit wraps our servers in a secure SSL server that then talks to our actual server via an unencrypted connection.

    That was a painful gotcha to learn.

    WebSockets don't work in modern browsers without SSL. And they don't work with self-signed certificates unless you're running localhost. If you want browsers that are not on your machine to talk, you're gonna have to ensure a real SSL cert.

    Easiest way is to deploy on now.

    Connecting to the signaling server

    Talking to the server happens via WebSockets with a 40 line helper class. Instantiate the class, make a connection, listen for messages.

    We create a new WebSocket in connectToSocket, add some callbacks, and hope for the best. The onmessage listener allows us to add additional message listeners later via the messageListeners array.

    sendToServer lets us send a JSON object to our server, and addMsgListener lets us add a new message listener. We'll use this to wire up our PeerConnection helper to our server.

    Establishing a peer connection

    Learning our lesson from WebRTC part 1, we split our RTCPeerConnection stuff into a helper class.

    It's about 148 lines and handles the whole lifecycle. We talked about the code before, so here's a recap 👇

    constructor sets a bunch of instance vars, sets up a new RTCPeerConnection object, tells it which iceServers to use, connects local event listeners, starts listening for messages on our signaling server, and adds our media stream to the peerConnection.

    The next step is handleICECandidate, interactive connectivity establishment, which triggers when a new connection is attempted. It pings our signaling server and says "Yo, new ice candidate here".

    handleICECandidateEvent = (event) => {
    if (event.candidate) {
    type: "new-ice-candidate",
    target: this.targetUsername,
    candidate: event.candidate,

    After that, we’ve got the handleNegotiationNeededEvent, which is called when RTCPeerConnection says some negotiation needs to happen. I don't know what makes it say that.

    But the function creates a new connection offer, updates the local SDP description, and tells our signaling server that we're trying to call someone.

    handleNegotiationNeededEvent = () => {
    const { username, targetUsername } = this;
    .then((offer) => this.peerConnection.setLocalDescription(offer))
    .then(() =>
    name: username,
    target: targetUsername,
    type: "video-offer",
    sdp: this.peerConnection.localDescription,

    Handling signaling messages

    Then we have the fun stuff: handling messages from our signaling server.

    onSignalingMessage = (msg) => {
    switch (msg.type) {
    case "video-answer": // Callee has answered our offer
    case "new-ice-candidate": // A new ICE candidate has been received
    case "hang-up": // The other peer has hung up the call

    When a message comes in, we can do a couple different things. Set ourselves up as the answer party, add a new candidate to our connection, or close.

    Those functions are thin wrappers over WebRTC APIs.

    videoAnswer = ({ sdp }) => {
    .setRemoteDescription(new RTCSessionDescription(sdp))
    newICECandidate = ({ candidate }) => {
    this.peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
    close = () => {
    this.peerConnection = null;

    That's our PeerConnection object. In theory, we could instantiate many of them to connect to multiple remote machines at the same time.

    That will be a fun experiment.

    Putting it all together

    Holding it all together is our WebRTCPeerConnectionWithServer React component. It renders the UI, instantiates both helper classes from above, and handles the user clicking on buttons to drive the process.

    You can see the whole file on GitHub.

    Here are the salient parts. 👇

    call = (user) => {
    targetUsername: user,
    hangUp = () => {
    name: this.state.username,
    target: this.state.targetUsername,
    type: "hang-up",
    createPeerConnection = () => {
    if (this.peerConnection) return;
    this.peerConnection = new PeerConnection({
    gotRemoteStream: this.gotRemoteStream,
    gotRemoteTrack: this.gotRemoteTrack,
    signalingConnection: this.signalingConnection,
    onClose: this.closeVideoCall,
    localStream: this.state.localStream,
    username: this.state.username,
    targetUsername: this.state.targetUsername,
    closeVideoCall = () => {
    this.remoteVideoRef.current.srcObject &&
    .forEach((track) => track.stop());
    this.remoteVideoRef.current.src = null;
    targetUsername: null,
    callDisabled: false,

    call is where the fun starts. Saves whom we're calling to state and creates a peer connection.

    createPeerConnection passes all the things into our PeerConnection class.

    hangUp and closeVideoCall work together to finish our call. We need both because one is user-driven and the other is called when hangup comes from the other side.

    One last thing

    There's one message from the signaling server we have to handle in the glue area: An offer for a call.

    case "video-offer": // Invitation and offer to chat

    When the server tells us someone wants to connect, we have to create a new PeerConnection object on our client and handle the offer. Handling the offer means setting a remote SDP description and sending an answer.

    videoOffer = ({ sdp }) => {
    const { username, targetUsername } = this;
    .setRemoteDescription(new RTCSessionDescription(sdp))
    .then(() => this.peerConnection.createAnswer())
    .then((answer) => {
    return this.peerConnection.setLocalDescription(answer);
    .then(() => {
    name: username,
    targetUsername: targetUsername,
    type: "video-answer",
    sdp: this.peerConnection.localDescription,


    And then it works 🤞

    If all the stars align, you can now have a call between 2 browsers on different machines without talking to the server again.

    Swizec Teller writing a secret book avatarSwizec Teller writing a secret book@Swizec
    OMG IT WORKS! I got two browsers to talk to each other without a server

    Well ... a server is used for the initial handshake. BUT THEN NO SERVER #WebRTC

    Article coming soon
    Tweet media

    Did you enjoy this article?

    Published on May 16th, 2018 in Front End, Technical,

    Learned something new?
    Read more Software Engineering Lessons from Production

    I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.

    Subscribe below 👇

    Software Engineering Lessons from Production

    Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.

    "Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"

    ~ Ashish Kumar

    Join 15,883+ engineers learning lessons from my "raw and honest from the heart" emails.

    4.5 stars average rating

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

    Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Want to brush up on modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️