From 501d627392a6826522a665a04c1215968d43b98e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:47:01 -0300 Subject: [PATCH 1/4] fix(audio): review audio modal help screen - Adds a new Help view for unknown error codes - Correctly detect NotAllowedError (permissions) - they are currently being treated like unknown errors in the Help modal - Rephrase NotAllowedError help text; make it more succint and direct - Rephrase the unknown error help text; make it more succint and direct - Add error code and message to that view - Add public.media.audioTroubleshootingLinks to allow referencing KB links on the Help modal - See inline docs --- .../audio/audio-modal/component.jsx | 20 +- .../audio/audio-modal/container.jsx | 2 + .../components/audio/audio-modal/service.js | 7 + .../ui/components/audio/help/component.jsx | 175 ++++++++++++++---- .../ui/components/audio/help/styles.js | 40 +++- .../private/config/settings.yml | 9 + bigbluebutton-html5/public/locales/en.json | 10 +- 7 files changed, 220 insertions(+), 43 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index d32f988e43..8afc079537 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -132,6 +132,7 @@ class AudioModal extends Component { content: null, hasError: false, errCode: null, + errMessage: null, }; this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this); @@ -258,6 +259,7 @@ class AudioModal extends Component { return this.setState({ content: 'help', errCode: MIC_ERROR.NO_SSL, + errMessage: 'NoSSL', }); } @@ -363,6 +365,7 @@ class AudioModal extends Component { this.setState({ content: 'help', errCode: 0, + errMessage: type, disableActions: false, }); break; @@ -370,6 +373,7 @@ class AudioModal extends Component { default: this.setState({ errCode: 0, + errMessage: type, disableActions: false, }); break; @@ -514,16 +518,22 @@ class AudioModal extends Component { localEchoEnabled, showVolumeMeter, notify, + AudioError, } = this.props; + const { MIC_ERROR } = AudioError; const confirmationCallback = !localEchoEnabled ? this.handleRetryGoToEchoTest : this.handleJoinLocalEcho; - const handleGUMFailure = () => { + const handleGUMFailure = (error) => { + const errCode = error?.name === 'NotAllowedError' + ? MIC_ERROR.NO_PERMISSION + : 0 this.setState({ content: 'help', - errCode: 0, + errCode, + errMessage: error?.name || 'GUMFailure', disableActions: false, }); }; @@ -550,18 +560,20 @@ class AudioModal extends Component { } renderHelp() { - const { errCode } = this.state; - const { AudioError } = this.props; + const { errCode, errMessage } = this.state; + const { AudioError, getTroubleshootingLink } = this.props; const audioErr = { ...AudioError, code: errCode, + message: errMessage, }; return ( ); } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx index d61c8e20f8..6e89657df1 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx @@ -16,6 +16,7 @@ import { } from './service'; import Storage from '/imports/ui/services/storage/session'; import Service from '../service'; +import AudioModalService from '/imports/ui/components/audio/audio-modal/service'; const AudioModalContainer = (props) => ; @@ -98,5 +99,6 @@ export default lockContextContainer(withTracker(({ userLocks, setIsOpen }) => { notify: Service.notify, isRTL, AudioError, + getTroubleshootingLink: AudioModalService.getTroubleshootingLink, }); })(AudioModalContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js index ffb6c85716..b8e1a12de7 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js @@ -3,6 +3,7 @@ import Storage from '/imports/ui/services/storage/session'; const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone'; const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly'; +const TROUBLESHOOTING_LINKS = Meteor.settings.public.media.audioTroubleshootingLinks; export const setUserSelectedMicrophone = (value) => ( Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, !!value) @@ -78,6 +79,11 @@ export const closeModal = (callback) => { callback(); }; +const getTroubleshootingLink = (errorCode) => { + if (TROUBLESHOOTING_LINKS) return TROUBLESHOOTING_LINKS[errorCode] || TROUBLESHOOTING_LINKS[0]; + return null; +}; + export default { joinMicrophone, closeModal, @@ -85,4 +91,5 @@ export default { leaveEchoTest, didUserSelectedMicrophone, didUserSelectedListenOnly, + getTroubleshootingLink, }; diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx index 142954b0ea..41f07e1ced 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx @@ -1,63 +1,171 @@ import React, { Component } from 'react'; import { injectIntl, defineMessages } from 'react-intl'; +import PropTypes from 'prop-types'; import Styled from './styles'; +const propTypes = { + intl: PropTypes.shape({ + formatMessage: PropTypes.func.isRequired, + }).isRequired, + audioErr: PropTypes.shape({ + code: PropTypes.number, + message: PropTypes.string, + MIC_ERROR: PropTypes.shape({ + NO_SSL: PropTypes.number, + MAC_OS_BLOCK: PropTypes.number, + NO_PERMISSION: PropTypes.number, + }), + }).isRequired, + handleBack: PropTypes.func.isRequired, + troubleshootingLink: PropTypes.string, +}; + +const defaultProps = { + troubleshootingLink: '', +}; + const intlMessages = defineMessages({ - descriptionHelp: { - id: 'app.audioModal.helpText', - description: 'Text decription for the audio help', + helpSubtitle: { + id: 'app.audioModal.helpSubtitle', + description: 'Text description for the audio help subtitle', }, - backLabel: { - id: 'app.audio.backLabel', - description: 'audio settings back button label', + helpPermissionStep1: { + id: 'app.audioModal.helpPermissionStep1', + description: 'Text description for the audio permission help step 1', + }, + helpPermissionStep2: { + id: 'app.audioModal.helpPermissionStep2', + description: 'Text description for the audio permission help step 2', + }, + helpPermissionStep3: { + id: 'app.audioModal.helpPermissionStep3', + description: 'Text description for the audio permission help step 3', + }, + retryLabel: { + id: 'app.audio.audioSettings.retryLabel', + description: 'audio settings retry button label', }, noSSL: { id: 'app.audioModal.help.noSSL', - description: 'Text decription for domain not using https', + description: 'Text description for domain not using https', }, macNotAllowed: { id: 'app.audioModal.help.macNotAllowed', - description: 'Text decription for mac needed to enable OS setting', + description: 'Text description for mac needed to enable OS setting', + }, + helpTroubleshoot: { + id: 'app.audioModal.help.troubleshoot', + description: 'Text description for help troubleshoot', + }, + unknownError: { + id: 'app.audioModal.help.unknownError', + description: 'Text description for unknown error', + }, + errorCode: { + id: 'app.audioModal.help.errorCode', + description: 'Text description for error code', }, }); class Help extends Component { + renderNoSSL() { + const { intl } = this.props; + + return ( + + {intl.formatMessage(intlMessages.noSSL)} + + ); + } + + renderMacNotAllowed() { + const { intl } = this.props; + + return ( + + {intl.formatMessage(intlMessages.macNotAllowed)} + + ); + } + + renderPermissionHelp() { + const { intl } = this.props; + return ( + <> + + {intl.formatMessage(intlMessages.helpSubtitle)} + + +
  • {intl.formatMessage(intlMessages.helpPermissionStep1)}
  • +
  • {intl.formatMessage(intlMessages.helpPermissionStep2)}
  • +
  • {intl.formatMessage(intlMessages.helpPermissionStep3)}
  • +
    + + ); + } + + renderGenericErrorHelp() { + const { intl, audioErr } = this.props; + const { code, message } = audioErr; + + return ( + <> + + {intl.formatMessage(intlMessages.helpSubtitle)} + + + {intl.formatMessage(intlMessages.unknownError)} + + + {intl.formatMessage(intlMessages.errorCode, { 0: code, 1: message || 'UnknownError' })} + + + ); + } + + renderHelpMessage() { + const { audioErr } = this.props; + const { MIC_ERROR } = audioErr; + + switch (audioErr.code) { + case MIC_ERROR.NO_SSL: + return this.renderNoSSL(); + case MIC_ERROR.MAC_OS_BLOCK: + return this.renderMacNotAllowed(); + case MIC_ERROR.NO_PERMISSION: + return this.renderPermissionHelp(); + default: + return this.renderGenericErrorHelp(); + } + } + render() { const { intl, handleBack, - audioErr, + troubleshootingLink, } = this.props; - const { code, MIC_ERROR } = audioErr; - - let helpMessage = null; - - switch (code) { - case MIC_ERROR.NO_SSL: - helpMessage = intl.formatMessage(intlMessages.noSSL); - break; - case MIC_ERROR.MAC_OS_BLOCK: - helpMessage = intl.formatMessage(intlMessages.macNotAllowed); - break; - case MIC_ERROR.NO_PERMISSION: - default: - helpMessage = intl.formatMessage(intlMessages.descriptionHelp); - break; - } - return ( - - { helpMessage } - + {this.renderHelpMessage()} + { troubleshootingLink && ( + + + {intl.formatMessage(intlMessages.helpTroubleshoot)} + + + )} - @@ -65,4 +173,7 @@ class Help extends Component { } } +Help.propTypes = propTypes; +Help.defaultProps = defaultProps; + export default injectIntl(Help); diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/styles.js b/bigbluebutton-html5/imports/ui/components/audio/help/styles.js index b19fb065ba..33c560ddb4 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/help/styles.js +++ b/bigbluebutton-html5/imports/ui/components/audio/help/styles.js @@ -1,29 +1,36 @@ import styled from 'styled-components'; import Button from '/imports/ui/components/common/button/component'; import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints'; +import { jumboPaddingY, smPaddingY } from '/imports/ui/stylesheets/styled-components/general'; +import { + fontSizeSmaller, +} from '/imports/ui/stylesheets/styled-components/typography'; +import { + colorLink, +} from '/imports/ui/stylesheets/styled-components/palette'; const Help = styled.span` display: flex; flex-flow: column; - height: 10rem; + min-height: 10rem; `; const Text = styled.div` text-align: center; + justify-content: center; margin-top: auto; margin-bottom: auto; `; const EnterAudio = styled.div` - margin-top: 1.5rem; display: flex; justify-content: flex-end; + margin-top: ${jumboPaddingY}; `; -const BackButton = styled(Button)` +const RetryButton = styled(Button)` margin-right: 0.5rem; margin-left: inherit; - border: none; [dir="rtl"] & { margin-right: inherit; @@ -41,9 +48,32 @@ const BackButton = styled(Button)` } `; +const TroubleshootLink = styled.a` + color: ${colorLink}; +`; + +const UnknownError = styled.label` + font-size: ${fontSizeSmaller}; + justify-content: center; + text-align: center; + margin-top: ${smPaddingY}; + margin-bottom: ${smPaddingY}; +`; + +const PermissionHelpSteps = styled.ul` + text-align: left; + justify-content: center; + li { + margin-bottom: ${smPaddingY}; + } +`; + export default { Help, Text, EnterAudio, - BackButton, + RetryButton, + TroubleshootLink, + UnknownError, + PermissionHelpSteps, }; diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index b0cfe66ec3..79f3a09b63 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -771,6 +771,15 @@ public: # audio: high # webcam: medium # screenshare: medium + # + # audioTroubleshootingLinks: links to help users troubleshoot audio issues + # If no link is provided, the audio troubleshooting button will not be shown. + # Index is the error code: + # - 7: permission denied error code + # - 0: unknown error + #audioTroubleshootingLinks: + # 7: 'https://link.bigbluebutton.org/perm' + # 0: 'https://link.bigbluebutton.org/unk' stats: enabled: true interval: 10000 diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index db7c8b94e2..5e48c7ead1 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -732,8 +732,14 @@ "app.audioModal.no.arialabel" : "Echo is inaudible", "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?", "app.audioModal.settingsTitle": "Change your audio settings", - "app.audioModal.helpTitle": "There was an issue with your media devices", - "app.audioModal.helpText": "Did you give permission for access to your microphone? Note that a dialog should appear when you try to join audio, asking for your media device permissions, please accept that in order to join the audio conference. If that is not the case, try changing your microphone permissions in your browser's settings.", + "app.audioModal.helpTitle": "There was an issue with your audio devices", + "app.audioModal.helpSubtitle": "We couldn't enable your microphone", + "app.audioModal.helpPermissionStep1": "When joining a call, accept all requests if prompted to use your microphone.", + "app.audioModal.helpPermissionStep2": "Check browser and device settings to ensure microphone access is allowed.", + "app.audioModal.helpPermissionStep3": "Refresh the page and try again.", + "app.audioModal.help.troubleshoot": "Still having issues? Click here for help.", + "app.audioModal.help.unknownError": "Unknown error. Review your browser and system settings and try again.", + "app.audioModal.help.errorCode": "Error code: {0} - {1}", "app.audioModal.help.noSSL": "This page is unsecured. For microphone access to be allowed the page must be served over HTTPS. Please contact the server administrator.", "app.audioModal.help.macNotAllowed": "It looks like your Mac System Preferences are blocking access to your microphone. Open System Preferences > Security & Privacy > Privacy > Microphone, and verify that the browser you're using is checked.", "app.audioModal.audioDialTitle": "Join using your phone", From 68f66a1fbb47f41dded1cc8538a9abd61c5ab1b7 Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:17:18 -0300 Subject: [PATCH 2/4] fix(audio): handle NotAllowedError in skipCheck:true scenarios In scenarios where the join audio flow skips echo test, NotAllowedError (and any other errors) are all being mashed together under a generic MEDIA_ERROR object. Properly handle specific errors in audio-manager so they're correctly render in the audio modal help screen. --- .../audio/audio-modal/component.jsx | 11 ++++---- .../ui/services/audio-manager/error-codes.js | 2 ++ .../ui/services/audio-manager/index.js | 25 ++++++++++++------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index 8afc079537..7f89d61442 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -359,20 +359,21 @@ class AudioModal extends Component { } handleJoinMicrophoneError(err) { - const { type } = err; + const { type, errCode, errMessage } = err; + switch (type) { case 'MEDIA_ERROR': this.setState({ content: 'help', - errCode: 0, - errMessage: type, + errCode, + errMessage, disableActions: false, }); break; case 'CONNECTION_ERROR': default: this.setState({ - errCode: 0, + errCode, errMessage: type, disableActions: false, }); @@ -533,7 +534,7 @@ class AudioModal extends Component { this.setState({ content: 'help', errCode, - errMessage: error?.name || 'GUMFailure', + errMessage: error?.name || 'NotAllowedError', disableActions: false, }); }; diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/error-codes.js b/bigbluebutton-html5/imports/ui/services/audio-manager/error-codes.js index 7e8fede881..1c4bdbad08 100644 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/error-codes.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/error-codes.js @@ -1,7 +1,9 @@ const MIC_ERROR = { + UNKNOWN: 0, NO_SSL: 9, MAC_OS_BLOCK: 8, NO_PERMISSION: 7, + DEVICE_NOT_FOUND: 6, }; export default { MIC_ERROR }; diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index e712f26438..66a1aad8f2 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -333,14 +333,16 @@ class AudioManager { return this.bridge .joinAudio(callOptions, callStateCallback.bind(this)) .catch((error) => { - const { name } = error; - - if (!name) { - throw error; - } + const { name, message } = error; + const errorPayload = { + type: 'MEDIA_ERROR', + errMessage: message || 'MEDIA_ERROR', + errCode: AudioErrors.MIC_ERROR.UNKNOWN, + }; switch (name) { case 'NotAllowedError': + errorPayload.errCode = AudioErrors.MIC_ERROR.NO_PERMISSION; logger.error( { logCode: 'audiomanager_error_getting_device', @@ -353,6 +355,7 @@ class AudioManager { ); break; case 'NotFoundError': + errorPayload.errCode = AudioErrors.MIC_ERROR.DEVICE_NOT_FOUND; logger.error( { logCode: 'audiomanager_error_device_not_found', @@ -364,17 +367,21 @@ class AudioManager { `Error getting microphone - {${error.name}: ${error.message}}` ); break; - default: + logger.error({ + logCode: 'audiomanager_error_unknown', + extraInfo: { + errorName: error.name, + errorMessage: error.message, + }, + }, `Error getting microphone - {${name}: ${message}}`); break; } this.isConnecting = false; this.isWaitingPermissions = false; - throw { - type: 'MEDIA_ERROR', - }; + throw errorPayload; }); } From f91402bc4a32c29d9dc8d6a903befba5d355d02c Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:27:39 -0300 Subject: [PATCH 3/4] fix(audio): remove listen only retry routines Listen only has a built-in retry routine on join failures that's convoluted half-broken. It stems from the Kurento era where it could fail randomly due to a myriad of reasons. Production logs indicate that the retry is seldom used nowadays in mediasoup-based environments. The presence of the retry also breaks the error troubleshooting modal when actual failures happening, leaving users in the dark about what's happening. Remove the listen only retry code from AudioManager and bubble up any join failure to the callers. --- .../components/audio/audio-modal/service.js | 22 +++--- .../ui/services/audio-manager/index.js | 76 ++++--------------- 2 files changed, 22 insertions(+), 76 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js index b8e1a12de7..824b4f447b 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/service.js @@ -48,19 +48,15 @@ export const joinListenOnly = () => { Storage.setItem(CLIENT_DID_USER_SELECTED_MICROPHONE_KEY, false); Storage.setItem(CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY, true); - const call = new Promise((resolve) => { - Service.joinListenOnly().then(() => { - // Autoplay block wasn't triggered. Close the modal. If autoplay was - // blocked, that'll be handled in the modal component when then - // prop transitions to a state where it was handled OR the user opts - // to close the modal. - if (!Service.autoplayBlocked()) { - document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO")); - } - resolve(); - }); - }); - return call.catch((error) => { + return Service.joinListenOnly().then(() => { + // Autoplay block wasn't triggered. Close the modal. If autoplay was + // blocked, that'll be handled in the modal component when then + // prop transitions to a state where it was handled OR the user opts + // to close the modal. + if (!Service.autoplayBlocked()) { + document.dispatchEvent(new Event("CLOSE_MODAL_AUDIO")); + } + }).catch((error) => { throw error; }); }; diff --git a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js index 66a1aad8f2..e843aed42a 100755 --- a/bigbluebutton-html5/imports/ui/services/audio-manager/index.js +++ b/bigbluebutton-html5/imports/ui/services/audio-manager/index.js @@ -29,8 +29,6 @@ const STATS = Meteor.settings.public.stats; const MEDIA = Meteor.settings.public.media; const MEDIA_TAG = MEDIA.mediaTag; const ECHO_TEST_NUMBER = MEDIA.echoTestNumber; -const MAX_LISTEN_ONLY_RETRIES = 1; -const LISTEN_ONLY_CALL_TIMEOUT_MS = MEDIA.listenOnlyCallTimeout || 25000; const EXPERIMENTAL_USE_KMS_TRICKLE_ICE_FOR_MICROPHONE = Meteor.settings.public.app.experimentalUseKmsTrickleIceForMicrophone; @@ -374,7 +372,7 @@ class AudioManager { errorName: error.name, errorMessage: error.message, }, - }, `Error getting microphone - {${name}: ${message}}`); + }, `Error enabling audio - {${name}: ${message}}`); break; } @@ -385,18 +383,12 @@ class AudioManager { }); } - async joinListenOnly(r = 0) { + async joinListenOnly() { this.audioJoinStartTime = new Date(); this.logAudioJoinTime = false; - let retries = r; this.isListenOnly = true; this.isEchoTest = false; - const callOptions = { - isListenOnly: true, - extension: null, - }; - // Call polyfills for webrtc client if navigator is "iOS Webview" const userAgent = window.navigator.userAgent.toLocaleLowerCase(); if ( @@ -406,62 +398,20 @@ class AudioManager { iosWebviewAudioPolyfills(); } - // We need this until we upgrade to SIP 9x. See #4690 - const listenOnlyCallTimeoutErr = 'SIP_CALL_TIMEOUT'; - - const iceGatheringTimeout = new Promise((resolve, reject) => { - setTimeout(reject, LISTEN_ONLY_CALL_TIMEOUT_MS, listenOnlyCallTimeoutErr); - }); - - const handleListenOnlyError = (err) => { - if (iceGatheringTimeout) { - clearTimeout(iceGatheringTimeout); - } - - const errorReason = - (typeof err === 'string' ? err : undefined) || - err.errorReason || - err.errorMessage; - - logger.error( - { - logCode: 'audiomanager_listenonly_error', - extraInfo: { - errorReason, - audioBridge: this.bridge?.bridgeName, - retries, - }, - }, - `Listen only error - ${errorReason} - bridge: ${this.bridge?.bridgeName}` - ); - }; - - logger.info( - { - logCode: 'audiomanager_join_listenonly', - extraInfo: { logType: 'user_action' }, - }, - 'user requested to connect to audio conference as listen only' - ); + logger.info({ + logCode: 'audiomanager_join_listenonly', + extraInfo: { logType: 'user_action' }, + }, 'user requested to connect to audio conference as listen only'); window.addEventListener('audioPlayFailed', this.handlePlayElementFailed); - return this.onAudioJoining() - .then(() => - Promise.race([ - this.bridge.joinAudio(callOptions, this.callStateCallback.bind(this)), - iceGatheringTimeout, - ]) - ) - .catch(async (err) => { - handleListenOnlyError(err); - - if (retries < MAX_LISTEN_ONLY_RETRIES) { - retries += 1; - this.joinListenOnly(retries); - } - - return null; + return this.onAudioJoining.bind(this)() + .then(() => { + const callOptions = { + isListenOnly: true, + extension: null, + }; + return this.joinAudio(callOptions, this.callStateCallback.bind(this)); }); } From 9956af9aa1773912885956cfd9bad6dd52bbed7e Mon Sep 17 00:00:00 2001 From: prlanzarin <4529051+prlanzarin@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:33:23 -0300 Subject: [PATCH 4/4] fix(audio): improve help modal for listen only scenarios The audio troubleshooting modal has very microphone-specific strings, which might confuse users trying to join listen only. Review the Help screen so that listen only scenarios are more generic. As a bonus, review the unknownError locale with a more actionable text. --- .../audio/audio-modal/component.jsx | 56 +++++++++++-------- .../audio/audio-modal/container.jsx | 1 + .../ui/components/audio/help/component.jsx | 23 ++++++-- bigbluebutton-html5/public/locales/en.json | 5 +- 4 files changed, 55 insertions(+), 30 deletions(-) diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx index 7f89d61442..5d3f045eee 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx @@ -30,6 +30,7 @@ const propTypes = { isConnecting: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired, isUsingAudio: PropTypes.bool.isRequired, + isListenOnly: PropTypes.bool.isRequired, inputDeviceId: PropTypes.string, outputDeviceId: PropTypes.string, formattedDialNum: PropTypes.string.isRequired, @@ -131,8 +132,7 @@ class AudioModal extends Component { this.state = { content: null, hasError: false, - errCode: null, - errMessage: null, + errorInfo: null, }; this.handleGoToAudioOptions = this.handleGoToAudioOptions.bind(this); @@ -235,6 +235,7 @@ class AudioModal extends Component { this.setState({ hasError: false, content: null, + errorInfo: null, }); return this.handleGoToEchoTest(); @@ -258,8 +259,10 @@ class AudioModal extends Component { if (noSSL) { return this.setState({ content: 'help', - errCode: MIC_ERROR.NO_SSL, - errMessage: 'NoSSL', + errorInfo: { + errCode: MIC_ERROR.NO_SSL, + errMessage: 'NoSSL', + }, }); } @@ -280,6 +283,7 @@ class AudioModal extends Component { this.setState({ hasError: false, disableActions: true, + errorInfo: null, }); return joinEchoTest().then(() => { @@ -288,7 +292,7 @@ class AudioModal extends Component { disableActions: false, }); }).catch((err) => { - this.handleJoinMicrophoneError(err); + this.handleJoinAudioError(err); }); } @@ -306,6 +310,8 @@ class AudioModal extends Component { this.setState({ disableActions: true, + hasError: false, + errorInfo: null, }); return joinListenOnly().then(() => { @@ -313,11 +319,7 @@ class AudioModal extends Component { disableActions: false, }); }).catch((err) => { - if (err.type === 'MEDIA_ERROR') { - this.setState({ - content: 'help', - }); - } + this.handleJoinAudioError(err); }); } @@ -347,6 +349,7 @@ class AudioModal extends Component { this.setState({ hasError: false, disableActions: true, + errorInfo: null, }); joinMicrophone().then(() => { @@ -354,28 +357,32 @@ class AudioModal extends Component { disableActions: false, }); }).catch((err) => { - this.handleJoinMicrophoneError(err); + this.handleJoinAudioError(err); }); } - handleJoinMicrophoneError(err) { + handleJoinAudioError(err) { const { type, errCode, errMessage } = err; switch (type) { case 'MEDIA_ERROR': this.setState({ content: 'help', - errCode, - errMessage, disableActions: false, + errorInfo: { + errCode, + errMessage, + } }); break; case 'CONNECTION_ERROR': default: this.setState({ - errCode, - errMessage: type, disableActions: false, + errorInfo: { + errCode, + errMessage: type, + }, }); break; } @@ -533,9 +540,11 @@ class AudioModal extends Component { : 0 this.setState({ content: 'help', - errCode, - errMessage: error?.name || 'NotAllowedError', disableActions: false, + errorInfo: { + errCode, + errMessage: error?.name || 'NotAllowedError', + }, }); }; @@ -561,20 +570,21 @@ class AudioModal extends Component { } renderHelp() { - const { errCode, errMessage } = this.state; - const { AudioError, getTroubleshootingLink } = this.props; + const { errorInfo } = this.state; + const { AudioError, getTroubleshootingLink, isListenOnly } = this.props; const audioErr = { ...AudioError, - code: errCode, - message: errMessage, + code: errorInfo?.errCode, + message: errorInfo?.errMessage, }; return ( ); } diff --git a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx index 6e89657df1..7d5a56e7e8 100755 --- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx @@ -100,5 +100,6 @@ export default lockContextContainer(withTracker(({ userLocks, setIsOpen }) => { isRTL, AudioError, getTroubleshootingLink: AudioModalService.getTroubleshootingLink, + isListenOnly: Service.isListenOnly(), }); })(AudioModalContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx index 41f07e1ced..239f2fabb5 100644 --- a/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/audio/help/component.jsx @@ -7,6 +7,7 @@ const propTypes = { intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired, }).isRequired, + isListenOnly: PropTypes.bool.isRequired, audioErr: PropTypes.shape({ code: PropTypes.number, message: PropTypes.string, @@ -25,9 +26,13 @@ const defaultProps = { }; const intlMessages = defineMessages({ - helpSubtitle: { - id: 'app.audioModal.helpSubtitle', - description: 'Text description for the audio help subtitle', + helpSubtitleMic: { + id: 'app.audioModal.helpSubtitleMic', + description: 'Text description for the audio help subtitle (microphones)', + }, + helpSubtitleGeneric: { + id: 'app.audioModal.helpSubtitleGeneric', + description: 'Text description for the audio help subtitle (generic)', }, helpPermissionStep1: { id: 'app.audioModal.helpPermissionStep1', @@ -68,6 +73,14 @@ const intlMessages = defineMessages({ }); class Help extends Component { + getSubtitle() { + const { intl, isListenOnly } = this.props; + + return !isListenOnly + ? intl.formatMessage(intlMessages.helpSubtitleMic) + : intl.formatMessage(intlMessages.helpSubtitleGeneric); + } + renderNoSSL() { const { intl } = this.props; @@ -93,7 +106,7 @@ class Help extends Component { return ( <> - {intl.formatMessage(intlMessages.helpSubtitle)} + {this.getSubtitle()}
  • {intl.formatMessage(intlMessages.helpPermissionStep1)}
  • @@ -111,7 +124,7 @@ class Help extends Component { return ( <> - {intl.formatMessage(intlMessages.helpSubtitle)} + {this.getSubtitle()} {intl.formatMessage(intlMessages.unknownError)} diff --git a/bigbluebutton-html5/public/locales/en.json b/bigbluebutton-html5/public/locales/en.json index 5e48c7ead1..626baf6076 100755 --- a/bigbluebutton-html5/public/locales/en.json +++ b/bigbluebutton-html5/public/locales/en.json @@ -733,12 +733,13 @@ "app.audioModal.echoTestTitle": "This is a private echo test. Speak a few words. Did you hear audio?", "app.audioModal.settingsTitle": "Change your audio settings", "app.audioModal.helpTitle": "There was an issue with your audio devices", - "app.audioModal.helpSubtitle": "We couldn't enable your microphone", + "app.audioModal.helpSubtitleMic": "We couldn't enable your microphone", + "app.audioModal.helpSubtitleGeneric": "We're having trouble establishing an audio connection", "app.audioModal.helpPermissionStep1": "When joining a call, accept all requests if prompted to use your microphone.", "app.audioModal.helpPermissionStep2": "Check browser and device settings to ensure microphone access is allowed.", "app.audioModal.helpPermissionStep3": "Refresh the page and try again.", "app.audioModal.help.troubleshoot": "Still having issues? Click here for help.", - "app.audioModal.help.unknownError": "Unknown error. Review your browser and system settings and try again.", + "app.audioModal.help.unknownError": "Review your browser and system settings. Restart your browser and try again.", "app.audioModal.help.errorCode": "Error code: {0} - {1}", "app.audioModal.help.noSSL": "This page is unsecured. For microphone access to be allowed the page must be served over HTTPS. Please contact the server administrator.", "app.audioModal.help.macNotAllowed": "It looks like your Mac System Preferences are blocking access to your microphone. Open System Preferences > Security & Privacy > Privacy > Microphone, and verify that the browser you're using is checked.",