diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 12a0ee40b47398e816e1f2143264f31a7ae53af6..e1b6344a5503dcefcec36c4ce89383fcc0fd1746 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -3,7 +3,7 @@ import BaseAudioBridge from './base';
 import logger from '/imports/startup/client/logger';
 import { fetchStunTurnServers } from '/imports/utils/fetchStunTurnServers';
 import {
-  isUnifiedPlan, toUnifiedPlan, toPlanB, stripMDnsCandidates,
+  isUnifiedPlan, toUnifiedPlan, toPlanB, stripMDnsCandidates, analyzeSdp,
 } from '/imports/utils/sdpUtils';
 
 const MEDIA = Meteor.settings.public.media;
@@ -221,6 +221,13 @@ class SIPSession {
         logger.debug({ logCode: 'sip_js_different_host_name' }, 'Different host name. need to kill');
       }
 
+      const localSdpCallback = (sdp) => {
+        // For now we just need to call the utils function to parse and log the different pieces.
+        // In the future we're going to want to be tracking whether there were TURN candidates
+        // and IPv4 candidates to make informed decisions about what to do on fallbacks/reconnects.
+        analyzeSdp(sdp);
+      };
+
       let userAgentConnected = false;
 
       this.userAgent = new window.SIP.UA({
@@ -236,6 +243,7 @@ class SIPSession {
         hackPlanBUnifiedPlanTranslation: isSafari,
         hackAddAudioTransceiver: isSafariWebview,
         relayOnlyOnReconnect: this.reconnectAttempt && RELAY_ONLY_ON_RECONNECT,
+        localSdpCallback,
       });
 
       const handleUserAgentConnection = () => {
diff --git a/bigbluebutton-html5/imports/utils/sdpUtils.js b/bigbluebutton-html5/imports/utils/sdpUtils.js
index 7545838e22049546d52e8cc4b60eca090954e5b3..642a5376f8380690afa4a3316803369738d3c98a 100755
--- a/bigbluebutton-html5/imports/utils/sdpUtils.js
+++ b/bigbluebutton-html5/imports/utils/sdpUtils.js
@@ -65,6 +65,176 @@ const stripMDnsCandidates = (sdp) => {
   return transform.write(parsedSDP);
 };
 
+const analyzeSdp = (sdp) => {
+  // For now we just need to parse and log the different pieces. In the future we're going to want
+  // to be tracking whether there were TURN candidates and IPv4 candidates to make informed
+  // decisions about what to do on fallbacks/reconnects.
+  const parsedSDP = transform.parse(sdp);
+
+  const v4Info = {
+    found: false,
+    public: false,
+  };
+
+  const v6Info = {
+    found: false,
+    public: false,
+  };
+
+  const srflxInfo = {
+    found: false,
+    type: 'not found',
+    public: false,
+  };
+
+  const prflxInfo = {
+    found: false,
+    type: 'not found',
+    public: false,
+  };
+
+  const relayInfo = {
+    found: false,
+    type: 'not found',
+    public: false,
+  };
+
+  const isPublicIpv4 = (ip) => {
+    const ipParts = ip.split('.');
+    switch (ipParts[0]) {
+      case 10:
+      case 127:
+        return false;
+      case 172:
+        return ipParts[1] <= 16 || ipParts[1] > 32;
+      case 192:
+        return ipParts[1] !== 168;
+      default:
+        return true;
+    }
+  };
+
+  const parseIP = (ip) => {
+    if (ip.indexOf(':') !== -1) return { type: 'v6', public: true };
+    if (ip.indexOf('.local') !== -1) return { type: 'mdns', public: false };
+    if (ip.indexOf('.')) return { type: 'v4', public: isPublicIpv4(ip) };
+    return { type: 'unknown', public: false };
+  };
+
+  // Things to parse:
+  // Are there any IPv4/IPv6
+  // Is there a server reflexive candidate? (srflx) is a public or private IP
+  // Is there a relay (TURN) candidate
+  parsedSDP.media.forEach((media) => {
+    if (media.candidates) {
+      // console.log("**** Found candidates ****")
+      media.candidates.forEach((candidate) => {
+        // console.log(candidate)
+        const ipInfo = parseIP(candidate.ip);
+        switch (ipInfo.type) {
+          case 'v4':
+            v4Info.found = true;
+            v4Info.public = v4Info.public || ipInfo.public;
+            break;
+          case 'v6':
+            v6Info.found = true;
+            v6Info.public = v6Info.public || ipInfo.public;
+            break;
+        }
+
+        switch (candidate.type) {
+          case 'srflx':
+            srflxInfo.found = true;
+
+            if (srflxInfo.type === 'not found') {
+              srflxInfo.type = ipInfo.type;
+            } else if (srflxInfo.type !== ipInfo.type) {
+              srflxInfo.type = 'both';
+            }
+
+            srflxInfo.public = srflxInfo.public || ipInfo.public;
+            break;
+          case 'prflx':
+            prflxInfo.found = true;
+
+            if (prflxInfo.type === 'not found') {
+              prflxInfo.type = ipInfo.type;
+            } else if (prflxInfo.type !== ipInfo.type) {
+              prflxInfo.type = 'both';
+            }
+
+            prflxInfo.public = prflxInfo.public || ipInfo.public;
+            break;
+          case 'relay':
+            relayInfo.found = true;
+
+            if (relayInfo.type === 'not found') {
+              relayInfo.type = ipInfo.type;
+            } else if (relayInfo.type !== ipInfo.type) {
+              relayInfo.type = 'both';
+            }
+
+            relayInfo.public = relayInfo.public || ipInfo.public;
+            break;
+        }
+      });
+      // console.log("**** End of candidates ****")
+    }
+  });
+
+  // candidate types
+  logger.info({
+    logCode: 'sdp_utils_candidate_types',
+    foundV4Candidate: v4Info.found,
+    foundV4PublicCandidate: v4Info.public,
+    foundV6Candidate: v6Info.found,
+  }, `Found candidates ${v4Info.found ? 'with' : 'without'} type v4 (public? ${v4Info.public}) and ${v6Info.found ? 'with' : 'without'} type v6`);
+
+  // server reflexive
+  if (srflxInfo.found) {
+    logger.info({
+      logCode: 'sdp_utils_server_reflexive_found',
+      candidateType: srflxInfo.type,
+      canddiatePublic: srflxInfo.public,
+    }, 'Found a server reflexive candidate');
+  } else {
+    logger.info({
+      logCode: 'sdp_utils_no_server_reflexive',
+    }, 'No server reflexive candidate found');
+  }
+
+  // peer reflexive
+  if (prflxInfo.found) {
+    logger.info({
+      logCode: 'sdp_utils_peer_reflexive_found',
+      candidateType: prflxInfo.type,
+      canddiatePublic: prflxInfo.public,
+    }, 'Found a peer reflexive candidate');
+  } else {
+    logger.info({
+      logCode: 'sdp_utils_no_peer_reflexive',
+    }, 'No peer reflexive candidate found');
+  }
+
+  // relay
+  if (relayInfo.found) {
+    logger.info({
+      logCode: 'sdp_utils_relay_found',
+      candidateType: relayInfo.type,
+      canddiatePublic: relayInfo.public,
+    }, 'Found a relay candidate');
+  } else {
+    logger.info({
+      logCode: 'sdp_utils_no_relay',
+    }, 'No relay candidate found');
+  }
+};
+
 export {
-  interop, isUnifiedPlan, toPlanB, toUnifiedPlan, stripMDnsCandidates,
+  interop,
+  isUnifiedPlan,
+  toPlanB,
+  toUnifiedPlan,
+  stripMDnsCandidates,
+  analyzeSdp,
 };
diff --git a/bigbluebutton-html5/public/compatibility/sip.js b/bigbluebutton-html5/public/compatibility/sip.js
index 73beb201d8079d9c580e19fa760618554af69592..8f9353936a1806d2df047d99e9fdf933768dc485 100755
--- a/bigbluebutton-html5/public/compatibility/sip.js
+++ b/bigbluebutton-html5/public/compatibility/sip.js
@@ -9972,6 +9972,12 @@ UA.prototype.getConfigurationCheck = function () {
         }
       },
 
+      localSdpCallback: function(localSdpCallback) {
+        if (typeof localSdpCallback === 'function') {
+          return localSdpCallback;
+        }
+      },
+
       forceRport: function(forceRport) {
         if (typeof forceRport === 'boolean') {
           return forceRport;
@@ -11648,6 +11654,12 @@ MediaHandler.prototype = Object.create(SIP.MediaHandler.prototype, {
         if (self.session.ua.configuration.hackStripTcp) {
           sdpWrapper.sdp = sdpWrapper.sdp.replace(/^a=candidate:\d+ \d+ tcp .*?\r\n/img, "");
         }
+        
+        // Ensure that this block is after all other SDP manipulations
+        var localSdpCallback = self.session.ua.configuration.localSdpCallback;
+        if (localSdpCallback && typeof localSdpCallback === 'function') {
+          localSdpCallback(sdpWrapper.sdp);
+        }
 
         self.ready = true;
         return sdpWrapper.sdp;