Arthur de Jong

Open Source / Free Software developer

summaryrefslogtreecommitdiffstats
path: root/src/webrtc.js
blob: c619dd92f253b343f73d227bd3ee1026a5bfba87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/*
 * Webchat WebRTC application
 * https://arthurdejong.org/webchat/
 *
 * Copyright (C) 2020 Arthur de Jong
 *
 * Released under the GNU General Public License, either version 3, or
 * (at your option) any later version.
 */

class WebRTC {
  constructor(server, trackHandler, peerHandler) {
    this.server = server
    this.trackHandler = trackHandler
    this.peerHandler = peerHandler
    this.peerConnections = {}
    this.streams = []
    // generate an identity identifier
    const numbers = window.crypto.getRandomValues(new Uint8Array(4))
    const symbols = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
    this.identity = Array.prototype.slice.call(numbers).map(x => symbols[x % 62]).join('')
    // announce ourselves via the channel
    server.sendMessage({announce: true, sender: this.identity})
  }

  /**
   * Send the specified stream to all peers.
   */
  addStream(stream) {
    this.streams.push(stream)
    // register stream with existing connections
    Object.keys(this.peerConnections).forEach(identity => {
      const peerConnection = this.peerConnections[identity]
      stream.getTracks().forEach(track => { this._addTrack(peerConnection, track, stream) })
    })
  }

  _addTrack(peerConnection, track, stream) {
    // send the track over the peer connection
    var sender = peerConnection.addTrack(track, stream)
    if (track.kind === 'video') {
      sender.setParameters({encodings: [{maxBitrate: 60000, scaleResolutionDownBy: 2}]})
    }
  }

  getPeerConnection(identity) {
    const self = this
    if (!(identity in self.peerConnections)) {
      const peerConnection = new RTCPeerConnection(self.server.RTCConfiguration)
      // handle connection state changes
      peerConnection.addEventListener('connectionstatechange', event => {
        this.peerHandler(event, peerConnection, identity)
        if (peerConnection.connectionState === 'failed' || peerConnection.connectionState === 'disconnected') {
          peerConnection.close()
          delete this.peerConnections[identity]
        }
      })
      // set up event handler to handled incoming tracks
      peerConnection.addEventListener('track', event => {
        this.trackHandler(event, peerConnection, identity)
      })
      // support delivering messages through the control channel for ICE
      peerConnection.addEventListener('icecandidate', event => {
        self.server.sendMessage({icecandidate: event.candidate, sender: self.identity, recipient: identity})
      })
      // re-send the offer if renegotiation is needed
      peerConnection.addEventListener('negotiationneeded', event => {
        peerConnection.createOffer().then(offer => {
          peerConnection.setLocalDescription(offer).then(x => {
            self.server.sendMessage({offer: offer, sender: self.identity, recipient: identity})
          })
        })
      })
      // add any existing streams to the connection
      this.streams.forEach(stream => {
        stream.getTracks().forEach(track => { this._addTrack(peerConnection, track, stream) })
      })
      this.peerConnections[identity] = peerConnection
    }
    return this.peerConnections[identity]
  }

  /**
   * Handle an incoming message on the control channel.
   */
  handleMessage(msg) {
    const self = this
    var peerConnection
    // ignore our own messages
    if (msg.sender === self.identity || !msg.sender) { return }
    if (msg.announce && !(msg.sender in self.peerConnections)) {
      // handle new announce message by sending an offer
      peerConnection = self.getPeerConnection(msg.sender)
      peerConnection.createOffer().then(offer => {
        peerConnection.setLocalDescription(offer).then(x => {
          self.server.sendMessage({offer: offer, sender: self.identity, recipient: msg.sender})
        })
      })
    } else if (msg.offer && msg.recipient === self.identity) {
      // handle offers for us and answer
      peerConnection = self.getPeerConnection(msg.sender)
      peerConnection.setRemoteDescription(new RTCSessionDescription(msg.offer))
      peerConnection.createAnswer().then(answer => {
        peerConnection.setLocalDescription(answer).then(function () {
          self.server.sendMessage({answer: answer, sender: self.identity, recipient: msg.sender})
        })
      })
    } else if (msg.answer && msg.recipient === self.identity && msg.sender in self.peerConnections) {
      // handle answers to offers
      peerConnection = self.getPeerConnection(msg.sender)
      peerConnection.setRemoteDescription(new RTCSessionDescription(msg.answer)).then(x => {})
    } else if ('icecandidate' in msg && msg.recipient === self.identity && msg.sender in self.peerConnections) {
      // handle ICE candidate messages
      peerConnection = self.getPeerConnection(msg.sender)
      peerConnection.addIceCandidate(msg.icecandidate)
    }
  }
}

module.exports = WebRTC