From 0f3351ee16bc8b5f9d69e09551e73768d4a15d2e Mon Sep 17 00:00:00 2001
From: Moritz Langenstein <ml5717@ic.ac.uk>
Date: Tue, 29 Oct 2019 23:53:50 +0000
Subject: [PATCH] (ml5717) Added cross-platform heartbeat and reconnection
 backoff

---
 package-lock.json     |   6 +--
 src/liowebrtc         |   2 +-
 src/room.js           |  16 ++++++-
 src/y-webrtc/index.js | 102 +++++++++++++++++++++++++++++++++---------
 4 files changed, 100 insertions(+), 26 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index 7bde0ea..7401d2c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6870,9 +6870,9 @@
       }
     },
     "source-map-support": {
-      "version": "0.5.15",
-      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.15.tgz",
-      "integrity": "sha512-wYF5aX1J0+V51BDT3Om7uXNn0ct2FWiV4bvwiGVefxkm+1S1o5jsecE5lb2U28DDblzxzxeIDbTVpXHI9D/9hA==",
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
+      "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
       "dev": true,
       "requires": {
         "buffer-from": "^1.0.0",
diff --git a/src/liowebrtc b/src/liowebrtc
index 7e94603..ce4a2eb 160000
--- a/src/liowebrtc
+++ b/src/liowebrtc
@@ -1 +1 @@
-Subproject commit 7e94603dca2fbffe844fe5ee4f3361e9c361e7cc
+Subproject commit ce4a2ebe160804ed84f7b6fc3bd10c91e766bdcd
diff --git a/src/room.js b/src/room.js
index ae28294..e92d017 100644
--- a/src/room.js
+++ b/src/room.js
@@ -81,8 +81,22 @@ class Room extends EventTarget {
         name: "webrtc",
         url: "/",
         room: this.name,
+        handshake: {
+          initial: 100,
+          interval: 500,
+        },
+        heartbeat: {
+          interval: 500,
+          minimum: 1000,
+          timeout: 10000,
+        },
         onUserEvent: (event) => {
-          if (event.action == "userID") {
+          if (event.action == "userConnection") {
+            const { quality } = event
+            this.dispatchEvent(
+              new CustomEvent("userConnection", { detail: quality }),
+            )
+          } else if (event.action == "userID") {
             const { id } = event
             this.ownID = id
             this.dispatchEvent(new CustomEvent("allocateOwnID", { detail: id }))
diff --git a/src/y-webrtc/index.js b/src/y-webrtc/index.js
index dc2f730..97ff153 100644
--- a/src/y-webrtc/index.js
+++ b/src/y-webrtc/index.js
@@ -14,6 +14,20 @@ function extend(Y) {
       super(y, options)
       this.webrtcOptions = options
 
+      this.webrtcOptions.handshake = this.webrtcOptions.handshake || {}
+      this.webrtcOptions.handshake.initial =
+        this.webrtcOptions.handshake.initial || 100
+      this.webrtcOptions.handshake.interval =
+        this.webrtcOptions.handshake.interval || 500
+
+      this.webrtcOptions.heartbeat = this.webrtcOptions.heartbeat || {}
+      this.webrtcOptions.heartbeat.interval =
+        this.webrtcOptions.heartbeat.interval || 500
+      this.webrtcOptions.heartbeat.minimum =
+        this.webrtcOptions.heartbeat.minimum || 1000
+      this.webrtcOptions.heartbeat.timeout =
+        this.webrtcOptions.heartbeat.timeout || 10000
+
       this.queue = new Worker("js/queue.js")
       this.queue.onmessage = (event) => {
         const method = event.data.method
@@ -96,11 +110,13 @@ function extend(Y) {
         console.log("TODO: LEFT ROOM")
       })
 
-      this.webrtc.on("channelError", (a, b, c, d) => console.log(a, b, c, d))
+      this.webrtc.on("channelError", (a, b, c, d) =>
+        console.log("TODO: CHANNEL ERROR", a, b, c, d),
+      )
 
       this.webrtc.on("channelOpen", (dataChannel, peer) => {
         this.checkAndEnsureUser()
-        console.log(dataChannel)
+
         // Start a handshake to ensure both sides are able to use the channel
         function handshake(peer) {
           const _peer = this.webrtc.getPeerById(peer.id)
@@ -121,10 +137,16 @@ function extend(Y) {
             message: "tw",
           })
 
-          setTimeout(handshake.bind(this, peer), 500)
+          setTimeout(
+            handshake.bind(this, peer),
+            this.webrtcOptions.handshake.interval,
+          )
         }
 
-        setTimeout(handshake.bind(this, peer), 100)
+        setTimeout(
+          handshake.bind(this, peer),
+          this.webrtcOptions.handshake.initial,
+        )
       })
 
       this.webrtc.on("receivedPeerData", (type, message, peer) => {
@@ -166,17 +188,14 @@ function extend(Y) {
         return
       }
 
-      console.log(
-        this.webrtc
-          .getPeerById(uid)
-          .getStats(null)
-          .then((stats) => stats.forEach((report) => console.log(report))),
-      )
-
-      const health = {}
+      const health = {
+        lastStatsResolved: true,
+        lastReceivedBytes: 0,
+        lastReceivedTimestamp: Date.now(),
+      }
       health.cb = setInterval(
         this.heartbeat.bind(this, this.webrtc.getPeerById(uid), health),
-        500,
+        this.webrtcOptions.heartbeat.interval,
       )
 
       this.peers.set(uid, health)
@@ -193,26 +212,67 @@ function extend(Y) {
         return
       }
 
+      if (!health.lastStatsResolved) {
+        return peer.end(true)
+      }
+      health.lastStatsResolved = false
+
       const self = this
 
-      // TODO: Check which stats are supported by different browsers
-      // TODO: Check massive renegotiation on reconnect
       peer.getStats(null).then((stats) => {
-        stats.forEach((report) => {
-          if (report.type == "candidate-pair" && report.selected) {
-            if (Date.now() - report.lastPacketReceivedTimestamp > 10000) {
-              return peer.end(true)
-            }
+        health.lastStatsResolved = true
 
-            if (Date.now() - report.lastPacketReceivedTimestamp > 500) {
+        let disconnect = true
+
+        stats.forEach((report) => {
+          if (
+            report.type == "candidate-pair" &&
+            report.bytesSent > 0 &&
+            report.bytesReceived > 0 &&
+            report.writable
+          ) {
+            const timeSinceLastReceived =
+              Date.now() - health.lastReceivedTimestamp
+
+            if (report.bytesReceived != health.lastReceivedBytes) {
+              health.lastReceivedBytes = report.bytesReceived
+              health.lastReceivedTimestamp = Date.now()
+            } else if (
+              timeSinceLastReceived > self.webrtcOptions.heartbeat.timeout
+            ) {
+              return
+            } else if (
+              timeSinceLastReceived > self.webrtcOptions.heartbeat.interval
+            ) {
               self.queue.postMessage({
                 method: "send",
                 uid: peer.id,
                 channel: "heartbeat",
               })
             }
+
+            for (let f of this.userEventListeners) {
+              f({
+                action: "userConnection",
+                quality:
+                  1.0 -
+                  (self.webrtcOptions.heartbeat.timeout -
+                    Math.max(
+                      timeSinceLastReceived,
+                      self.webrtcOptions.heartbeat.minimum,
+                    )) /
+                    (self.webrtcOptions.heartbeat.timeout -
+                      self.webrtcOptions.heartbeat.minimum),
+              })
+            }
+
+            disconnect = false
           }
         })
+
+        if (disconnect) {
+          peer.end(true)
+        }
       })
     }
 
-- 
GitLab