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; -