DEV Community

Cover image for Code Roaster: WebRTC
Peter Hoffmann
Peter Hoffmann

Posted on

Code Roaster: WebRTC

Introduction

I want to start a little series with contributions from other people that is called "Code Roaster". This is an invitation to share your code for constructive criticism.

The sentence I hear most often wenn getting shown other peoples code is

I know it's a total mess but it's working and I am going to do it the right way some time in the future.

Let's try to do it right this time and open the lids of our pots and take a look at other peoples' work. Taste it and add your grain of salt.

About me and my work

Hy, I'm working as a full stack developer in a German university and constantly try to improve my skills in programming while keeping up to date with current developments in web technologies. My greatest struggle in IT is the never ending fight against support for outdated browsers (like IE10) and their technological backlog.

Today's project

In 2013 Chris Ball published a serverless WebRTC example that let signaling be done by the user e.g. via IM. The code was maintained for three years until no further development was done. My target was to minimize the example to the smallest working code base using modern web techniques in vanilla js.

I took the code, threw away everything that was unnecessary in my eyes and tried to separate webrtc logic from the user interface. I thereby lost all design as I really suck at that (invitations to anybody who is looking for an ui/ux exercise).

Design decisions

I separated the login into two classes/modules that interact through events. The coupling seems unnecessary complicated:

    const webRtc = new WebRTC(stunServers)
    const gui = new GUI(webRtc)
    webRtc.weave(gui)

Perhaps anybody has an idea how to polish this? Aside from this I don't see any way to improve my code. I invite you to drop your 2¢ for nicer, easier readable, faster running, reusable, maintainable, well structured code.
Fire up the roaster!

Code

The full project can be found at Github

GUI module

/* global EventTarget, CustomEvent */

function timestamp () {
  return (new Date()).toTimeString().substr(0, 8)
}

export default class GUI extends EventTarget {
  constructor (target) {
    super()
    this.target = target

    this.connect = document.querySelector('.connect')
    this.connect.querySelector('button').addEventListener('click', _ => this.clickConnect())
    this.descriptionBox = this.connect.querySelector('textarea')
    this.descriptionBox.value = ''

    this.chat = document.querySelector('.chat')
    this.chat.querySelector('button').addEventListener('click', _ => this.sendMessage())
    this.messageBox = this.chat.querySelector('input[type="text"]')
    this.chatBox = this.chat.querySelector('.chatlog')

    this.addEventListener('candidate', candidateEvent => (this.descriptionBox.value = JSON.stringify(candidateEvent.detail)))
    this.addEventListener('connect', connectEvent => {
      this.connect.style.display = 'none'
      this.chat.style.display = 'initial'
    })
    this.addEventListener('message', messageEvent => this._addChatLine(messageEvent.detail, 'you'))
  }

  trigger (name, detail) {
    this.target.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: detail }))
  }

  clickConnect () {
    const description = this.descriptionBox.value
    try {
      this.trigger('connect', JSON.parse(description))
    } catch (e) {
      this.trigger('connect')
    }
    this.descriptionBox.value = ''
  }

  sendMessage () {
    const message = this.messageBox.value
    if (message.length) {
      this._addChatLine(message, 'me')
      this.trigger('message', message)
    }
    this.messageBox.value = ''
  }

  _addChatLine (text, type) {
    this.chatBox.insertAdjacentHTML('beforeend', `<p class="from-${type}">[${timestamp()}] ${text}</p>`)
    this.chatBox.scrollTop = this.chatBox.scrollHeight
  }
}

WebRTC module

/* global EventTarget, RTCPeerConnection, CustomEvent */

export default class webRTC extends EventTarget {
  constructor (iceServers) {
    super()
    this.connection = new RTCPeerConnection({ 'iceServers': iceServers })
    this.connection.addEventListener('icecandidate',
      ICECandidateEvent => ICECandidateEvent.candidate === null && this.trigger('candidate', this.connection.localDescription))
    this.connection.addEventListener('datachannel',
      ChannelEvent => this.connectChannel(ChannelEvent.channel))
    this.addEventListener('connect', this.connectHandler)
  }

  weave (target) {
    this.target = target
  }

  trigger (name, detail) {
    this.target.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: detail }))
  }

  connectHandler (connectEvent) {
    switch (this.connection.signalingState) {
      case 'stable':
        if (connectEvent.detail === null) {
          this.setupChannel()
          this.connection.createOffer()
            .then(desc => this.connection.setLocalDescription(desc))
        } else {
          this.connection.setRemoteDescription(connectEvent.detail)
          this.connection.createAnswer()
            .then(desc => this.connection.setLocalDescription(desc))
        }
        break
      case 'have-local-offer':
        this.connection.setRemoteDescription(connectEvent.detail)
    }
  }

  setupChannel () {
    this.connectChannel(this.connection.createDataChannel('channel'))
  }

  connectChannel (channel) {
    channel.addEventListener('open', _ => this.trigger('connect'))
    channel.addEventListener('message', messageEvent => this.trigger('message', messageEvent.data))
    this.addEventListener('message', message => channel.send(message.detail))
  }
}

Top comments (4)

Collapse
 
prakartigoel24 profile image
Prakarti Goel

Hi, i am trying to create a simple functionality like this one, where a user creates a room with a roomID and another user joins that room with the same roomID, then the room owner can play a song and the other user will be able to listen to that song as well in sync. I am trying to do this in React.js. Any idea how can i do this?

Collapse
 
ramesh profile image
Ramesh Elaiyavalli

Hi Peter - cool concept. Roasting developer's code. 😀

Your code looks simple enough. For a good screen share experience, you want to offer/ adjust FPS. Better frame rate, better screencast experience.

Killer feature: Adjust zoom level at participant size.
Everyone I know codes in VS code. With dark mode as a default, it is hard for the participants to see code and follow along. Instead of constantly asking the guy sharing screen to zoom in, it would be great if remote viewers can do it on their end. This way developers focus on coding, and others watching adjust to their preferred zoom levels.

webRTC looks surprisingly simple with RTCPeerConnection. Ends up surprisingly complex, when you add more and more features & scale.

Good luck!

Collapse
 
97amarnathk profile image
Amarnath Karthi

You should checkout gitDuck. It has a similar comcept.

Collapse
 
hoffmann profile image
Peter Hoffmann • Edited

Just checked it out, sounds good but is still in beta.
If anyone is interested in GitDuck too, you can support me by registering for beta.