From cc7513279eb6cfd6bb0dc8714885043413b484e0 Mon Sep 17 00:00:00 2001
From: Chad Pilkey <capilkey@gmail.com>
Date: Wed, 12 Jun 2019 21:01:20 +0000
Subject: [PATCH] IPv4 FS audio fallback, auto-reconnect, and fixed input
 select

---
 .../imports/api/audio/client/bridge/base.js   |   1 +
 .../imports/api/audio/client/bridge/sip.js    | 217 +++++++++++++-----
 .../components/audio/audio-modal/styles.scss  |  11 +-
 .../imports/ui/components/audio/container.jsx |   5 +
 .../ui/services/audio-manager/index.js        |  62 +++--
 .../private/config/settings.yml               |   1 +
 bigbluebutton-html5/private/locales/en.json   |   1 +
 .../public/resources/sounds/LeftCall.mp3      | Bin 0 -> 20257 bytes
 8 files changed, 218 insertions(+), 80 deletions(-)
 create mode 100644 bigbluebutton-html5/public/resources/sounds/LeftCall.mp3

diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
index a9c63969a2..c2155b08d7 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/base.js
@@ -16,6 +16,7 @@ export default class BaseAudioBridge {
       started: 'started',
       ended: 'ended',
       failed: 'failed',
+      reconnecting: 'reconnecting',
     };
   }
 
diff --git a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
index 92d6542bd3..d42f1337e5 100755
--- a/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
+++ b/bigbluebutton-html5/imports/api/audio/client/bridge/sip.js
@@ -11,38 +11,18 @@ const MEDIA_TAG = MEDIA.mediaTag;
 const CALL_TRANSFER_TIMEOUT = MEDIA.callTransferTimeout;
 const CALL_HANGUP_TIMEOUT = MEDIA.callHangupTimeout;
 const CALL_HANGUP_MAX_RETRIES = MEDIA.callHangupMaximumRetries;
+const IPV4_FALLBACK_DOMAIN = Meteor.settings.public.app.ipv4FallbackDomain;
 const ICE_NEGOTIATION_FAILED = ['iceConnectionFailed'];
 const CALL_CONNECT_TIMEOUT = 15000;
 const ICE_NEGOTIATION_TIMEOUT = 20000;
 
