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 aabcbe8f61f1685fc9c2d92ca31113894c38248a..29efe73ed010636e004f93d5e944d1e5fea366f1 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/component.jsx
@@ -37,6 +37,13 @@ const propTypes = {
   joinFullAudioImmediately: PropTypes.bool.isRequired,
   joinFullAudioEchoTest: PropTypes.bool.isRequired,
   forceListenOnlyAttendee: PropTypes.bool.isRequired,
+  audioLocked: PropTypes.bool.isRequired,
+  resolve: PropTypes.func.isRequired,
+  isMobileNative: PropTypes.bool.isRequired,
+  isIOSChrome: PropTypes.bool.isRequired,
+  isIEOrEdge: PropTypes.bool.isRequired,
+  hasMediaDevices: PropTypes.bool.isRequired,
+  formattedTelVoice: PropTypes.string.isRequired,
 };
 
 const defaultProps = {
@@ -341,12 +348,12 @@ class AudioModal extends Component {
     const {
       isEchoTest,
       intl,
-      isIOSChrome,
+      hasMediaDevices,
     } = this.props;
 
     const { content } = this.state;
 
-    if (isIOSChrome) {
+    if (!hasMediaDevices) {
       return (
         <div>
           <div className={styles.warning}>!</div>
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 f43cae3aa65af47470c37df4f6b3a2d2a71e6b67..07cb94c2b80f9c68e45911a550e688370cdd760b 100755
--- a/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/audio/audio-modal/container.jsx
@@ -6,6 +6,7 @@ import getFromUserSettings from '/imports/ui/services/users-settings';
 import AudioModal from './component';
 import Meetings from '/imports/api/meetings';
 import Auth from '/imports/ui/services/auth';
+import deviceInfo from '/imports/utils/deviceInfo';
 import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
 import Service from '../service';
 
@@ -83,5 +84,6 @@ export default lockContextContainer(withModalMounter(withTracker(({ mountModal,
     isIOSChrome: browser().name === 'crios',
     isMobileNative: navigator.userAgent.toLowerCase().includes('bbbnative'),
     isIEOrEdge: browser().name === 'edge' || browser().name === 'ie',
+    hasMediaDevices: deviceInfo.hasMediaDevices,
   });
 })(AudioModalContainer)));
diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
index 0946f0baf3c708e2665e91649bdafa9f4bf26198..9429ff05d4ffb83ac71dc768d1c24aed5c709fdd 100755
--- a/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-preview/component.jsx
@@ -18,6 +18,10 @@ const propTypes = {
   startSharing: PropTypes.func.isRequired,
   changeWebcam: PropTypes.func.isRequired,
   changeProfile: PropTypes.func.isRequired,
+  joinVideo: PropTypes.func.isRequired,
+  resolve: PropTypes.func.isRequired,
+  hasMediaDevices: PropTypes.bool.isRequired,
+  webcamDeviceId: PropTypes.string.isRequired,
 };
 
 const intlMessages = defineMessages({
@@ -81,8 +85,56 @@ const intlMessages = defineMessages({
     id: 'app.video.notReadableError',
     description: 'error message When the webcam is being used by other software',
   },
+  iOSError: {
+    id: 'app.audioModal.iOSBrowser',
+    description: 'Audio/Video Not supported warning',
+  },
+  iOSErrorDescription: {
+    id: 'app.audioModal.iOSErrorDescription',
+    description: 'Audio/Video not supported description',
+  },
+  iOSErrorRecommendation: {
+    id: 'app.audioModal.iOSErrorRecommendation',
+    description: 'Audio/Video recommended action',
+  },
 });
 
+const handleGUMError = (error) => {
+  // logger.error(error);
+  // logger.error(error.id);
+  // logger.error(error.name);
+  // console.log(error);
+
+  let convertedError;
+
+  switch (error.name) {
+    case 'SourceUnavailableError':
+    case 'NotReadableError':
+      // hardware failure with the device
+      break;
+    case 'NotAllowedError':
+      // media was disallowed
+      convertedError = intlMessages.NotAllowedError;
+      break;
+    case 'AbortError':
+      // generic error occured
+      break;
+    case 'NotFoundError':
+      // no webcam found
+      convertedError = intlMessages.NotFoundError;
+      break;
+    case 'SecurityError':
+      // user media support is disabled on the document
+      break;
+    case 'TypeError':
+      // issue with constraints or maybe Chrome with HTTP
+      break;
+    default:
+      // default error message handling
+      break;
+  }
+};
+
 class VideoPreview extends Component {
   constructor(props) {
     super(props);
@@ -98,6 +150,8 @@ class VideoPreview extends Component {
     this.scanProfiles = this.scanProfiles.bind(this);
     this.doGUM = this.doGUM.bind(this);
     this.displayPreview = this.displayPreview.bind(this);
+    this.supportWarning = this.supportWarning.bind(this);
+    this.renderModalContent = this.renderModalContent.bind(this);
 
     this.deviceStream = null;
 
@@ -112,6 +166,70 @@ class VideoPreview extends Component {
     };
   }
 
+  componentDidMount() {
+    const { webcamDeviceId, hasMediaDevices } = this.props;
+
+    this._isMounted = true;
+
+    // Have to request any device to get past checks before finding devices. If this is
+    // skipped then we get devices with no labels
+    if (hasMediaDevices) {
+      try {
+        navigator.mediaDevices.getUserMedia({ audio: false, video: true }).then(() => {
+          if (!this._isMounted) return;
+
+          navigator.mediaDevices.enumerateDevices().then(async (devices) => {
+            const webcams = [];
+            let initialDeviceId;
+
+            if (!this._isMounted) return;
+
+            // set webcam
+            devices.forEach((device) => {
+              if (device.kind === 'videoinput') {
+                webcams.push(device);
+                if (!initialDeviceId || (webcamDeviceId && webcamDeviceId === device.deviceId)) {
+                  initialDeviceId = device.deviceId;
+                }
+              }
+            });
+
+            logger.debug(`Enumerate devices came back. There are ${devices.length} devices and ${webcams.length} are video inputs`);
+
+            if (initialDeviceId) {
+              this.setState({
+                availableWebcams: webcams,
+              });
+
+              this.scanProfiles(initialDeviceId);
+            }
+          }).catch((error) => {
+            // CHANGE THIS TO SOMETHING USEFUL
+            logger.warning(`Error enumerating devices. name: [${error.name}] message: [${error.message}]`);
+            handleGUMError(error);
+          });
+        });
+      } catch (error) {
+        // CHANGE THIS TO SOMETHING USEFUL
+        logger.warning(`Error grabbing initial video stream. name: [${error.name}] message: [${error.message}]`);
+        handleGUMError(error);
+      }
+    }
+  }
+
+  componentWillUnmount() {
+    // console.log("unmounting video preview");
+    this.stopTracks();
+    this.deviceStream = null;
+    if (this.video) {
+      // console.log("clear video srcObject");
+      this.video.srcObject = null;
+    }
+
+    this._isMounted = false;
+  }
+
+
   stopTracks() {
     // console.log("in stop tracks");
     if (this.deviceStream) {
@@ -123,42 +241,6 @@ class VideoPreview extends Component {
     }
   }
 
-  handleGUMError(error) {
-    // logger.error(error);
-    // logger.error(error.id);
-    // logger.error(error.name);
-    // console.log(error);
-
-    let convertedError;
-
-    switch (error.name) {
-      case 'SourceUnavailableError':
-      case 'NotReadableError':
-        // hardware failure with the device
-        break;
-      case 'NotAllowedError':
-        // media was disallowed
-        convertedError = intlMessages.NotAllowedError;
-        break;
-      case 'AbortError':
-        // generic error occured
-        break;
-      case 'NotFoundError':
-        // no webcam found
-        convertedError = intlMessages.NotFoundError;
-        break;
-      case 'SecurityError':
-        // user media support is disabled on the document
-        break;
-      case 'TypeError':
-        // issue with constraints or maybe Chrome with HTTP
-        break;
-      default:
-        // default error message handling
-        break;
-    }
-  }
-
   handleSelectWebcam(event) {
     const webcamValue = event.target.value;
 
@@ -208,23 +290,20 @@ class VideoPreview extends Component {
 
     // logger.debug('starting scan');
 
-    const checkWebcamExists = () => {
-      // logger.debug('initial webcam check');
-      // we call gUM with no constraints so we know if any stream is available
-      this.doGUM(deviceId, {}).then((stream) => {
-        if (!this._isMounted) return;
-
-        // We don't need to do anything with the returned stream
-        nextProfile();
-      }).catch((error) => {
-        if (!this._isMounted) return;
+    const scanningCleanup = () => {
+      this.video.onloadedmetadata = undefined;
 
-        // webcam might no longer exist or be available
-        logger.debug(`Error with profile: ${CAMERA_PROFILES[currNum].name}`);
+      if (availableProfiles.length > 0) {
+        const defaultProfile = availableProfiles.find(profile => profile.default)
+          || availableProfiles[0];
+        logger.debug(`Found default profile: ${JSON.stringify(defaultProfile)}`);
 
-        this.handleGUMError(error);
+        this.displayPreview(deviceId, defaultProfile);
+      }
 
-        scanningCleanup();
+      this.setState({
+        scanning: false,
+        availableProfiles,
       });
     };
 
@@ -241,7 +320,7 @@ class VideoPreview extends Component {
           if (!this._isMounted) return;
 
           logger.debug(`Error with fetching profile {${CAMERA_PROFILES[currNum].name}} skipping to next profile. Error is {${error.name}}`);
-          currNum++;
+          currNum += 1;
           nextProfile();
         });
       } else {
@@ -250,6 +329,26 @@ class VideoPreview extends Component {
       }
     };
 
+    const checkWebcamExists = () => {
+      // logger.debug('initial webcam check');
+      // we call gUM with no constraints so we know if any stream is available
+      this.doGUM(deviceId, {}).then(() => {
+        if (!this._isMounted) return;
+
+        // We don't need to do anything with the returned stream
+        nextProfile();
+      }).catch((error) => {
+        if (!this._isMounted) return;
+
+        // webcam might no longer exist or be available
+        logger.debug(`Error with profile: ${CAMERA_PROFILES[currNum].name}`);
+
+        handleGUMError(error);
+
+        scanningCleanup();
+      });
+    };
+
     const getVideoDimensions = () => {
       // logger.debug('loaded metadata');
       if (!this.video.videoWidth) {
@@ -266,27 +365,10 @@ class VideoPreview extends Component {
         logger.debug(`Not including profile ${CAMERA_PROFILES[currNum].name}`);
       }
 
-      currNum++;
+      currNum += 1;
       nextProfile();
     };
 
-    const scanningCleanup = () => {
-      this.video.onloadedmetadata = undefined;
-
-      if (availableProfiles.length > 0) {
-        const defaultProfile = availableProfiles.find(profile => profile.default)
-          || availableProfiles[0];
-        logger.debug(`Found default profile: ${JSON.stringify(defaultProfile)}`);
-
-        this.displayPreview(deviceId, defaultProfile);
-      }
-
-      this.setState({
-        scanning: false,
-        availableProfiles,
-      });
-    };
-
     this.video.onloadedmetadata = getVideoDimensions;
 
     checkWebcamExists();
@@ -326,67 +408,6 @@ class VideoPreview extends Component {
     });
   }
 
-  componentDidMount() {
-    const { webcamDeviceId } = this.props;
-
-    this._isMounted = true;
-
-    // Have to request any device to get past checks before finding devices. If this is
-    // skipped then we get devices with no labels
-    try {
-      navigator.mediaDevices.getUserMedia({ audio: false, video: true }).then((stream) => {
-        if (!this._isMounted) return;
-
-        navigator.mediaDevices.enumerateDevices().then(async (devices) => {
-          const webcams = [];
-          let initialDeviceId;
-
-          if (!this._isMounted) return;
-
-          // set webcam
-          devices.forEach((device) => {
-            if (device.kind === 'videoinput') {
-              webcams.push(device);
-              if (!initialDeviceId || (webcamDeviceId && webcamDeviceId === device.deviceId)) {
-                initialDeviceId = device.deviceId;
-              }
-            }
-          });
-
-          logger.debug(`Enumerate devices came back. There are ${devices.length} devices and ${webcams.length} are video inputs`);
-
-          if (initialDeviceId) {
-            this.setState({
-              availableWebcams: webcams,
-            });
-
-            this.scanProfiles(initialDeviceId);
-          }
-        }).catch((error) => {
-          // CHANGE THIS TO SOMETHING USEFUL
-          logger.warning(`Error enumerating devices. name: [${error.name}] message: [${error.message}]`);
-          this.handleGUMError(error);
-        });
-      });
-    } catch (error) {
-      // CHANGE THIS TO SOMETHING USEFUL
-      logger.warning(`Error grabbing initial video stream. name: [${error.name}] message: [${error.message}]`);
-      this.handleGUMError(error);
-    }
-  }
-
-  componentWillUnmount() {
-    // console.log("unmounting video preview");
-    this.stopTracks();
-    this.deviceStream = null;
-    if (this.video) {
-      // console.log("clear video srcObject");
-      this.video.srcObject = null;
-    }
-
-    this._isMounted = false;
-  }
-
   handleJoinVideo() {
     const {
       joinVideo,
@@ -395,7 +416,22 @@ class VideoPreview extends Component {
     joinVideo();
   }
 
-  render() {
+  supportWarning() {
+    const { intl } = this.props;
+
+    return (
+      <div>
+        <div className={styles.warning}>!</div>
+        <h4 className={styles.main}>{intl.formatMessage(intlMessages.iOSError)}</h4>
+        <div className={styles.text}>{intl.formatMessage(intlMessages.iOSErrorDescription)}</div>
+        <div className={styles.text}>
+          {intl.formatMessage(intlMessages.iOSErrorRecommendation)}
+        </div>
+      </div>
+    );
+  }
+
+  renderModalContent() {
     const {
       intl,
     } = this.props;
@@ -409,13 +445,7 @@ class VideoPreview extends Component {
     } = this.state;
 
     return (
-      <Modal
-        overlayClassName={styles.overlay}
-        className={styles.modal}
-        onRequestClose={this.handleProceed}
-        hideBorder
-        contentLabel={intl.formatMessage(intlMessages.webcamSettingsTitle)}
-      >
+      <div>
         {browser().name === 'edge' || browser().name === 'ie' ? (
           <p className={styles.browserWarning}>
             <FormattedMessage
@@ -503,6 +533,28 @@ class VideoPreview extends Component {
             />
           </div>
         </div>
+      </div>
+    );
+  }
+
+  render() {
+    const {
+      intl,
+      hasMediaDevices,
+    } = this.props;
+
+    return (
+      <Modal
+        overlayClassName={styles.overlay}
+        className={styles.modal}
+        onRequestClose={this.handleProceed}
+        hideBorder
+        contentLabel={intl.formatMessage(intlMessages.webcamSettingsTitle)}
+      >
+        { hasMediaDevices
+          ? this.renderModalContent()
+          : this.supportWarning()
+      }
       </Modal>
     );
   }
diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx
index f8a77f2ac48eb2288a1f002bfce406444b8c7aac..bdb78a5a36d9ed3813f654997b053f02fc1e1f4a 100755
--- a/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/video-preview/container.jsx
@@ -1,6 +1,7 @@
 import React from 'react';
 import { withModalMounter } from '/imports/ui/components/modal/service';
 import { withTracker } from 'meteor/react-meteor-data';
+import deviceInfo from '/imports/utils/deviceInfo';
 import Service from './service';
 import VideoPreview from './component';
 import VideoService from '../video-provider/service';
@@ -8,14 +9,13 @@ import VideoService from '../video-provider/service';
 const VideoPreviewContainer = props => <VideoPreview {...props} />;
 
 export default withModalMounter(withTracker(({ mountModal }) => ({
-  closeModal: () => {
-    mountModal(null);
-  },
   startSharing: () => {
     mountModal(null);
     VideoService.joinVideo();
   },
+  closeModal: () => mountModal(null),
   changeWebcam: deviceId => Service.changeWebcam(deviceId),
   webcamDeviceId: Service.webcamDeviceId(),
   changeProfile: profileId => Service.changeProfile(profileId),
+  hasMediaDevices: deviceInfo.hasMediaDevices,
 }))(VideoPreviewContainer));
diff --git a/bigbluebutton-html5/imports/ui/components/video-preview/styles.scss b/bigbluebutton-html5/imports/ui/components/video-preview/styles.scss
index 061c3e6aa05e6c8df3a96874d2c072368160a402..d8ad92a0432e6b6c7d5356552958a38b211d44a6 100755
--- a/bigbluebutton-html5/imports/ui/components/video-preview/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/video-preview/styles.scss
@@ -2,6 +2,23 @@
 @import '/imports/ui/stylesheets/mixins/focus';
 @import "/imports/ui/components/modal/simple/styles";
 
+.warning {
+  text-align: center;
+  font-weight: var(--headings-font-weight);
+  font-size: 5rem;
+  white-space: normal;
+}
+.text {
+  margin: var(--line-height-computed);
+  text-align: center;
+}
+
+.main {
+  margin: var(--line-height-computed);
+  text-align: center;
+  font-size: var(--font-size-large);
+}
+
 .actions {
   margin-left: auto;
   margin-right: 3px;
diff --git a/bigbluebutton-html5/imports/utils/deviceInfo.js b/bigbluebutton-html5/imports/utils/deviceInfo.js
index 4291ce290937089b2f4aeab1bf8a807d83e503a5..a5dfb72da1d334e4b44f09e6cdaa0bbbdce5bb3a 100755
--- a/bigbluebutton-html5/imports/utils/deviceInfo.js
+++ b/bigbluebutton-html5/imports/utils/deviceInfo.js
@@ -12,8 +12,8 @@ const deviceInfo = {
       isPhone: smallSide <= MAX_PHONE_SHORT_SIDE,
     };
   },
+  hasMediaDevices: !!navigator.mediaDevices,
 };
 
 
 export default deviceInfo;
-