diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js new file mode 100644 index 0000000000..45ca5dc30d --- /dev/null +++ b/src/CallMediaHandler.js @@ -0,0 +1,65 @@ +/* + Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import UserSettingsStore from './UserSettingsStore'; +import * as Matrix from 'matrix-js-sdk'; +import q from 'q'; + +export default { + getDevices: function() { + // Only needed for Electron atm, though should work in modern browsers + // once permission has been granted to the webapp + return navigator.mediaDevices.enumerateDevices().then(function(devices) { + const audioIn = []; + const videoIn = []; + + if (devices.some((device) => !device.label)) return false; + + devices.forEach((device) => { + switch (device.kind) { + case 'audioinput': audioIn.push(device); break; + case 'videoinput': videoIn.push(device); break; + } + }); + + // console.log("Loaded WebRTC Devices", mediaDevices); + return { + audioinput: audioIn, + videoinput: videoIn, + }; + }, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); }); + }, + + loadDevices: function() { + // this.getDevices().then((devices) => { + const localSettings = UserSettingsStore.getLocalSettings(); + // // if deviceId is not found, automatic fallback is in spec + // // recall previously stored inputs if any + Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']); + Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']); + // }); + }, + + setAudioInput: function(deviceId) { + UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId); + Matrix.setMatrixCallAudioInput(deviceId); + }, + + setVideoInput: function(deviceId) { + UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId); + Matrix.setMatrixCallVideoInput(deviceId); + }, +}; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index b9ef73dd42..e2fdeb4687 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -22,6 +22,7 @@ import UserSettingsStore from '../../UserSettingsStore'; import KeyCode from '../../KeyCode'; import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; +import CallMediaHandler from '../../CallMediaHandler'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -79,6 +80,8 @@ export default React.createClass({ // RoomView.getScrollState() this._scrollStateMap = {}; + CallMediaHandler.loadDevices(); + document.addEventListener('keydown', this._onKeyDown); this._matrixClient.on("accountData", this.onAccountData); }, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b553c8d721..7300d82541 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -24,6 +24,7 @@ const dis = require("../../dispatcher"); const q = require('q'); const packageJson = require('../../../package.json'); const UserSettingsStore = require('../../UserSettingsStore'); +const CallMediaHandler = require('../../CallMediaHandler'); const GeminiScrollbar = require('react-gemini-scrollbar'); const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); @@ -176,6 +177,7 @@ module.exports = React.createClass({ email_add_pending: false, vectorVersion: undefined, rejectingInvites: false, + mediaDevices: null, }; }, @@ -196,6 +198,8 @@ module.exports = React.createClass({ }); } + this._refreshMediaDevices(); + // Bulk rejecting invites: // /sync won't have had time to return when UserSettings re-renders from state changes, so getRooms() // will still return rooms with invites. To get around this, add a listener for @@ -257,6 +261,20 @@ module.exports = React.createClass({ this.setState({ electron_settings: settings }); }, + _refreshMediaDevices: function() { + q().then(() => { + return CallMediaHandler.getDevices(); + }).then((mediaDevices) => { + // console.log("got mediaDevices", mediaDevices, this._unmounted); + if (this._unmounted) return; + this.setState({ + mediaDevices, + activeAudioInput: this._localSettings['webrtc_audioinput'], + activeVideoInput: this._localSettings['webrtc_videoinput'], + }); + }); + }, + _refreshFromServer: function() { const self = this; q.all([ @@ -883,6 +901,110 @@ module.exports = React.createClass({ ; }, + _mapWebRtcDevicesToSpans: function(devices) { + return devices.map((device) => {device.label}); + }, + + _setAudioInput: function(deviceId) { + this.setState({activeAudioInput: deviceId}); + CallMediaHandler.setAudioInput(deviceId); + }, + + _setVideoInput: function(deviceId) { + this.setState({activeVideoInput: deviceId}); + CallMediaHandler.setVideoInput(deviceId); + }, + + _requestMediaPermissions: function(event) { + const getUserMedia = ( + window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia + ); + if (getUserMedia) { + return getUserMedia.apply(window.navigator, [ + { video: true, audio: true }, + this._refreshMediaDevices, + function() { + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: _t('No media permissions'), + description: _t('You may need to manually permit Riot to access your microphone/webcam'), + }); + }, + ]); + } + }, + + _renderWebRtcSettings: function() { + if (this.state.mediaDevices === false) { + return
+

{_t('VoIP')}

+
+

+ {_t('Missing Media Permissions, click here to request.')} +

+
+
; + } else if (!this.state.mediaDevices) return; + + const Dropdown = sdk.getComponent('elements.Dropdown'); + + let microphoneDropdown =

{_t('No Microphones detected')}

; + let webcamDropdown =

{_t('No Webcams detected')}

; + + const defaultOption = { + deviceId: '', + label: _t('Default Device'), + }; + + const audioInputs = this.state.mediaDevices.audioinput.slice(0); + if (audioInputs.length > 0) { + let defaultInput = ''; + if (!audioInputs.some((input) => input.deviceId === 'default')) { + audioInputs.unshift(defaultOption); + } else { + defaultInput = 'default'; + } + + microphoneDropdown =
+

{_t('Microphone')}

+ + {this._mapWebRtcDevicesToSpans(audioInputs)} + +
; + } + + const videoInputs = this.state.mediaDevices.videoinput.slice(0); + if (videoInputs.length > 0) { + let defaultInput = ''; + if (!videoInputs.some((input) => input.deviceId === 'default')) { + videoInputs.unshift(defaultOption); + } else { + defaultInput = 'default'; + } + + webcamDropdown =
+

{_t('Camera')}

+ + {this._mapWebRtcDevicesToSpans(videoInputs)} + +
; + } + + return
+

{_t('VoIP')}

+
+ {microphoneDropdown} + {webcamDropdown} +
+
; + }, + _showSpoiler: function(event) { const target = event.target; target.innerHTML = target.getAttribute('data-spoiler'); @@ -1080,6 +1202,7 @@ module.exports = React.createClass({ {this._renderUserInterfaceSettings()} {this._renderLabs()} + {this._renderWebRtcSettings()} {this._renderDevicesPanel()} {this._renderCryptoInfo()} {this._renderBulkOptions()} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5d6a010638..0cfd62f7ea 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -129,6 +129,15 @@ "Add email address": "Add email address", "Add phone number": "Add phone number", "Admin": "Admin", + "VoIP": "VoIP", + "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", + "No Microphones detected": "No Microphones detected", + "No Webcams detected": "No Webcams detected", + "No media permissions": "No media permissions", + "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", + "Default Device": "Default Device", + "Microphone": "Microphone", + "Camera": "Camera", "Advanced": "Advanced", "Algorithm": "Algorithm", "Always show message timestamps": "Always show message timestamps",