From 08d177dac04f42fd50d5e6714c7b92f24568ba75 Mon Sep 17 00:00:00 2001
From: Joao Siebel <joaos_desenv@imdt.com.br>
Date: Thu, 18 Apr 2019 18:15:48 -0300
Subject: [PATCH] add initial network logic

---
 .../imports/api/network-information/index.js  |   9 ++
 .../api/network-information/server/index.js   |   2 +
 .../api/network-information/server/methods.js |   6 +
 .../server/methods/userInstabilityDetected.js |  22 ++++
 .../network-information/server/publisher.js   |  21 +++
 .../methods/setUserEffectiveConnectionType.js |   1 -
 .../imports/startup/client/base.jsx           |   2 +-
 .../components/slow-connection/component.jsx  |   6 +-
 .../user-list-item/user-icons/component.jsx   |   1 -
 .../components/video-provider/component.jsx   |   5 +-
 .../ui/services/network-information/index.js  | 124 ++++++++++++++++--
 .../private/config/settings.yml               |   7 +-
 bigbluebutton-html5/server/main.js            |   1 +
 13 files changed, 186 insertions(+), 21 deletions(-)
 create mode 100644 bigbluebutton-html5/imports/api/network-information/index.js
 create mode 100644 bigbluebutton-html5/imports/api/network-information/server/index.js
 create mode 100644 bigbluebutton-html5/imports/api/network-information/server/methods.js
 create mode 100644 bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js
 create mode 100644 bigbluebutton-html5/imports/api/network-information/server/publisher.js

diff --git a/bigbluebutton-html5/imports/api/network-information/index.js b/bigbluebutton-html5/imports/api/network-information/index.js
new file mode 100644
index 0000000000..de033433a7
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/index.js
@@ -0,0 +1,9 @@
+import { Meteor } from 'meteor/meteor';
+
+const NetworkInformation = new Mongo.Collection('network-information');
+
+if (Meteor.isServer) {
+  NetworkInformation._ensureIndex({ meetingId: 1 });
+}
+
+export default NetworkInformation;
diff --git a/bigbluebutton-html5/imports/api/network-information/server/index.js b/bigbluebutton-html5/imports/api/network-information/server/index.js
new file mode 100644
index 0000000000..c043da1bfd
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/index.js
@@ -0,0 +1,2 @@
+import './methods';
+import './publisher';
diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods.js b/bigbluebutton-html5/imports/api/network-information/server/methods.js
new file mode 100644
index 0000000000..6eaf424c0d
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/methods.js
@@ -0,0 +1,6 @@
+import { Meteor } from 'meteor/meteor';
+import userInstabilityDetected from './methods/userInstabilityDetected';
+
+Meteor.methods({
+  userInstabilityDetected,
+});
diff --git a/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js
new file mode 100644
index 0000000000..52e52d3603
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/methods/userInstabilityDetected.js
@@ -0,0 +1,22 @@
+import { check } from 'meteor/check';
+import NetworkInformation from '/imports/api/network-information';
+import Logger from '/imports/startup/server/logger';
+
+export default function userInstabilityDetected(credentials, sender) {
+  const { meetingId, requesterUserId: receiver } = credentials;
+
+  check(meetingId, String);
+  check(receiver, String);
+  check(sender, String);
+
+  const payload = {
+    time: new Date().getTime(),
+    meetingId,
+    receiver,
+    sender,
+  };
+
+  Logger.debug(`Receiver ${receiver} reported a network instability`);
+
+  return NetworkInformation.insert(payload);
+}
diff --git a/bigbluebutton-html5/imports/api/network-information/server/publisher.js b/bigbluebutton-html5/imports/api/network-information/server/publisher.js
new file mode 100644
index 0000000000..0e83b5cb9e
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/network-information/server/publisher.js
@@ -0,0 +1,21 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import NetworkInformation from '/imports/api/network-information';
+
+function networkInformation(credentials) {
+  const { meetingId } = credentials;
+
+  check(meetingId, String);
+
+  return NetworkInformation.find({
+    meetingId,
+  });
+}
+
+function publish(...args) {
+  const boundNetworkInformation = networkInformation.bind(this);
+
+  return boundNetworkInformation(...args);
+}
+
+Meteor.publish('network-information', publish);
diff --git a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js
index 7a50c580e6..fd6a632d90 100644
--- a/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js
+++ b/bigbluebutton-html5/imports/api/users/server/methods/setUserEffectiveConnectionType.js
@@ -4,7 +4,6 @@ import RedisPubSub from '/imports/startup/server/redis';
 import Logger from '/imports/startup/server/logger';
 import setEffectiveConnectionType from '../modifiers/setUserEffectiveConnectionType';
 
