Code Roaster: WebRTC

hoffmann

Peter Hoffmann

Posted on July 28, 2019

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))
  }
}
💖 💪 🙅 🚩
hoffmann
Peter Hoffmann

Posted on July 28, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Code Roaster: WebRTC
coderoaster Code Roaster: WebRTC

July 28, 2019