-export default class SIPBridge extends BaseAudioBridge {
-  constructor(userData) {
-    super(userData);
-
-    const {
-      userId,
-      username,
-      sessionToken,
-    } = userData;
-
-    this.user = {
-      userId,
-      sessionToken,
-      name: username,
-    };
-
-    this.media = {
-      inputDevice: {},
-    };
-
-    this.protocol = window.document.location.protocol;
-    this.hostname = window.document.location.hostname;
-
-    // SDP conversion utilitary methods to be used inside SIP.js
-    window.isUnifiedPlan = isUnifiedPlan;
-    window.toUnifiedPlan = toUnifiedPlan;
-    window.toPlanB = toPlanB;
-    window.stripMDnsCandidates = stripMDnsCandidates;
+class SIPSession {
+  constructor(user, userData, protocol, hostname, baseCallStates) {
+    this.user = user;
+    this.userData = userData;
+    this.protocol = protocol;
+    this.hostname = hostname;
+    this.baseCallStates = baseCallStates;
   }
 
   static parseDTMF(message) {
@@ -58,11 +38,25 @@ export default class SIPBridge extends BaseAudioBridge {
       const callExtension = extension ? `${extension}${this.userData.voiceBridge}` : this.userData.voiceBridge;
 
       const callback = (message) => {
+        // There will sometimes we erroneous errors put out like timeouts and improper shutdowns,
+        // but only the first error ever matters
+        if (this.alreadyErrored) {
+          logger.info({ logCode: 'sip_js_absorbing_callback_message' }, `Absorbing a redundant callback message. ${JSON.stringify(message)}`);
+          return;
+        }
+
+        if (message.status === this.baseCallStates.failed) {
+          this.alreadyErrored = true;
+        }
+
         managerCallback(message).then(resolve);
       };
 
       this.callback = callback;
 
+      // If there's an extension passed it means that we're joining the echo test first
+      this.inEchoTest = !!extension;
+
       return this.doCall({ callExtension, isListenOnly, inputStream })
         .catch((reason) => {
           reject(reason);
@@ -98,6 +92,8 @@ export default class SIPBridge extends BaseAudioBridge {
 
   transferCall(onTransferSuccess) {
     return new Promise((resolve, reject) => {
+      this.inEchoTest = false;
+
       const timeout = setInterval(() => {
         clearInterval(timeout);
         logger.error({ logCode: 'sip_js_transfer_timed_out' }, 'Timeout on transfering from echo test to conference');
@@ -114,7 +110,7 @@ export default class SIPBridge extends BaseAudioBridge {
 
       this.currentSession.on('dtmf', (event) => {
         if (event.body && (typeof event.body === 'string')) {
-          const key = SIPBridge.parseDTMF(event.body);
+          const key = SIPSession.parseDTMF(event.body);
           if (key === '7') {
             clearInterval(timeout);
             onTransferSuccess();
@@ -141,6 +137,8 @@ export default class SIPBridge extends BaseAudioBridge {
         }
 
         this.currentSession.bye();
+        this.userAgent.stop();
+
         hangupRetries += 1;
 
         setTimeout(() => {
@@ -178,8 +176,6 @@ export default class SIPBridge extends BaseAudioBridge {
         callerIdName,
       } = this.user;
 
-      let userAgentConnected = false;
-
       // WebView safari needs a transceiver to be added. Made it a SIP.js hack.
       // Don't like the UA picking though, we should straighten everything to user
       // transceivers - prlanzarin 2019/05/21
@@ -193,7 +189,18 @@ export default class SIPBridge extends BaseAudioBridge {
 
       logger.debug('Creating the user agent');
 
-      let userAgent = new window.SIP.UA({
+      if (this.userAgent && this.userAgent.isConnected()) {
+        if (this.userAgent.configuration.hostPortParams === this.hostname) {
+          logger.debug('Reusing the user agent');
+          resolve(this.userAgent);
+          return;
+        }
+        logger.debug('different host name. need to kill');
+      }
+
+      let userAgentConnected = false;
+
+      this.userAgent = new window.SIP.UA({
         uri: `sip:${encodeURIComponent(callerIdName)}@${hostname}`,
         wsServers: `${(protocol === 'https:' ? 'wss://' : 'ws://')}${hostname}/ws`,
         displayName: callerIdName,
@@ -207,21 +214,23 @@ export default class SIPBridge extends BaseAudioBridge {
         hackAddAudioTransceiver: isSafariWebview,
       });
 
-      userAgent.removeAllListeners('connected');
-      userAgent.removeAllListeners('disconnected');
-
       const handleUserAgentConnection = () => {
         userAgentConnected = true;
-        resolve(userAgent);
+        resolve(this.userAgent);
       };
 
       const handleUserAgentDisconnection = () => {
-        userAgent.stop();
-        userAgent = null;
+        if (this.userAgent) {
+          this.userAgent.removeAllListeners();
+          this.userAgent.stop();
+          this.userAgent = null;
+        }
 
         let error;
         let bridgeError;
 
+        if (this.userRequestedHangup) return;
+
         if (userAgentConnected) {
           error = 1001;
           bridgeError = 'Websocket disconnected';
@@ -238,10 +247,10 @@ export default class SIPBridge extends BaseAudioBridge {
         reject(this.baseErrorCodes.CONNECTION_ERROR);
       };
 
-      userAgent.on('connected', handleUserAgentConnection);
-      userAgent.on('disconnected', handleUserAgentDisconnection);
+      this.userAgent.on('connected', handleUserAgentConnection);
+      this.userAgent.on('disconnected', handleUserAgentDisconnection);
 
-      userAgent.start();
+      this.userAgent.start();
     });
   }
 
@@ -279,8 +288,10 @@ export default class SIPBridge extends BaseAudioBridge {
     return new Promise((resolve) => {
       const { mediaHandler } = currentSession;
 
-      this.connectionCompleted = false;
-      this.inEcho = false;
+      let iceCompleted = false;
+      let fsReady = false;
+
+      this.currentSession = currentSession;
 
       let connectionCompletedEvents = ['iceConnectionCompleted', 'iceConnectionConnected'];
       // Edge sends a connected first and then a completed, but the call isn't ready until
@@ -291,7 +302,8 @@ export default class SIPBridge extends BaseAudioBridge {
       }
 
       const checkIfCallReady = () => {
-        if (this.connectionCompleted && this.inEcho) {
+        if (iceCompleted && fsReady) {
+          this.webrtcConnected = true;
           this.callback({ status: this.baseCallStates.started });
           resolve();
         }
@@ -312,9 +324,10 @@ export default class SIPBridge extends BaseAudioBridge {
       const handleSessionAccepted = () => {
         logger.info({ logCode: 'sip_js_session_accepted' }, 'Audio call session accepted');
         clearTimeout(callTimeout);
+        currentSession.off('accepted', handleSessionAccepted);
 
         // If ICE isn't connected yet then start timeout waiting for ICE to finish
-        if (!this.connectionCompleted) {
+        if (!iceCompleted) {
           iceNegotiationTimeout = setTimeout(() => {
             this.callback({
               status: this.baseCallStates.failed,
@@ -338,7 +351,7 @@ export default class SIPBridge extends BaseAudioBridge {
         clearTimeout(callTimeout);
         clearTimeout(iceNegotiationTimeout);
         connectionCompletedEvents.forEach(e => mediaHandler.off(e, handleConnectionCompleted));
-        this.connectionCompleted = true;
+        iceCompleted = true;
 
         checkIfCallReady();
       };
@@ -347,6 +360,8 @@ export default class SIPBridge extends BaseAudioBridge {
       const handleSessionTerminated = (message, cause) => {
         clearTimeout(callTimeout);
         clearTimeout(iceNegotiationTimeout);
+        currentSession.off('terminated', handleSessionTerminated);
+
         if (!message && !cause && !!this.userRequestedHangup) {
           return this.callback({
             status: this.baseCallStates.ended,
@@ -356,7 +371,7 @@ export default class SIPBridge extends BaseAudioBridge {
         logger.error({ logCode: 'sip_js_call_terminated' }, `Audio call terminated. cause=${cause}`);
 
         let mappedCause;
-        if (!this.connectionCompleted) {
+        if (!iceCompleted) {
           mappedCause = '1004';
         } else {
           mappedCause = '1005';
@@ -371,7 +386,7 @@ export default class SIPBridge extends BaseAudioBridge {
       currentSession.on('terminated', handleSessionTerminated);
 
       const handleIceNegotiationFailed = (peer) => {
-        if (this.connectionCompleted) {
+        if (iceCompleted) {
           logger.error({ logCode: 'sipjs_ice_failed_after' }, 'ICE connection failed after success');
         } else {
           logger.error({ logCode: 'sipjs_ice_failed_before' }, 'ICE connection failed before success');
@@ -404,44 +419,131 @@ export default class SIPBridge extends BaseAudioBridge {
 
       const inEchoDTMF = (event) => {
         if (event.body && typeof event.body === 'string') {
-          const dtmf = SIPBridge.parseDTMF(event.body);
+          const dtmf = SIPSession.parseDTMF(event.body);
           if (dtmf === '0') {
-            this.inEcho = true;
+            fsReady = true;
             checkIfCallReady();
           }
         }
         currentSession.off('dtmf', inEchoDTMF);
       };
       currentSession.on('dtmf', inEchoDTMF);
+    });
+  }
+}
 
-      this.currentSession = currentSession;
+export default class SIPBridge extends BaseAudioBridge {
+  constructor(userData) {
+    super(userData);
+
+    const {
+      userId,
+      username,
+      sessionToken,
+    } = userData;
+
+    this.user = {
+      userId,
+      sessionToken,
+      name: username,
+    };
+
+    this.media = {
+      inputDevice: {},
+    };
+
+    this.protocol = window.document.location.protocol;
+    this.hostname = window.document.location.hostname;
+
+    // SDP conversion utilitary methods to be used inside SIP.js
+    window.isUnifiedPlan = isUnifiedPlan;
+    window.toUnifiedPlan = toUnifiedPlan;
+    window.toPlanB = toPlanB;
+    window.stripMDnsCandidates = stripMDnsCandidates;
+  }
+
+  joinAudio({ isListenOnly, extension, inputStream }, managerCallback) {
+    const hasFallbackDomain = typeof IPV4_FALLBACK_DOMAIN === 'string' && IPV4_FALLBACK_DOMAIN !== '';
+
+    return new Promise((resolve, reject) => {
+      let { hostname } = this;
+
+      this.activeSession = new SIPSession(this.user, this.userData, this.protocol, hostname, this.baseCallStates);
+
+      const callback = (message) => {
+        if (message.status === this.baseCallStates.failed) {
+          let shouldTryReconnect = false;
+
+          // Try and get the call to clean up and end on an error
+          this.activeSession.exitAudio().catch(() => {});
+
+          if (this.activeSession.webrtcConnected) {
+            // webrtc was able to connect so just try again
+            message.silenceNotifications = true;
+            callback({ status: this.baseCallStates.reconnecting });
+            shouldTryReconnect = true;
+          } else if (hasFallbackDomain === true && hostname !== IPV4_FALLBACK_DOMAIN) {
+            message.silenceNotifications = true;
+            logger.info({ logCode: 'sip_js_attempt_ipv4_fallback' }, 'Attempting to fallback to IPv4 domain for audio');
+            hostname = IPV4_FALLBACK_DOMAIN;
+            shouldTryReconnect = true;
+          }
+
+          if (shouldTryReconnect) {
+            const fallbackExtension = this.activeSession.inEchoTest ? extension : undefined;
+            this.activeSession = new SIPSession(this.user, this.userData, this.protocol, hostname, this.baseCallStates);
+            this.activeSession.joinAudio({ isListenOnly, extension: fallbackExtension, inputStream }, callback)
+              .then((value) => {
+                resolve(value);
+              }).catch((reason) => {
+                reject(reason);
+              });
+          }
+        }
+
+        return managerCallback(message);
+      };
+
+      this.activeSession.joinAudio({ isListenOnly, extension, inputStream }, callback)
+        .then((value) => {
+          resolve(value);
+        }).catch((reason) => {
+          reject(reason);
+        });
     });
   }
 
+  transferCall(onTransferSuccess) {
+    return this.activeSession.transferCall(onTransferSuccess);
+  }
+
+  exitAudio() {
+    return this.activeSession.exitAudio();
+  }
+
   setDefaultInputDevice() {
     const handleMediaSuccess = (mediaStream) => {
       const deviceLabel = mediaStream.getAudioTracks()[0].label;
       window.defaultInputStream = mediaStream.getTracks();
       return navigator.mediaDevices.enumerateDevices().then((mediaDevices) => {
         const device = mediaDevices.find(d => d.label === deviceLabel);
-        return this.changeInputDevice(device.deviceId);
+        return this.changeInputDevice(device.deviceId, deviceLabel);
       });
     };
 
     return navigator.mediaDevices.getUserMedia({ audio: true }).then(handleMediaSuccess);
   }
 
-  changeInputDevice(value) {
+  changeInputDevice(deviceId, deviceLabel) {
     const {
       media,
     } = this;
-
     if (media.inputDevice.audioContext) {
       const handleAudioContextCloseSuccess = () => {
         media.inputDevice.audioContext = null;
         media.inputDevice.scriptProcessor = null;
         media.inputDevice.source = null;
-        return this.changeInputDevice(value);
+        return this.changeInputDevice(deviceId);
       };
 
       return media.inputDevice.audioContext.close().then(handleAudioContextCloseSuccess);
@@ -453,14 +555,15 @@ export default class SIPBridge extends BaseAudioBridge {
       media.inputDevice.audioContext = new window.webkitAudioContext();
     }
 
-    media.inputDevice.id = value;
+    media.inputDevice.id = deviceId;
+    media.inputDevice.label = deviceLabel;
     media.inputDevice.scriptProcessor = media.inputDevice.audioContext
       .createScriptProcessor(2048, 1, 1);
     media.inputDevice.source = null;
 
     const constraints = {
       audio: {
-        deviceId: value,
+        deviceId,
       },
     };
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
index bbbefe01c0..c42b8f4c8a 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/styles.scss
@@ -162,14 +162,17 @@
 }
 
 .connectingAnimation {
+  margin: auto;
+  display: inline-block;
+  width: 1.5em;
+
   &:after {
     overflow: hidden;
     display: inline-block;
     vertical-align: bottom;
     content: "\2026"; /* ascii code for the ellipsis character */
     width: 0;
-    margin-right: 0.75em;
-    margin-left: 0.75em;
+    margin-left: 0.25em;
 
     :global(.animationsEnabled) & {
       animation: ellipsis steps(4, end) 900ms infinite;
@@ -179,9 +182,7 @@
 
 @keyframes ellipsis {
   to {
-    width: 1.25em;
-    margin-right: 0;
-    margin-left: 0;
+    width: 1.5em;
   }
 }
 
diff --git a/bigbluebutton-html5/imports/ui/components/audio/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
index 7ef508254e..e26257d92f 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/container.jsx
@@ -24,6 +24,10 @@ const intlMessages = defineMessages({
     id: 'app.audioManager.leftAudio',
     description: 'Left audio toast message',
   },
+  reconnectingAudio: {
+    id: 'app.audioManager.reconnectingAudio',
+    description: 'Reconnecting audio toast message',
+  },
   genericError: {
     id: 'app.audioManager.genericError',
     description: 'Generic error message',
@@ -116,6 +120,7 @@ export default withModalMounter(injectIntl(withTracker(({ mountModal, intl }) =>
       JOINED_AUDIO: intlMessages.joinedAudio,
       JOINED_ECHO: intlMessages.joinedEcho,
       LEFT_AUDIO: intlMessages.leftAudio,
+      RECONNECTING_AUDIO: intlMessages.reconnectingAudio,
     },
     error: {
       GENERIC_ERROR: intlMessages.genericError,
diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
index 740fc59d84..2f370a6195 100755
--- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
+++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js
@@ -19,6 +19,7 @@ const CALL_STATES = {
   STARTED: 'started',
   ENDED: 'ended',
   FAILED: 'failed',
+  RECONNECTING: 'reconnecting',
 };
 
 class AudioManager {
@@ -81,6 +82,12 @@ class AudioManager {
   }
 
   askDevicesPermissions() {
+    // Check to see if the stream has already been retrieved becasue then we don't need to
+    // request. This is a fix for an issue with the input device selector.
+    if (this.inputStream) {
+      return Promise.resolve();
+    }
+
     // Only change the isWaitingPermissions for the case where the user didnt allowed it yet
     const permTimeout = setTimeout(() => {
       if (!this.devicesInitialized) { this.isWaitingPermissions = true; }
@@ -107,35 +114,38 @@ class AudioManager {
     this.isListenOnly = false;
     this.isEchoTest = false;
 
-    const callOptions = {
-      isListenOnly: false,
-      extension: null,
-      inputStream: this.inputStream,
-    };
-
     return this.askDevicesPermissions()
       .then(this.onAudioJoining.bind(this))
-      .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
+      .then(() => {
+        const callOptions = {
+          isListenOnly: false,
+          extension: null,
+          inputStream: this.inputStream,
+        };
+        return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
+      });
   }
 
   joinEchoTest() {
     this.isListenOnly = false;
     this.isEchoTest = true;
 
-    const callOptions = {
-      isListenOnly: false,
-      extension: ECHO_TEST_NUMBER,
-      inputStream: this.inputStream,
-    };
-
     return this.askDevicesPermissions()
       .then(this.onAudioJoining.bind(this))
-      .then(() => this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)));
+      .then(() => {
+        const callOptions = {
+          isListenOnly: false,
+          extension: ECHO_TEST_NUMBER,
+          inputStream: this.inputStream,
+        };
+        return this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this));
+      });
   }
 
   async joinListenOnly(retries = 0) {
     this.isListenOnly = true;
     this.isEchoTest = false;
+
     const { name } = browser();
     // The kurento bridge isn't a full audio bridge yet, so we have to differ it
     const bridge = this.useKurento ? this.listenOnlyBridge : this.bridge;
@@ -222,7 +232,6 @@ class AudioManager {
     const bridge = (this.useKurento && this.isListenOnly) ? this.listenOnlyBridge : this.bridge;
 
     this.isHangingUp = true;
-    this.isEchoTest = false;
 
     return bridge.exitAudio();
   }
@@ -289,6 +298,10 @@ class AudioManager {
     if (!this.error && !this.isEchoTest) {
       this.notify(this.intl.formatMessage(this.messages.info.LEFT_AUDIO), false, 'audio_off');
     }
+    if (!this.isEchoTest) {
+      this.playHangUpSound();
+    }
+
     window.parent.postMessage({ response: 'notInAudio' }, '*');
   }
 
@@ -298,12 +311,14 @@ class AudioManager {
         STARTED,
         ENDED,
         FAILED,
+        RECONNECTING,
       } = CALL_STATES;
 
       const {
         status,
         error,
         bridgeError,
+        silenceNotifications,
       } = response;
 
       if (status === STARTED) {
@@ -316,10 +331,16 @@ class AudioManager {
         const errorKey = this.messages.error[error] || this.messages.error.GENERIC_ERROR;
         const errorMsg = this.intl.formatMessage(errorKey, { 0: bridgeError });
         this.error = !!error;
-        this.notify(errorMsg, true);
         logger.error({ logCode: 'audio_failure', error, cause: bridgeError }, `Audio Error ${JSON.stringify(errorMsg)}`);
-        this.exitAudio();
-        this.onAudioExit();
+        if (silenceNotifications !== true) {
+          this.notify(errorMsg, true);
+          this.exitAudio();
+          this.onAudioExit();
+        }
+      } else if (status === RECONNECTING) {
+        logger.info({ logCode: 'audio_reconnecting' }, 'Attempting to reconnect audio');
+        this.notify(this.intl.formatMessage(this.messages.info.RECONNECTING_AUDIO), true);
+        this.playHangUpSound();
       }
     });
   }
@@ -409,6 +430,11 @@ class AudioManager {
     return this._userData;
   }
 
+  playHangUpSound() {
+    this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename}/resources/sounds/LeftCall.mp3`);
+    this.alert.play();
+  }
+
   notify(message, error = false, icon = 'unmute') {
     const audioIcon = this.isListenOnly ? 'listen' : icon;
 
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index 8c45ed80c8..c5495a5d17 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -20,6 +20,7 @@ public:
     askForFeedbackOnLogout: false
     allowUserLookup: false
     enableNetworkInformation: false
+    ipv4FallbackDomain: ""
     defaultSettings:
       application:
         animations: true
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index cd59a3c82e..cd227e18b0 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -394,6 +394,7 @@
     "app.audioManager.joinedAudio": "You have joined the audio conference",
     "app.audioManager.joinedEcho": "You have joined the echo test",
     "app.audioManager.leftAudio": "You have left the audio conference",
+    "app.audioManager.reconnectingAudio": "Attempting to reconnect audio",
     "app.audioManager.genericError": "Error: An error has occurred, please try again",
     "app.audioManager.connectionError": "Error: Connection error",
     "app.audioManager.requestTimeout": "Error: There was a timeout in the request",
diff --git a/bigbluebutton-html5/public/resources/sounds/LeftCall.mp3 b/bigbluebutton-html5/public/resources/sounds/LeftCall.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..eaead7041bb34072a4f79bdef53c827f8009ecc7
GIT binary patch
literal 20257
zcmdqIc{r5c8$bTc7z_quA3|d+A<GyPvdl2nMj=Fz8T-;gw9sP4I@at_Dtq>%DAL>5
zN|GhfDq~56QX$#q_e}41_4)p;>vvthzkYxG&UHD@@jT~#-S@fAec$JP&U40^sLcU@
z1*+ue;^G1}#KFG<Cw#-sP)_;)q!WGtUT7_>hL#4_Y`?87_$(a!V;3~`zw&Pbr-l8u
zvD-rT&m(pZ3ya;Re{TG0;PeT&|ML#J-~XFC|04oh*#8^J|0g1_*3*dpD&YUQb_qD{
z2R;=9{|o`Z_A9`}%Zoq=iipU{$}20Yt84ArMIcz)+8%ImadV?keEj{71_y_QojrSj
zN{yk@latf3vvYH=-nda*eDh{yZSC!bhNh+#2BWj{@zbaM{X;_|Z{ECrKQTQ$_vOpd
z((1;>))ouoWk({|nrmxlVX?5L-@@sl6`*)q0K#O;(TV2&_WQyAZ?CNN0wN#?jyIAD
z0B~>?76AYP?S!BKu$-gH)4>btf6AtE(PiS7aKoS{x!Zr;UfFco$!>flk2#DTy`S-Q
z(E`n@tj7t!d8KLtpVqAW>cy_YKHsaqp}qg0_Pd<x<rNNgeK8J4re$&f>Fp0UJ#Ot^
zi~P6e?DD#nir?(P2aA5u^!F5<Ic*NsmUq9(HATyqe|vy6))KF!rl@fI%9>nGmWny2
z;{s7jpqN`ULBWLeB`;XCd<S;!<!28MHo?!F(4t+K`;L!!eUDZK_oi7~*+AptN-i-4
zRCu{eHcv-vU3xkCl-(07>yh`Gb)C|Lnul~xpIbfc*m&e>@xaZwI|5xL5jZL!MDsYg
zee3-#)>bRKH;c8|e6FnO<AcV})21qZ6;?$rOjx5ctc^NG>#xUPRi*9^F_k7Q<aMZz
z%S%dI5F~6R6+7SevsjwQyQO*i^!9yG9QOFtdr;zXoU)FTD!llyyl_m3a)}k!Y=~81
zmb4O}z<<8-xWv<sERc>}KCV9-Q;P2t!C?}ifz|7VZFvC=CLiaHT#Q5Nt6km+Tw?uX
z&D>#kWcS{9?;0H|d3pT6#mFF|hyRrHb8g9Gbg4djXo#Au4xu_$-cjAjS|<}<rH)&M
zKSlzgVRE&4696>TJbe;{heZNSEuP)LB__^Z=L8!MERjEIM;g@cT>l)FE!(6#9{epm
zy*Vr5PX2b*x6q)5lisdka~dtOtzgfetgTHOH<(;w&D8^R%igRiVSR1S*yiL>K!B5Y
zx^`{1U+Yy5TQ(+G2A%sXcVG;h4n*{hq`#}>?{%U1UDu}`0*nyXo4DkqzoV>+gs4I$
z`K@c;ei3Z6c7H=Q#=Pb{FTU01ORUu`$ZCrn_}YO^=okFefn9CNWN!6U+4inionAPn
z8u{z~kHO?4UFSkcrcIwpPAkyk^#?nuds*vt#1+Buknk=#boV2LTgu--B@jP+Kmo8w
z<h>S5Cva(!N9Y&*zt!xf#J`QL*7}@VRgFPKn5z2pzIgE&yjK=ox@;$-muMFSB#X7h
zC`YND1Qd7+thmTw5`0}uGFnhz4+G%Uicu<7Yd2-VA!ZOJT*;@yusxWC#X{-|VB35p
z6YfV?F<oF>92~ahM-gMOsX9@QJA=9Ku?46ULBKN&c6NY|tT_IZ7GvkhB?2L%hyn~x
zJC*Z<$=rkxDtkPu0^?B;=19nHhS~|eLO^NNTxcAH2kg6R4U6HxrSZouk{mp3C>}_f
zC?`M`<=?s*2Bhr-m~mV%fQf+uWGsx!oaNJejPJ8aYz`0%=T}RYFkx*GtqU3Y9|y!;
zH3Gm%rcDiK?<S2xNrj+3eirW(7S@Q@AM#+aR!(r>cX3b^pmyS102K@;99wFlfc(zR
zT{<I1Uly4#tT4cV#qZW$*>P+2bYTWIB~Lq9LkEtMOgjlp_<a6ME+n59@VZTvg!9j{
zjdVG6oP6dHXrw2#_bb-}@~akwucaIz0DSTxCXR{*qMk823kPuMSBU%w4wAU>@mj9^
zP#O&aMX5oauzo5TB^TNyqF9>&13Kqe4H5r5*s3vTUD;|_uUdY{t9-kEZgl5X^MLaO
zzXK(whHN}LXVb@6Ggr206)U9E&etcXu(s|T;7~YV5G}F44geUiLuo?XWsimd02Okb
zP-&Hv8yrPxwpPpi^rGpDjhGZ8N6+g9YM~%K7rC~5OeyaUsWSH<jTXzu&piwYr(%6k
zzVU-}D?P~Ncr;v)1Dk>GB-icQc@`06%fXWf*im7YR4Y|A04oM5a8Zlp%i?0K4-i=Z
zSDJ^-Rs;iRyp3w@cLXrJ+tr&sQTHBh^uA|g3UEpQK(Mq52Na+qg`mou5c2_lBose@
zMI$*<3f?iX%H?W0K2eh?7%R&Ui;pJW%PiU<(ohH(S}4rKCzCNoJgEn7!k*V$^z=lW
zrEXWxO^<R=Ou1Y_q<_$KO;4Si-(RJXVjprTU~k!}EF15=Y9c+}Rw_u-%!-$Xl#BoM
z3D#OxE2{!*9|(wKRfwtRsz_+-$^w8+AGi>|wgPPd;1W|D@cS${4I^W{j`L4LC?_df
z+!nOr6(1YeBfjwgiXrbKMWBq_E;7x}BvM4TO>+YR7#Ig&C(a3}ljUNPxoBM{IYb$s
zMmSUzLLo>%7`+V%rzR{W^Wh8uUNT&og9G0Ml`b^nBcJGwpVu>+f{^6WscqMhS)s8o
zH_Y@7(&J2rLlOwRbm`Ko686V4JSud+0N?!PDvLE&I?N7upG<?-xc9OLWvvj9-J2?r
zVy`Yt{J3OH!~wi4Qb7e7=?e}9Dg-7nqMRh0bZ?_#e|2<ns7!K@62FXR>JiD_7~yV#
zA)=F)L`Me?qXWWNvOQE8Xy+g6Ko{+n%Es32$L}$751M@QD(1SyNP^BS$FjVE^7jo6
z`Mqy<)9l@Okx#4u7-lRO08|rV1oE`Pf9j#`)8bO33~u-E<`(eSRrfg|)Zo(637<)g
z5R91EhhUZL2Dix~>kA<t%H*9zF&nfTx%Yu>cQp^Oc)qu?M+25k%#}TntU@XP@OuDn
z@y~nPh3?O-BRbOPZRF~wxkuj@EKxbI14jD6C#7UXy+yf7Uc4%>;#G5fAs`A&&lpWl
z6#!8uY|kHk(_h(AeUDVuSZQsX%%B3)AQ37;fzLb4ZhD$g90yo<YA`h=mQcd7s6w@H
zxZA=#LV>eV$agpX%kC~lye&bjTa`DT?LGA-D3UD~yS%CTXPGtP=WGDxB^sc93(MRA
zLI|!tY7z!$^1*&m-5hdv!(T_Q>YxNN##KUd0caYkv+b}r<_nsEHC^_E%gSJVUz8N$
zw)+IW<vRSZqLEy*Ty-q1nVpx8&_sl#pPe|x<UJf6V6@Nz;hFN%tzI4<-;?D1Fr`JO
zr7yO8YSiPL#=pXV^<?Wtggu^tF1jQS>I-6;Euh>)ZBd2A(34cc+$Y+gLf>NH+pg9+
z?jFu<f1mvG!BWx}mR+O&;MgCCm7;&3k#Ibbf!9DpBeOctuiCLoeMjdfLPXoi1s!$>
z2^mxrkR^$u*f91?+`6PJmhmvoVr(K|a5Fo_kF_-!896tZy36>-z~kL$=D0$N{Ngq$
zY&U+Oa~chMg%id+O$(wvES)uVJw>=zQB}niB>bMNv2H!y3>cFEz|5C}N+N$!`Y2bM
zMe*eG<sgT7=O46aB$NG-$Gu^FXPg{8$EC<Kh7@d{8oz`nVmq`F4N+b?>e5<T>hR_|
z@+T6T{PT6xdP!*ik!P+K-W+Tf$(jr|ACCNQgj?BL*VY14W~z@6sOD|%kHJ~<F(b%*
zkE=79f@+N0UbUyu%C`y@?RNisc}4s2^9?Z#y84NA)U>FY-ybEdnQXGwsl<=`A|@#7
zA>&t8CKInc0zSC%5GmLbXH1!m@rs-I=yz9`*QrpQIkk2`GW8t4s8s?<ydB@!w1Rlh
zzQ~Z~AxXpTkDx-@Op%5!yy-E-?s9U-#|%<j1=h<mI3XOZnmoN&BD26+IrKQv;MU&L
zWAAISSMHhokz(>u^Nj-+LtQLrv~~x|a|P^-x1d$FvXV6QtgnM8+4qD9&o=2#l2fk@
z>eNr{YBlMNq-RP0p=O!3Zo2CNCU#(0pq+*BDDjQ!gxDx536VunB60Yg$)1J~3y0W8
z=nl(^)xL<t7@DuDG6JHhMJ41B!U#Z72fuOshu{a&H0_V&UJ!yXqT-(K>jxjyN{;6B
z?9t5p%gAr-pII&reb>wHK%0bgLn2~VY(1&;S<19(uEN(H=9DrrpCz$YNb>B5o59br
za;!tQ{~`Wy@tRRNfvxQISnZ3bCwoSp+&77Oy+4Rs1~EWUnqQ}&KgBDC=NrBNSGue3
zAdxoI0-$iuYpRx<Hk3#EJ1xQ#b4NmU&+2&5-9`6(Q`d9|cuX+8qUf1ivE$I$)V^Jo
z7xh{)A{GQ~2!q4pxtstl3w7QW>b&@zdQ>AO2zTEXSrT+ArvQ5QtGQL=?<|ll-&WZ_
zsuVN;gzdg|9{|MAGtPiKe@N0JUT%wCt&$I%hv@Y41`w;u<@XbPv=st7p3r;pP?)<4
zLqU_mMBBN#K}Yc`umkSmXLJE@4mccSNf1<Gu%ewAc83rTwTKp&e$f0Vs_w$ot9K$<
zHK#m4mdjg~@1BHz>iupK#Fro7rGp2sH<E(!fHbVA;5a2tUJ0v=BzyBxnJoZUIR480
zX($y%CUPEIbpwcGXvJ8w&*yHsQvA4~4u+zK1MUm+W_}aU@LurHmOP;`CEZl@A`$Ar
zS`Q}vpotiwZ1#-4&NY^6^6*0JfGbe6YZv6nFF3t7;}b}T`?zf>bkBMvS;KwOi9ELu
zS#bP)ne82{j|O|*f`YC^jC>yI9F@^nS-u(E=2a1GwKV2Vb&P%2qQwyh2w~=R78U9o
zs~?hvA9BChFdXR1iWv~2DnayQxi|(ao}3}@o^Z`mGJ106Oqs`vD0n0Y`G1yS*4)FF
z7p}dzHd1onzylQ>{(bdMpeqAVAj)7)lnd0SBDoS*t9%E7+krzui{-sO>H8)fkkZoD
z+K`eLZ!^@=mKf4<Hqmhv$V8IMrQp$Ac2BTq*c~C-CoQ;e9gc!UxClVn@LDL6fHWV6
zO&Wv%pkDZL>5J2-F?b5v<rXa=3GfU91UYH3cr=-TbYaKrB|w-TwRio2)hok&_cuw<
zUb~Wb`G${Fxpc$HmAV0dtOiGvE5kUcrVUU$FpKv$)@FDM&7hJkxVCZ7Xf(n%A-NvJ
zQOU5=(6(STF*z_TY;60ShI<afo<+vkM!i=FNs)wD^X2-Ocgqv-B`!v}QixeDbsyTh
zlEc<={pz>Rm!0UnlmoBuXFV^u{7Y%;>}+T5bl~v8rpJ-2EqhC@0sz3{wZ!t!zfv58
z3<PZ_*(=zU)y3SAY|R57z<wlc!?(2Q2_!qjAiIc;q^O=3-9$~25KyiMLnyX)GNk!>
zr8&CW?xPqZ8XQOfQ9#!A#xE-Z-|;?iz|i8Z9gIiP90+O6`=|~Ed4P*whCzlxq5ydY
zN_<<lk2wZSZRD0Uj(VM7+bxHd4layy!ObwGCy*_6n3vxYM0f9aIvqRlHK85lb+L^2
zy1}4zx%IjEjoC=>1sF^Ta>kbDSf7@RG92L72roc$6DScnwXG<UwYg-F{|pUqk-XW)
zz+szPXLR`K0XS|!M;+NA$j(c*sPJ7!ZI4HkAV&vz`1|bulzNrg&rfO48zSCSMg;)*
zj!vp{@h-&;9U8v~;j5I^JG2>;VT8K~G1JQ=Xobm82sT(VZZkOJ2bRJUtP@xbX=M7^
z*`YQ77>!!k%fUw_Q*u%GFfJ7iM6gRPj}Yg>*fc1FiK<3&$w885BBS{d)9#5-&UKwV
z37{@IN_xqYKcwiU;&$ecl5K4)h~Ip(_~$g6Xg|Qg{x0iBpMlLaD|E(A((qAju&oM?
z59-{W%N{2x@siLD10YxF%9Em80;&V}Ss&tr@)XocEjPSk5hs@cQAHU*U9Vubp;F?W
zMBz<PI=r`OXZN72cqL<IyskPH7I%bmyyeQ}uHu7QX8e%4ZS$_DQ!QD~&Mt}}$Luwh
zJZ^j8rj8P?j#^XiV99sQl?qW-?OIRdOeD)A=d=l5r1K5c%rC!$-P~U2@%mVi_Ko$#
zWL9NHiVY|qgwxD)L5(dbha|2@$Cxh^Ohk7d+a-@+zQrC2@pAl{tF;@@=Jo!7OOnro
ze&5i3$WsTrMTx|QCy<A7`RVql%Bd*Rn6|d<S5Vd!eb-d&yW*qjpXKU3fegYPS=mZg
zTht4vh9<*+I$1KF$?$bCuqmaIM|z8<+IbG|ba?UoOIr~hu_qFi1XH|ZJXf$}!tZJH
z$n;Hx*B-M-e|}nn?d6ldb-fup_j)tC_SV`%CK3XOL#VkB2e#}0AnAYq4%yD?D__!i
za?i)l)m%5V(Z%~LpqW`%)O`lSC4?i*o1pSWaAa}c4m?vX_O?y!*@41c1tkZyMvaqh
z9AixwocYZKyjB%}n0Q?+&6QVdbc7t^{4k!HEYC%MN<7rNqaFUD#2}d#yYZN*ms~1P
z@VHJTStMEX5ulAwiEi;m_wF~vwvh#U-5{{GU`(pr)L6$eEw#>$<Yj5}yo@lfnwEpM
z#2H)^mH>QB!>VLfw;??Z)h7ywJr@is3lhv2CtR`7-`DROiuAA1?~JiL1cVpd5tWH*
zck?^jT=gp6h8>fPsEqti;NdZ_Q4X-y>%2b#oNss796v_}$ey3Zo;|D+M_<)!TA)IF
zlL8*>PQ#}OiOzf~y7z0s6}WbBkulR@=!tm$yj(jKw^xTrC2H;4_+gGx@LmCzjqXl*
zY8JxgWH-ulXJ%p%&RuWq7;iN{+(I$}%B)TBV`U;4QbD!P$N7pI0Klkk5#Y~;^fJyr
zFL<T{biD0VHXMoux-6931dx2OkK<D(gol@+=6GqtG-pV<J&#)NJ>fG-r<=X_O?M2*
zwf;MpIo#qK32t!jRAGY&NF%M9io~x-`2V!e9L&YfHi-Zk$=O11lFqR)PTf&@`QXW?
z9|%jKA|rY>#01sjfya&Z+L6u`1csf@$eh#i0P_H!+W}!;4<iA9_v6@GG)@PJ^gnNZ
zg;&7X$7MI!G)zSfn27-%ieJ9@!cnYTqt)E;)Pul%Nh#hJC#-{64TGTLBnTA15m=QC
zx|66YcC;XFHRZN9|IwgE3kAR{t6~N)q$tJM&7?zio~?JBwBP>@*h>2jKj@0PfjyJZ
z<RtX54YaGycsm#u4)7Ue0O}_TMLTKemb&hON$x6yxEMA=!Qaz>DxeFGo<p0;_%1$|
zd70tqAZ)KMG6Y#Xu4-63Bau$P9kMAiN>jK(Hfm)pU$*t*7E*_BoB#kaic11RI)XwN
zry}CH5m1L@Z74Ni6&`@KTU6uZ;^txE^%#T6j9|suw5T#p!160o6j{Cdh>BpteD~DX
zME|s5_jz6CvGBUev43Zv#Maa7w#ztZ^}L;*zdH$e1hYK=^a0>WY^Q8?hq9UHHS4w<
zi0~^2I~`f037=N%e|ZcI-|yY5x^Nc)*SSQf!QN64%uwdPn<M>3_=KtKdoI_evgSZx
zlyG}NDxt%k%QyrGynaY%fl3g7;_)!yX`Z68*_HZ_2dfL~YeoWeCh%!f5O6`<`o)|R
zVm(>?O9sm%{J0J>S%6XACw&qjGXfgC9)ICwWWPU|w9Sgd@HS07#dCMqO^cM!!)!zO
z^ol|6w!t9?izm!B9WQ}!(sVM(js!!Ws#Z$oB2IJdQV>$(MOmS!Sm_>T)kJSOwT8Vu
z&r+0J6v`O@jSB8!i9?lyzpzRrOqSkyv1)fry7=k+cwb(8;N?a|R73T(UpiYGWo<BW
znTr!9EY?*gS|R|*?E?VVKCFDGEo!&8cG_Ouo1sX!QY#0)z!hjJ7nGfjfV+b<`QC>i
z1#ov__+)JhgmlzT|7SH7+ArdJ&!`?wvNG%foU3M*AE5nLV^nQI&!5j`h#$r&r<Uwb
z@3GPl`V+6VJ}$7n^EW}}7ywFClrlA2XB~GiGJjkn@MCx!pT#L<LC$2Hh1LwZ4JsnS
z-%FOzw3&|EyC(P4DnQ?nu~fU`SYly{>}j=1m%NFQe|nq$wS%qOE(eFJ3tkNn5G@N#
zAVVDQKR6SirsWqcd`buvW5vh+D%|<OV?o9-+)gv4z`nzioUMI&*Msqr(()ArzJ<eq
zs5k)tS@HakF+y64ir#a(*5-<xG7QbjrG{BhfLT2<*V5d3z*94-_|vqz&9^;!orN@-
zG#Z9q3_Wi+w?|*tEcaFO7>o5dctsrYX&gXB*fPM+3<!i_Z)}(Bgd$Tfx&)53K(0J9
z3akF`sL3C%XmOV}^RX5r_Lh3fOReSCr3%KCho2Y^1OrhvufEOzfEJ$QRfO!*FeRuE
zvov<Tr?khTps#7i${(*z)EqaHIKFp=uf_3gcFaEZ1ocCNGbMTsf|l*v89wyc5Mr6{
zv$KZWF)lCUd#Xie!{9@aHMqg^DY~Fw1`A32@{aQpMEU!++9+?*PqZ)h6}39bNrjor
zi-s?+ldjmFxk9lm&wpaJGnJXS`zzvVvyIFxpQ=PghJ5S4`UZ1|m*%ak<~lH0!K@XI
z$8ic}BK;y@cd;L=ljFFKOfw0A@||jKP63Ty?F~=0$taAEA??V{1<~g6ov+T3Tg+Lk
z3BvyWz?M(V?_Y9aK+{JyL9Nk*3-98!s%X8EdWj-jXS&P)-iqdZIg+0bNZk@#+NQWS
z)h!LX=$Hb55MSi;NY^OEZUdk@<zIJ_f$iyVOVZ;ci6S{)adCd=!#;npgD8M0r^-V=
zb>IPNKs{hFCco(?J|q3~j-x3yqEnzs#t8`h--CL5{@z-e78S5)?lU*Phde14P}&YS
zV>_>fyia)U@Z<9I=@O|Y+ka@S#{A@6eR6+K(FvZ*%bk@NjIL;cz;6)NL@VnB8h}F6
z_Qxe9rp<gqvqR4cMCb$RX^<<q+i1%ALcqtp)>IFb8lPB-db*G9&1{8wK`1&^;Ixib
zVa(m6oVyRHyPgxT$_3JUxW)l9blihWEzPS@n}I*FIfIfSx6XP+{>N@qwsmbS8&uo+
z1+Z!)!0p4-uVGE6Os7+VcT1zfIff+9>c2Vc0uRC!cA8z0<nKCpk!P#FtH{Zc`28t5
z*G)m>kKmwvfNzj!71u5*fDo0A>7w)=A>H@ImT=)@5N3AtS^J9_)phFJtzj{3*h(ki
z)UrJ*RGY>(Tx-5aHOw2es-vJjS-jC49{)jZ-B=i!XaQ(A_f-U{Pa~hX<krwbW?n6@
zJ}sRa{$k!BX|Rs&&q2;8DvS<O^tx5DJM+N<&7RM)2LpRX=^zUG6*5}rEoyJjej7bo
zZ)@9ts59^w57iXP4mp*?Qlp!<z5inceyCx6rUHQQ7y;lEN_YP5t|L3&_YVNGkxDbk
za}09M#lH6MmR5a(9eB5Z+P!drcW~FqPRsA=d!~(#%Clbnf%(jG@1cUT=WNmfqLm-c
z7Cb9>w>ndma2Ftm2y1E0ihaHUlN1M(OW@Q2jvmVcwLVid#2-T@rE8BohVg~?)9*ns
zvgD|+s`hlwm$_$|CAN25@}5Fhg!RG{<LLL&6@z|so1N}vCha2MyZQ7&Lz#4??e&A-
z+P?gefyEjk(P`8q9E))gF?u+CRQ{rBu0z_XS+W4Zifon9-(c3bd9sbcC8X!95L9tZ
zIKcWu#PlhN3SBoqYzMQV@L!7)wvy<X+mGu$;O)Bq7$zJ3g0i!EHfCHVNl~}DE<YOK
zqT-)#QIh34?8DeweI`e*{*~x6=VJy0T;Qh^lB=GxlA!FJTT*(<s?0=uL?mclk79g$
z`*;T0`CX7!FBAF0E4frF-r*XHznmMXIhxDKz;-J@x_Y{-zv|gLYgQ;99i1kk{zQJ(
zzUu%__9hd|<9!gWHK+&z493e5t2%4<iJPYGDs*P&?&tR=bKL?xl{%m+kJK{5e+pVs
z$DF%GKnlloDq5O{oUCnwmM9j4Qv@|aNy@a+eViDbXU!?Y??R)eiXnwVR-R7#24PaB
zPNi?I%>K+si~pn26{|Vc8WIK^{7#jjAk&Lp3O)C!t}>@O5>D9=lpOf-MZVT$e9mKk
zcbS7|^`rKlld`t5Zh!3_q^834ilR9`uEgv}@)@jFJF0RiMbp0)el6(ZOcTbFFY*r$
z))woL6O#(L3@&Z}UZ))qjBrX{h)RKf-bKr#CMc&6riDM<-G}S47c8nD=8JtAotRR1
zx!ygX!pyXx@X{fRdy{{#MsBcHP!NEYWxNUiJO)~p8@C6;(nQ0Ht2{m1LNk;IVHJwI
z#GLEb_}!|~)^xG=>@UBQh|Pj@Vm$s}uvp(f;!7U)c`l-nlal%!>s+za`0+VjD#D_0
zhn?$$-4t@(-)C@jliPg1W?t~mlaI?NYR?MT*7JcTY$(wa<YIk)7qsGnm$e{|i-*em
ztRi|n(dX3VQiZn<4w?O7wBpQEvQWOHmb}fMjPmp?r-T4eo*&+vq&^CR5NeKSDBRZz
z5Kh`SjHkdV-)Jd3BBEbcQek}OPT&FH5MZycsxL@~szp%Ne4z>`NS&~XySRq#wtpTa
z4QM2Xi(}+2k<?`*atsVzpKd$U<X8UT>D$oO-^`bkN=9GK|1=8*=&8p*y`u^gU|jyp
zxUdnOCfxxo2)Q+}{i{^li|>+>eyOtD&xEh=Y{VRQ2=^Jy+pic=uKpW`wbtCq>R|u^
z0g((i(efUAcZe508u7f-iEg^Max^>ZvN-g(a+t>XrDOUs&pYJ99Bs1BB8fif;zfB^
zjjw@kg%|nQikpYvTqSM5K>y0jw8DcjeGne(wX=p2jqc)A{23=U{F~qBcMsbiL>Om1
z!2{^SEC6p7e>sodj?~t9u4Xn;&x<K@tos?^wOjSp%j=>u6%!(D8%M)E<3A%irRk>q
zzd=|VHB<mO^o0rlN4SLFOT+o~Obg+kA8e=w8sR3F;&;ryvn-tR)hC&D>z0%e!kmbQ
z#UdA~-c1$#2{tpccz|&DIOt<AVO6Ye`44<bG37Z9Yu^({4OEzT)(|Om;bOEWZxq7z
zz3gFrkL+)uW$lr_1=xCg=GNBSPfiF@%ej<ARm~7Se7j0zm$Co^I@v3pCZI~~l1Ne5
zbc}U6!aSJNj<@m;IKQo|*8c52&&P5v9D_Ii0|FJdL=%oiS+f;q-kx9^gxJQz4B1g#
zV#i$LDLqxK5XSC#XNy&v2B~=eC`Q2@xF$;1YahThc_NR?m^+^oa(R93;%==W_#hu;
zb)tG#aCP?QnY-4aGLirB0Mj#bu>xpj>j}_IE>F$|DVm}`QPU)Sw8Q!|9$<HItsS$)
z?(ADK)#oh^!BJp(>i4zzFFhakej?VRmD=9=2gZiA<z;glP`Je!008A##h@iM#-g`$
zoxSYq8C?Ha7*Y~d8a(y(&haY87=yF$EHZIeUYO5$q|}J7X1nXyZ=6r>V?KhZ0v&}(
z_^}EtsQ2scxmE(86GQ+eJq~00_C%DxWp_Pd30?e#boq`<%fx)cCVBG-_f9=t>+Qcm
zK*cRUa7YQ8AWfTcv#11HFSTv(8IPUw4-*Skcs~fmmnO5eUKZdTj6P*^(m8c3uy`9$
zsAuM$E|#dOW&+ezh9;_-^gf}j>;TWbdVH?>_JP+yd`8Dxe=}eOt+!(ENN8A`2N+Bw
z^s@v5^sbKk(4=$^&PeMPT&D|*^PH!^L(IAESBbha<c5m+&1UYJqss*BMBi^`ULpNO
z3N&FA5=lr~aT5Rl%B}SYwWK;prdZ7B3;gYbaa1v%`=G_4O2wX-TUvxyEyry%ouF@=
zxyXM^6%=Vr4|soToyiY*JyZ~AkxUd+b05DPqYwrUZ00ejd{M{C<c`gT0%nj6*KF%^
zLuR%SkEe^R{;ol+t!^yZROf6O+f~0oa5j=XL4vsZ)~0#3eu$miOHL})btEPWR9wD7
zn7H^jmw7)jL5wr?C9JQ4Z{$RRe6eHOK73*RQ%ljd#$b1MQoP5p@A+#H1+?NZ7U+fi
zLdj1B?udGK6E~4jGMsR@zI2S}j?<c|vM~F5VKb7B82&CKCZ%@J=6*z(q|e2KOgv1;
zlD)QyoR#L#guN(*qQz#)KQDeO%HddsvUvAoryh{(iT+s_zH74Qed$%0{&~WUKXU`R
zkC#87aB6)KVVW$w!k6RX1)w0A0a1h_mk>=D%Ij`9yVm{rVkp(yLsN313J=2uH}-xG
zl~~BW^9SRt2Wx2u+QP^EJ_oir&t=x3i_nqi$#H%+=6m6YB+5|&V#I$sOHU7b*2p;Z
zs3CiW{7%E|L)o0;(ShTm@;oTBPH^ReJFCa#$(_84;qj&JWsiAQE6CNq&P)B`^DgUC
z<<phfu0S%tyt4`F$#v>NSpSP5LqAH8`ek$8!uoZCN7_1{Z<o)7B-oTiiQod^&(ohr
z$vLw|^@PwXANEE5C-3s?odXOVw=X}2cA}vIrYE+8V7jG<91b)Q@~rOL5926GHfP-X
z?RDulJMu9)Hr#xsbfuPo{7WTLeF?4qfq?JoN!Pxx205TZ$H#y?a>1dB28w80GX$D;
zT~nD-IC5Xt`NoCC(4J^ddt9IB;o~D20fzl+7T3p_4%>fWfY$SYCe$s~GpA>@W@?(?
z;WKRwPot=SN+=ZI^<GX^(D~JG(__VeWM#)$$h>WLRc=Jc0EmjaE_TY0Ff3B1Wu#mz
z7g}%~-7O-=flj;K|Fg5|)qbgjzxKX*JCapjwT(>6Xa%=VfaG$y+VI?9gtB$8sEoq_
zc~jIqYD}uPMi;%-FThFCB1&%??c!8@<U^k;l=<#UzX!{TICgkZ!u;Aj3_wC=7qZQi
zppr2K<*!a0Ov>lrqL5Xq>O|e-6qZAkKb1T5m){Y!nyG-kcV6B5b+6Za#p9>lf1p?_
zzn4*}+J0+*gbgr)5Jgk(iIWcB_clE=`8Xqi^OY837f+U8fxzM2rWAz1rv%y$xVbe?
z>u>r|w%dJ%;5;Or1nF7Lm_idk>p1{71l?|2o&rl$)W2LRy)?Yl`o0?qP_z%O0ytjQ
zp=sxt)R4qv_@;*LVmMckA?KbOfjCd)4i=Ybpy~D=h39e)#mjQNCpYZ-5B%n|Wqn~M
zN&MiCuTP8NMQiLaiwDV7pI%K-db#&e;>#{a1tqC@@#(6)dJP(b@!^_Qy;r)UdJk&f
z;99TlUC_w+jl(_w7IC1g8;;036ba73F~(8vC&==iI{}l>FbL1(3O(<8nfF}nc)T<1
zLPU9OY{H8hnMp+*`e~H=7+&qYjjND6A)^XG7ljU@&LkK5XEHQdfMbsgPu2cABFa;W
zT26j8QQnE9yp7XquGy1;r?Cne{lA$HZ++Qfk&vjuq87H|79qkG;!lDEZOSb$3H!l#
z1=lg(QLUvWr^HwCFkkKNdBEFxHG}ZTmjU5RemsKFB8McxUjyn5H#_y7KN(4IEU5R2
zF;Cas98uO&yY-t1YtB58wUr%QsNm>&Z4n$|0-ARhT#rw08AiV&P23?QqH#K@7=8=D
zsFbyx3?US%mR(O#^ENr-qEs6B8)Q==a=jVcQ?|0Gkp*=fW|z3pB=Pi_x9DX(D^Kea
zR}1Kp-340S#Io1lk7MpWK8)F0_T!sMuI-KA0IZi@ysUMmm96Fq1E!#9jyl<4&#NUZ
zc+)C<M{c9SdErdVMJGs#O}NmV`{o+F??dXV7KtHc4{QD>wwK!0AWn8S;Q|>7F|h*w
zP_Od{rS`EEw?GqCLfNnby^d$V2@3s`)#5>wc^^dzcY=>mOf6zH`KyrRHmb`CcjZ3_
zNb?-Fy?a!O(Dc1sU-W_1k7Sd@n?bFA1YL@}Ng_iVf^kZjXD%_ojT%1O6L<&45g7$*
zzi8p;CH&p;E@x%+VG^nfZmV!bS?)U#8Xiix%b@%{BFaiV7S$QK0VZ8K7)LJHz(B`D
zYo~SOW8w#iPc(G%Eh&5L*XOt_yD^b`)=NcBtE;Jh74@T(%5dPH;DaVYwkx!VGtP6M
z2bCCC;XKB|lQSd~VyK2EL_F*S@mNB5h6qbzK^PNL;xP6{{2A7|6F$M->Ej}Yyux@W
zTt*t5(aWW>rdgF6qpp6|TvlAT;9VlN59|l-TF`LY#cbA9M9K-C*83?tqv4%Kiq%HR
z6d=L-XPjcMqVb{4+Y$5cczBDT@~6(S)xl|_BK{d+tu=VQ>5-?7Vxq08-_l5q$I0rA
zO0DyYvdZ?7W|alXR^0x!q(3q(vDOe&WVhrbXnx!!778J{E8d7O9u*xqZyR)jG1S@l
zN`Yi*wu<dtDc@QZx}VY2^VaTvs5fscM=;?}oG|hmP>stuWJcQ#9&~+x7LOcK%Dy~I
zU+k==wx*^))Zollg&NRFOJ5yDF*%a<`~g@hr(IR;=wm`T3RSuQBjZj<Qn13|<)g<H
zH&ZT2vI3IZcn{3h$26pc8DGqGYY@3){3%#S-ZUo(94;J&-^1`_=%5ui?KM-hRM0v#
z>=kmILuggVW*wnT-_vewONk4GBdt|>+a}O0iHZ;7cwXEDTDMnN(k^;qZRAex8{|o6
z##($o^S$K#sQ7QO)>`N0w%*Pb5@G2)QbquHriJ0~mH<t=50~?kfRv)Y2d3Cm8o?<J
zcH6@0eMb56Oh{$|kK7kZxBo_9{d^RuZ>2EJ7~DpckgJ7TYrwxdkr2+X4E_|3IpYZt
z<A>3d(CD@}%-tc($DiSsHv|M_|7?b_Hg3FR6*Ay5_MRpHQd#xKqGvxfmVY2%2>xay
zQ|w}lhE;fJT%)p6yR}ZwRc}!wGDA;CVeB_^Pzfs#VW^<Zy5L@v2^35dCat+!_V1j>
zl9*lbJ`dwdJHd3Wl4|C4hJK9-eiVagq&*XNiPBJ9!=iP0MEEQ6G`{C#cJtxJWCG*A
zII8)G>>28Ge6c;~HwW;#d1U~I%@JbT4VS2uM6hamosxl7&bD2S+S`dgNK?vB0Yz(G
z138;0iA?^q`Oz!hmbnkme?#AT<d@>F^DvSCvL+bZJ)cm5U%G~WTaas(yhH2rQ+v1P
zR=b8zolm(`MBM?w;BdO!^lf>9&te2`{E=a~V*f2g3YSeyW8FU2WGJk%3VDr2cx3uP
zHnHmo+l9VBgm;NsuWQ=qnP5N{n)Tv-XRk7k?InNa_PG$BlBFqyN;!6{U=QKct5a?n
z^}3x{staRj>GYdBSOKZ_IC-qLOHw%iNZ%WlCb}tIDMh9TW)bj+#>dnbuFjHEg+njw
zZT6{PBsdYLMqZfJh+F;<jrDeZ^#>I1;~Wnjm;rW5dixZq{3BoK49s>NNY~EOnZY`I
zoGZv`uP4>)QhvedWaiiZcU<6J;B;Nl%QuWSlAW=nn$E8M_S7eND9L?K>s-&zzjUg-
zL5Ivr6)wyxCCG^`t-brRX0f)eH?M2?VMTmS$FBlbCs(=viTyg4O3f{L-NIb#oFPb;
z-8e4m?KWDn7?HTtq?p>j5&SinUev}A(FfTs^b>-E3iO2Qo%J`vH0`HEpW?wA$xV&R
zF&gNK;c@C+ekq{)DVE4bM!{1!C-4-)ysBw^z$TV=O8q{jS2brJaWY??Lw%<npUjEh
zI$39>uq-F$FDNVOjM;%7zG1PJF0}4s;;Rc{ZwCwZYkR-fs9E2sZl|!Nb|?DPb>Xq!
zgy41kuBV&ui3Y$?j<h;E;{WLtm3N|D%Fa!@)&>6kc=y1pgeHz$^qME9wqq}kYUE$a
z<D;YN2P$GD;RDn+@>J*=G&YlynF0wRKI1stO{|kWYIu*Cb-e=ZIl3=RuC@=qdE-A?
z(0b+}!Z1PWbwTUyJ+wRzKS4h3G!w;YEe-T67ic}ViNAVoC1!iu`O(?YS834P?3s!)
zRtQFNxpmgg^wA9prsyho^59qm4)P81bGc!q@Y9#Pw^3M}IGq<X3x2wijsQh3%l;pH
ze4h*TWR$#TiRR(GrthyU_Tg<bJF`dr=cpIxu*m5WboIbnK>q0sLZyhbWuYttDCRA(
z&gd5H)feRnE*hDC5H#A8{p#AYOxU<~kxk@(g8?t0r*xC|Y3+mi$MoAX0H-ICn$oD-
zgOHzU0+Je`EyTToN2Vq5b2SoX_NRMdOskJMeV>qi;07*|f>0hirqN6_e=x`N&HaK%
z>4UY-QG-GJ!wSj+S0MEH%kTFKuN+XmtGNANW}x+KLWDL2ZO#d9qwk_M*?RHtb9Wla
zgDV#>3TG=$6zsU!J67{*q>yy3Lqer2zZ@2m7%W8v*YlEION_55Kkg$8)*BpF{P;|_
z`SHX>{ewwyH}#Yr7&JEh%jDmBBQqm6O2_ah&?QmKwBgt3NEaG)a}h$$?v#gNoMaAb
zOM9QF2@~g2<sApNR<T3d{}OK{MUdfXBjPAkWTYaI*L2D?(8~B59kIowwMmjvLRe)&
z6hhQ;_LKOJ29_%SrDLsyg{fvaLM;<6f#T}eSQgxV!7u6}X~t(R3AkPDko7h?tp^@b
zA%n-Sgy>)ZMo|p($3YijhQhICit|h_W;s{sW_+omp%{I&zWK+uq9qh^F!DEtP2+yn
zAe76uJ$)N!AnLeRVHSSK7z7eq8}2)vF<U%2J3?`JT{LTH_wbzH?ugGhg((?$j=f~7
z>nH1Y^O`N|;;*OaQ8C?6>1#soFnlNP?-a^5f6Z<5$<%WGH;=7b7kyIV@fD6&+mtn?
zXuU1ieK8poUzCB6qf&L{Pl|jFDL`#;(kK}bI7)aX3GjKEn6zAa%KELr_p#p?tgRd7
zqiYLMVR+y8b9gWol)IC;gzmuei{Y1*IjzM@`v=)d*r4G|1g(Pf$OC$9dHX5z6z(kh
z9rLJQ@?>0Tczj-`_j|tu!K7NhyBQ)V_x4&rT&`2ad*MG!SnC`1({;m(rqrmwXa3-q
zQaxE^J^aBi$&+Kr>RBc^fJrbaQ(LmFl}ITVn;86f^txB;{}B7Gew%kbO_Lam!zoJZ
z6EH2pP<~0CH;(1w(0+86qT*eRP`PQprlV(?Sz}+8S^b+oekC?rW9#b=jT~~mzN)WM
zn&z?!5O`u2-5)^K@*Yq+biVuPR*5t8JoIpAx{g5Rq0?DeojJ>zySX+0GG;9g58Is)
zwu}n|&sva)Uui;P?4_7urP>DWN>)*<ri^5&$D{G%TvN+*bt1UO4A%+zkc(&mQtu|#
zC6%RxIO_xh?#cu^FKFU@71hhTyD)tO>bd{n@YW}R7%zT!6ylUV?EzXM^nlS|I#ONk
z3BWIvzqiI{+Y#X^l_L^%NU{C))_%1+hn^1CUHVJE9=Ua2r2L*NVBozP5QkKjR~E{?
znM(wOrUHXxgd49<=Nh?223%>>ssHDU;B>3U!$?-x!>wCmD-Qpx*S!AWn|w;{pd~p#
z!IGS``!ycOQ_9pydj1t|nVUL5g>R#)*iw7<2>H$kJjoj<SO<@Of=e+6PDJ3zIl;Yj
zLqeVjWqvpJ>@Yf+>PltuDEy2`g!yd0*7{ez8X3*|=tc9!SVwi6d8_R!1E4chMIoCY
zm_Ri-5oke)`Ws#2P-WqK8aF9r<#dm=ql~hf*WdkQ*23~miIB%e=piZDZSZ{-@cDXB
zNhTkhDjAd;w?(b8^sLDaC#e%t$OH8^q7u9zz6<1#72Hdczalh@s+_Dg?C4SA2=#%S
zxqJ}-zF=JOkvmk#K#|0kW3fs*q?9FD0-ag+5500ZJ<}!3$D!`=hkUcekF_#g7w6i-
zg~PtKQ$AV;P{NhNyzw!~>W*3A5|=qTiaMc7Xg7^d<$s7+Tcb~E%dQ*D*d(MaeES6c
zC=RrqEt;^fK=-<fmdhSfYY)nV54>VbG8s6?SdX33*}!4})9gzK0sia75mE+@mC6{t
z4#T7378%|$He!%}KE1ju?Fi4?{+9(NaWG7h_JT9yD~0?v%DTQvn+)o<-}=Dp){h*k
ze-v)?_BpEHYVE|-ii%3M8RF$%HDm`w{-tO6zH^B+wQ5Uv;B`g%R5SFvihu(3=<^i|
z*{RADj=e+V7w!ERW#G-(x<A_itc}`*gg0Wc_<Wi}FZa{_60zg;)T515nAE)9NXym2
zLaUhLqEJ3-H813Rm(kPF`Ha0V5_b7d)CH~QGenph(H$hXjf$7c+fJEx;8xI7`8Xa;
zo{lZiij#=}<y*_QG``2zQvYJQwz-p@`J4#=RPKWv1=IU4hh(AyB46=Z>ALuM+duN*
zVQd}@E7|>{da>c)_A&O*-aK9245+N>eZ1$9iV~4ievg4<eD2AZanHcf8c;hbu)HEX
zw=%}sg~w2h*Dud&N>H1a<>zM|gTfnr$B*2NWP|)F-?o2tReE@D5OLDa+U52Pq>wP~
zESwUk4{<a^asnt<x2Kp-$!lKfdOQlhNx;RhasA!mgB<3XMM6wEU1NY1rT5Pp07eiT
z_nJ;D+kD>57rZwSDvynS?7;OpeOfIgKq=d&>+oNI2hDHK?A1TmN%T^E$+PVZ+gtEa
z;HPiUdZLBJ1gwGtMUeXA0VZAuKJF=f>16Ct{9J5_E}{ilf5$g1Ih&{GZJDPw=W6B)
zDf?M{w;y!L<M1e6N>b||7QSy2&s4>y-PS`6ja{@Yk!c9#KhYNL;5Cdv>JM>1m?j?t
zc;}a$jVrC*W*j$${h?#Al*%+<JT0yv%(@XpM+Hm{5hpyis)wV%om0>i%<4jl__qO1
z)t)1F*KT!Z@kj2=f13Z;;}`v=RX~`cb9^bP_b1P%>52iZ%R+`w<s;NrFEU_A;WQg6
z9Zf2syVeaCG$N=)E<dS<p70X83Jfjz*|B)*#d6Ro74XM);Ff8$uztA3Bp~+901_Z$
zCJ5&qP?t?MQeu3H^FBp4WF!=t7TcU5!Yd4fa)`mT1`)x;gH~~1j>=m?zdKQo0*{lk
z54y%A2<%mt@mzj)S=s;jw=9aRZ%4oU{|waYPGf4r5nj18A<2fMs&-z<y660U_M8Gz
zXrB74zDrSr{c@+J+`~l-z1D7D{*P{GZu82WD~aambC<!dC=L$Cngxb<tRiT~2o72i
zc6E>60PtOX_bh5jtC@oeE!0f-z<3tnZCB`Dw(DX0gnc@A1Y$YgXw)$|u!>THKr$^p
z|8oKc-DCH;_qp{|NG3Y~Pe*HQvR%jqt>2crq%8=8tBgv4apVbii@ZIS_b0WcU7a4X
z!xU-5ClL7;|4w69?I0!OQ*gD$w^O%~fhh%?kWbRw1{2bFPM*U)`a}yKH7eznLT8V-
zO*JEIZOiB3cfnuehl>uoN*+nxt4J%`=%(BD($7TQFLaC}Xd6U7hyg8BNZ3xU9{$Q7
zqcSPX*V&tICsb|Qc)<HYTV?Lcm_fgDCVz!kn5j+7knPjcZa17&6w=l4w~_wB2Nk~>
z-BnTtfgOp1JLq83HvWEaW*v#MDVn@>_MEBcEOJ3{M4^aguFa>D>#tFT#|fLpw@{~s
z?XGHex}R|Q;M;%F?9BH6c->?T&JAQ$>F#~0{d}%MT7+#q-=ekV_^d-wXUPOjs~2SG
zKivMhp!KNYr{m-Y<7=#-m}iX&&pF_=1$R_L^kMMk%V~F|EZ6Iz-ao2#h1Kqya;2O7
zk49&1_UN=Nre!DC3$K)vXF2+&Oz$d0SINbS!aEIcrk76znx?BdoT$IM34wgC;P?D3
z8e77m+~LNsoCeh@>=`!~FIcy$AQ&YG^-}>p{was)4MDbO3!~y<avR3fIg;J~SNU<K
zV*R=5gM~aGgYVUr=_{1*d5h9yCsPJQ>2O$h?Ty`Hj}S6N5+$?2@BTNtUUHv9I$|qy
zt|HmiGs}UKJN-x=bRqrmJWW4AMXn`vbTZiszhqdPPUT%y^C@jqtLL<HQ3G^s@J=ck
z?y(0<dFQKx(_QqxdOmNDMg>0kEf9;f%006#$`h4CeK&Xds^6(fxa7&k{oIE7G02OM
zBQ*TlS*qe{z?S$`%xB}uoWE(y=G87O)@N@SYn?F;y3!kpCoz04nwEIhoEm<WDVN+9
zqRf#Aefr|e7kg5u(ZI*lRu*`+WHa(#zU~1|4=-1xnjLpm9`TkvgOf>|K*&mnDd#4}
z_j$#M8tQNh$!pYkAp9L+x%6m)&?<D`996co?#@nx0=<!M;PyljXPmnS2)1%_#p&Mj
zrMn-7S1blcPWItcE0p-W5w+K}$ttr+JP2l-Gro-9;>%CA^{jDdE$~?uqt4oC%LRyP
z3i5EPfr<m)h2C&ZA&;W5E~iU-m-$p(GtRVbvR2+kg81yRekgUJX_LRQx%pE0M0$oP
zJS!y8&?U+rt7R=FtE3SN=}1vZbQY&@jVcd_Lzv?DT=MG*WMFZ(I}kj6?YzTwMaSpM
zk-wnW)tbrO5|{H4&vblv4j+WZn)Z;@jJ|OKUjS1f5==bIP75U%`uPZaROt$T<#&gR
z-#s?JfJAJ{Eq29P-dr>8-8Yq_JKQfP<wNN?sg0h@-NvEMA<6h8r=eRuC=Oc={M_yA
zAdip`26}X>qRc9jv_9;*chTxUB{cI{7Or9kb`^%hcJLs0FN}*O0}WOmS|-!cysLM-
z9q)*|{e1oxFOcWkFFv2DntH4DG&hqz+)$aw3)Ai@S<aHmFSmT9ABiaXhRorL#bKU_
z(R!mq%1Y|tKHtJmbvyqA!|M8f7_2Yz6%mcz4>NV2_r$t>4tUq8?)k3ox>3}W27{6{
zR18U`Kg#EfH+ocb3O<z-eQ77r(m=1hX6!dPYwKa?%TyhCsilcv-dsI5Q&cMKg+Cwm
z<Rdc58!e48Eyfa}FcS340_7)}A8vE2SPIU8v?HSrpMN~Fr^-Fg9xg_wR@*%GhMJPP
z*)B96jY|pdl1Hme=n>8?`3Z{iYK@5|Q=NF=hQ;=2ygwDXDLdaS%>-g$!fGp5B-{jG
zdOwofCPbUj0@38S#r|Duw%KRhFS1`zUb3Z}`_X*s);reij5o*aeXV^bLMx-LD%7O6
zf8MT~4D1t#*(Ik8Oo?yo;wczbc#@vmD3R`JUsc7X+%*2@n2fuc_Qk7N3pDjcG}dAQ
zh&5C76n|&Ta4_8my?9nsxjtrxc)>#EWY%u8Dc8wTACTpVO_Ndjb0?B*c6WeZ5dEZB
zMwv<yvS*@QRNZkfN_Ql&iDSfTq9|ukzxjzXQRBF<;C|PV^Gz{gV*O|K)Ux@SuntFp
zR~F0W3#&HpcA2BMrP-?w2;G6B6e0Kms^GDmZ(TRViYkFT#EaLV*+9YNJD51bA*^%?
zj6I?g286RyCL9Pj=OiitIM@xwp08BipByK-O*OE(G;<_i%ajCvFsb8$KkD{%h_%dO
z|8RZ_yn@DjK}MxmyGVX`(B%Wl+OhK>k&viIu1uSh>5DcOzy$vU3WYlDg{8fmRB~g0
zcR2$b@aF5H0kZkjqJEP8uFk-{Z{;&1h69Xco-U7qGyS@;^~V?A6Z=<w1g=G}So6;=
z9uL~4E0lFLPd`EB5$3`5XnK?@-Eqm#&VOc<NN`QrCaQ5ZKa8ZOH_=wUIvav_bbF;&
zGm?mT_Y-}6ymwT_Yy&L?AtgYjIo~Cm5AlpPVe9Ev`PRBp>U&E`$MKappS3BEdvetL
zsZyvjJz7~PeKQA0B`FC!e<^w-eewLyiy0mu3FrfJY{e~cXf+30V$fpWOfR82Nzi|+
zea6K>i?a?f!q}t5nLj@DH;*I)T{vN}^~={!-*J8`rmJ0)R8koulxA*a=orlnPtZFT
zgTN-(7ixvVlMvE+MDkfX6(NYP6@$7<CCSbVW@XwIDluA3m*c>W)KmP(f%DOB8DC9)
zi4K}77(l+<T=@0hQbfgiz+tDs^wb$&ig>w>zCg%R4*hO5JX{_cEV>7)h40Z-FT&ux
zOlvGcHA04~rGmf0g@kH#7pYC(au`qM-)6UMfD_s}#u_z#`k++u>aAuFcwvQguj^3g
ztB(@SNw@84OH~H<ou#K;<trCZMabBre?|#7ic%)jsMxB)<_=RTxTkf_@!1$(sWW`4
zL{|A|`x#whS0K+@v!hu}BKidTMDIeh$li#K4zv+|SD`D6GEGFtd=x?e4G^!Ei5e;|
z&wpbBFUvpHIm?$)c<`lN+mr={JxsloLSptjlsMNRh2m76*bghZR3p&m6nF;I0y~s`
zPB{%mR<OJ@X*`(rY3ij^fS8cEjj^~=Cx5c2AXj-t0$mJd!$s+6i|I4O(uxCkvEz{N
zoB4FRx*0eBL-m==PUQ=c&*UyTg~t7`ZsG<(m)Vo|mJPc0^1<s-o4}T&bg|@0r&H%}
zb@g3{LsFQJ5OWy0##B{L4u4T%|3CLtph&#EHxnbxM=Gg)b?L6g$$ug@BZpY~XP4M8
zCs>DO`Yl4It}R3uWR06<B@_H;<!R{ZPK5Y2YSo6uEq^{21;vd!+RiU-38|M!JdLeB
z>ufQS+i6XEP7}>!L;PEomS0c>n@3bUJH&;!#VFUZ9Xsrt3dD=;;KxzHdc1GkaMn9-
zb~S3MzG=YnxBjMLu^ujqxYcOs6&|+9b6+rz+l9eb`HE)j+bGC3v(R8$Hj4xIA%Z9m
zs159H3;4e#m&_!^_4lPvPs3(M-93*kHi#9n@QCrmY^&Q<xZ-LYZ}RF}|H}XWs)w0>
zZ>wi&eE9z2zEef31QqT?Ub?Q_=5U}(G}b?F%>;%whp*IfmooGz+;f$lc|og5CMK{t
zDBS4kylc<D|6hOk+Mg}k@89iRyu1ECXi9PI){viP++qVSUG<W3G!zpI@ahscXX4u0
z0^CrzAh9X&;e!JTj~P1>1vD8J&b&Btt^;tg#79HTjyLw|k-e-QbD2{VRs^zrNf$lo
zH7SW%&=%%KAidY;w{k+M#^K}b!ixhV6FM54loWUz%o-R43a)TRi%Y!L6v{hf8uaXU
z2r$Q2)orPNarc+pe6RuAZmX>>n-TCb+Hz`;M$(+>Ua#J>HWNVs_qN%{u>2^nEG@E7
z%=nV{GJ%~(Q~u1uCykQUi!L^B6*BjW?Rfauv2v-%WSQz|QXP+*T?~|zZoSsq*kUF2
zaq`kkp$o^Hv@K?4JyiXixq8<$v5#)`TQ^<@Reca}?f9#nRm&z#4^v)x+12pf<V{_j
z>l_$<Fr_$&7q~215V!o7v}=Y{+$H_kK7IdtpRWJ^cmJmFf9K%;pp^If%lk!BdN;}}
z*NF@aS+q{COH<QF)nLKImJ1#Z1p)?@t?efg0~IU^lmZ1eXX<WS#<)<Q<-?)bc4C(w
zPVZN0jgf2B)>z~)%^2zyAa!jye_GDX-G|=K=5ALAI$%Df(0b|h%sd8029cbG2}+Az
zeGAgax#q?tF<ok6#Q}lSQ+Zz3uld<uaQ(me^ylqg)9)|Yu6!L7a6g-kUR54(X3)6=
z>_f^WJ~QK9`dW~q;VZ+ZJ;|U3Fx<+Q*K@VH?oI!yo_tYM-koRe_xm*r2L6w3Fl}1U
z*b3Zkb(oQ315d#O=Lp^ar&-zl^>B^<e~0{Qlkk>H^kZk=qjk<fm4Qi1ifbZpw**4T
z%NujEtRxajI?belI6WsOcxEuzGMt!TlZ{aF?{)Qr!XKgwI9{Z#5#IB{A0Y!QIJpu{
z1H_Z23W=*66p%pbFa5tTKlDZ0ob5`nsg6%~gk6<@pIfyG8vvU5J>~MG?drA0N;$4h
zw|AsY%fhAu6mYK_jCNLl0`8GTqQ;ZNX91jvkLA}egfrZ2T_W&(!vt*Rk9e{Fza3oO
z@_*D|bYG~z0NQc;g$20rGKLv=t_g4%OhfVkAT{^`>Hlv5m*@Q-pyzxE_e(rrVA|)v
T!1xd7S_R<nE6~*})NnNb?Vo{S

literal 0
HcmV?d00001

-- 
GitLab