-// http://wicg.github.io/netinfo/#effective-connection-types
 export default function setUserEffectiveConnectionType(credentials, effectiveConnectionType) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index fe043dd91c..630d62f033 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -202,7 +202,7 @@ Base.defaultProps = defaultProps;
 const SUBSCRIPTIONS_NAME = [
   'users', 'meetings', 'polls', 'presentations',
   'slides', 'captions', 'voiceUsers', 'whiteboard-multi-user', 'screenshare',
-  'group-chat', 'presentation-pods', 'users-settings', 'guestUser',
+  'group-chat', 'presentation-pods', 'users-settings', 'guestUser', 'network-information',
 ];
 
 const BaseContainer = withTracker(() => {
diff --git a/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx b/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx
index ef494623b9..9d8c102a12 100644
--- a/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/slow-connection/component.jsx
@@ -3,15 +3,15 @@ import cx from 'classnames';
 import { styles } from './styles';
 
 const SLOW_CONNECTIONS_TYPES = {
-  'slow-2g': {
+  critical: {
     level: styles.bad,
     bars: styles.oneBar,
   },
-  '2g': {
+  danger: {
     level: styles.bad,
     bars: styles.twoBars,
   },
-  '3g': {
+  warning: {
     level: styles.warning,
     bars: styles.threeBars,
   },
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
index dc50a5b67e..2660b4f673 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/user-list-item/user-icons/component.jsx
@@ -4,7 +4,6 @@ import Icon from '/imports/ui/components/icon/component';
 import SlowConnection from '/imports/ui/components/slow-connection/component';
 import { styles } from './styles';
 
-// https://github.com/bigbluebutton/bigbluebutton/issues/5286#issuecomment-465342716
 const SLOW_CONNECTIONS_TYPES = Meteor.settings.public.app.effectiveConnection;
 
 const propTypes = {
diff --git a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
index c1b5389201..a0edfa4fec 100755
--- a/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-provider/component.jsx
@@ -185,7 +185,7 @@ class VideoProvider extends Component {
           this.customGetStats(peer.peerConnection, peer.peerConnection.getRemoteStreams()[0].getVideoTracks()[0], (stats => updateWebcamStats(id, stats)));
         }
       });
-    }, 10000);
+    }, 5000);
   }
 
   componentWillUpdate({ users, userId }) {
@@ -560,7 +560,6 @@ class VideoProvider extends Component {
       if (this.webRtcPeers[id].peerConnection) {
         this.webRtcPeers[id].peerConnection.oniceconnectionstatechange = this._getOnIceConnectionStateChangeCallback(id);
       }
-      console.log('VideoProvider.createWebRTCPeer', this.webRtcPeers);
       newWebcamConnection({ userId: id, peer: this.webRtcPeers[id] });
       currentWebcamConnections(this.webRtcPeers);
     }
@@ -769,6 +768,7 @@ class VideoProvider extends Component {
         encodeUsagePercent: videoInOrOutbound.encodeUsagePercent,
         rtt: videoInOrOutbound.rtt,
         currentDelay: videoInOrOutbound.currentDelay,
+        pliCount: videoInOrOutbound.pliCount,
       };
 
       const videoStatsArray = statsState;
@@ -827,6 +827,7 @@ class VideoProvider extends Component {
           encodeUsagePercent: videoStats.encodeUsagePercent,
           rtt: videoStats.rtt,
           currentDelay: videoStats.currentDelay,
+          pliCount: videoStats.pliCount,
         },
       };
 
diff --git a/bigbluebutton-html5/imports/ui/services/network-information/index.js b/bigbluebutton-html5/imports/ui/services/network-information/index.js
index 414b494387..78e0e37b3e 100644
--- a/bigbluebutton-html5/imports/ui/services/network-information/index.js
+++ b/bigbluebutton-html5/imports/ui/services/network-information/index.js
@@ -1,4 +1,8 @@
-const NetworkInformationCollection = new Mongo.Collection(null);
+import NetworkInformation from '/imports/api/network-information';
+import { makeCall } from '/imports/ui/services/api';
+import Auth from '/imports/ui/services/auth';
+
+const NetworkInformationLocal = new Mongo.Collection(null);
 
 const NAVIGATOR_CONNECTION = 'NAVIGATOR_CONNECTION';
 const NUMBER_OF_WEBCAMS_CHANGED = 'NUMBER_OF_WEBCAMS_CHANGED';
@@ -6,6 +10,11 @@ const STARTED_WEBCAM_SHARING = 'STARTED_WEBCAM_SHARING';
 const STOPPED_WEBCAM_SHARING = 'STOPPED_WEBCAM_SHARING';
 const WEBCAMS_GET_STATUS = 'WEBCAMS_GET_STATUS';
 
+const DANGER_BEGIN_TIME = 5000;
+const DANGER_END_TIME = 30000;
+
+const WARNING_END_TIME = 60000;
+
 let monitoringIntervalRef;
 
 export const currentWebcamConnections = (webrtcConnections) => {
@@ -15,7 +24,7 @@ export const currentWebcamConnections = (webrtcConnections) => {
     payload: Object.keys(webrtcConnections),
   };
 
-  NetworkInformationCollection.insert(doc);
+  NetworkInformationLocal.insert(doc);
 };
 
 export const deleteWebcamConnection = (id) => {
@@ -25,10 +34,10 @@ export const deleteWebcamConnection = (id) => {
     payload: { id },
   };
 
-  NetworkInformationCollection.insert(doc);
+  NetworkInformationLocal.insert(doc);
 };
 
-export const getCurrentWebcams = () => NetworkInformationCollection
+export const getCurrentWebcams = () => NetworkInformationLocal
   .findOne({
     event: NUMBER_OF_WEBCAMS_CHANGED,
   }, { sort: { timestamp: -1 } });
@@ -45,13 +54,74 @@ export const newWebcamConnection = (webRtcPeer) => {
     },
   };
 
-  NetworkInformationCollection.insert(doc);
+  NetworkInformationLocal.insert(doc);
 };
 
 export const startBandwidthMonitoring = () => {
   monitoringIntervalRef = setInterval(() => {
-    console.log('startBandwidthMonitoring', NetworkInformationCollection.find({}).fetch());
-  }, 15000);
+    const monitoringTime = new Date().getTime();
+
+    const dangerLowerBoundary = monitoringTime - DANGER_BEGIN_TIME;
+
+    const warningLowerBoundary = monitoringTime - DANGER_END_TIME;
+    const warningUpperBoundary = monitoringTime - WARNING_END_TIME;
+
+    const warningZone = NetworkInformationLocal
+      .find({
+        event: WEBCAMS_GET_STATUS,
+        timestamp: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
+        $or: [
+          {
+            'payload.id': Auth.userID,
+            'payload.stats.deltaPliCount': { $gt: 0 },
+          },
+          {
+            'payload.id': { $ne: Auth.userID },
+            'payload.stats.deltaPacketsLost': { $gt: 0 },
+          },
+        ],
+      }).count();
+
+    const warningZoneReceivers = NetworkInformation
+      .find({
+        sender: Auth.userID,
+        time: { $lte: warningLowerBoundary, $gt: warningUpperBoundary },
+      }).count();
+
+    const dangerZone = NetworkInformationLocal
+      .find({
+        event: WEBCAMS_GET_STATUS,
+        timestamp: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
+        $or: [
+          {
+            'payload.id': Auth.userID,
+            'payload.stats.deltaPliCount': { $gt: 0 },
+          },
+          {
+            'payload.id': { $ne: Auth.userID },
+            'payload.stats.deltaPacketsLost': { $gt: 0 },
+          },
+        ],
+      }).count();
+
+    const dangerZoneReceivers = NetworkInformation
+      .find({
+        sender: Auth.userID,
+        time: { $lt: dangerLowerBoundary, $gte: warningLowerBoundary },
+      }).count();
+
+    if (warningZoneReceivers && !dangerZone) {
+      if (!warningZoneReceivers) {
+        makeCall('setUserEffectiveConnectionType', 'warning');
+      }
+    } else if (dangerZone) {
+      if (!dangerZoneReceivers) {
+        makeCall('setUserEffectiveConnectionType', 'danger');
+      }
+    } else {
+      makeCall('setUserEffectiveConnectionType', '');
+    }
+  }, 5000);
 };
 
 export const stopBandwidthMonitoring = () => {
@@ -69,12 +139,18 @@ export const updateNavigatorConnection = ({ effectiveType, downlink, rtt }) => {
     },
   };
 
-  NetworkInformationCollection.insert(doc);
+  NetworkInformationLocal.insert(doc);
 };
 
 export const updateWebcamStats = (id, stats) => {
   if (!stats) return;
 
+  const lastStatus = NetworkInformationLocal
+    .findOne(
+      { event: WEBCAMS_GET_STATUS, 'payload.id': id },
+      { sort: { timestamp: -1 } },
+    );
+
   const { video } = stats;
 
   const doc = {
@@ -83,11 +159,39 @@ export const updateWebcamStats = (id, stats) => {
     payload: { id, stats: video },
   };
 
-  NetworkInformationCollection.insert(doc);
+  if (lastStatus) {
+    const {
+      payload: {
+        stats: {
+          packetsLost,
+          packetsReceived,
+          packetsSent,
+          pliCount,
+        },
+      },
+    } = lastStatus;
+    const normalizedVideo = { ...video };
+
+    normalizedVideo.deltaPacketsLost = video.packetsLost - packetsLost;
+    normalizedVideo.deltaPacketsReceived = video.packetsReceived - packetsReceived;
+    normalizedVideo.deltaPacketsSent = video.packetsSent - packetsSent;
+    normalizedVideo.deltaPliCount = video.pliCount - pliCount;
+
+    doc.payload = {
+      id,
+      stats: normalizedVideo,
+    };
+
+    if (normalizedVideo.deltaPacketsLost > 0) {
+      makeCall('userInstabilityDetected', id);
+    }
+  }
+
+  NetworkInformationLocal.insert(doc);
 };
 
 export default {
-  NetworkInformationCollection,
+  NetworkInformationLocal,
   currentWebcamConnections,
   deleteWebcamConnection,
   getCurrentWebcams,
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 90a0987f38..d7f5b91743 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -83,9 +83,10 @@ public:
     showHelpButton: true
     enableExternalVideo: true
     effectiveConnection:
-      - slow-2g
-      - 2g
-      - 3g
+      - critical
+      - danger
+      - warning
+      - good
   kurento:
     wsUrl: HOST
     chromeDefaultExtensionKey: akgoaoikmbmhcopjgakkcepdgdgkjfbc
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index 82bffd06f5..24ca70c43b 100755
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -19,6 +19,7 @@ import '/imports/api/users-settings/server';
 import '/imports/api/voice-users/server';
 import '/imports/api/whiteboard-multi-user/server';
 import '/imports/api/video/server';
+import '/imports/api/network-information/server';
 
 import '/imports/api/external-videos/server';
 import '/imports/api/guest-users/server';
-- 
GitLab