From f65d630ad8df3f4457a035a0e01d91355ba6af44 Mon Sep 17 00:00:00 2001 From: isymchych Date: Tue, 9 Feb 2016 12:19:43 +0200 Subject: [PATCH] allow user to select camera and microphone --- conference.js | 147 +++++++++++++----- css/settingsmenu.css | 22 +++ index.html | 10 ++ lang/main.json | 4 +- modules/UI/UI.js | 19 ++- .../UI/side_pannels/settings/SettingsMenu.js | 46 ++++++ modules/UI/toolbars/Toolbar.js | 16 ++ modules/UI/util/UIUtil.js | 11 ++ modules/UI/videolayout/LocalVideo.js | 9 +- modules/UI/videolayout/VideoLayout.js | 5 +- modules/settings/Settings.js | 48 +++++- service/UI/UIEvents.js | 4 +- 12 files changed, 281 insertions(+), 60 deletions(-) diff --git a/conference.js b/conference.js index aa31c6def..efa4d0b80 100644 --- a/conference.js +++ b/conference.js @@ -19,7 +19,7 @@ const ConferenceErrors = JitsiMeetJS.errors.conference; const TrackEvents = JitsiMeetJS.events.track; const TrackErrors = JitsiMeetJS.errors.track; -let room, connection, localTracks, localAudio, localVideo, roomLocker; +let room, connection, localAudio, localVideo, roomLocker; /** * Known custom conference commands. @@ -120,6 +120,8 @@ function createLocalTracks (...devices) { // copy array to avoid mutations inside library devices: devices.slice(0), resolution: config.resolution, + cameraDeviceId: APP.settings.getCameraDeviceId(), + micDeviceId: APP.settings.getMicDeviceId(), // adds any ff fake device settings if any firefox_fake_device: config.firefox_fake_device }).catch(function (err) { @@ -293,11 +295,19 @@ export default { ]); }).then(([tracks, con]) => { console.log('initialized with %s local tracks', tracks.length); - localTracks = tracks; connection = con; - this._createRoom(); + this._createRoom(tracks); this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); + + // update list of available devices + if (JitsiMeetJS.isDeviceListAvailable() && + JitsiMeetJS.isDeviceChangeAvailable()) { + JitsiMeetJS.enumerateDevices((devices) => { + this.availableDevices = devices; + APP.UI.onAvailableDevicesChanged(); + }); + } // XXX The API will take care of disconnecting from the XMPP server // (and, thus, leaving the room) on unload. return new Promise((resolve, reject) => { @@ -360,6 +370,10 @@ export default { listMembersIds () { return room.getParticipants().map(p => p.getId()); }, + /** + * List of available cameras and microphones. + */ + availableDevices: [], /** * Check if SIP is supported. * @returns {boolean} @@ -449,32 +463,30 @@ export default { getLogs () { return room.getLogs(); }, - _createRoom () { + _createRoom (localTracks) { room = connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions()); this.localId = room.myUserId(); localTracks.forEach((track) => { - if(track.isAudioTrack()) { - localAudio = track; - } - else if (track.isVideoTrack()) { - localVideo = track; - } room.addTrack(track); - APP.UI.addLocalStream(track); + + if (track.isAudioTrack()) { + this.useAudioStream(track); + } else if (track.isVideoTrack()) { + this.useVideoStream(track); + } }); roomLocker = createRoomLocker(room); this._room = room; // FIXME do not use this - this.localId = room.myUserId(); let email = APP.settings.getEmail(); email && sendEmail(email); let nick = APP.settings.getDisplayName(); - (config.useNicks && !nick) && (() => { + if (config.useNicks && !nick) { nick = APP.UI.askForNickname(); APP.settings.setDisplayName(nick); - })(); + } nick && room.setDisplayName(nick); this._setupListeners(); @@ -489,6 +501,55 @@ export default { return options; }, + /** + * Start using provided video stream. + * Stops previous video stream. + * @param {JitsiLocalTrack} [stream] new stream to use or null + */ + useVideoStream (stream) { + if (localVideo) { + localVideo.stop(); + } + localVideo = stream; + + if (stream) { + this.videoMuted = stream.isMuted(); + + APP.UI.addLocalStream(stream); + + this.isSharingScreen = stream.videoType === 'desktop'; + } else { + this.videoMuted = false; + this.isSharingScreen = false; + } + + APP.UI.setVideoMuted(this.localId, this.videoMuted); + + APP.UI.updateDesktopSharingButtons(); + }, + + /** + * Start using provided audio stream. + * Stops previous audio stream. + * @param {JitsiLocalTrack} [stream] new stream to use or null + */ + useAudioStream (stream) { + if (localAudio) { + localAudio.stop(); + } + localAudio = stream; + + if (stream) { + this.audioMuted = stream.isMuted(); + + APP.UI.addLocalStream(stream); + } else { + this.audioMuted = false; + } + + APP.UI.setAudioMuted(this.localId, this.audioMuted); + }, + videoSwitchInProgress: false, toggleScreenSharing () { if (this.videoSwitchInProgress) { @@ -507,22 +568,13 @@ export default { createLocalTracks('video').then(function ([stream]) { return room.addTrack(stream); }).then((stream) => { - if (localVideo) { - localVideo.stop(); - } - localVideo = stream; - this.videoMuted = stream.isMuted(); - APP.UI.setVideoMuted(this.localId, this.videoMuted); - - APP.UI.addLocalStream(stream); - console.log('sharing local video'); - }).catch((err) => { - localVideo = null; - console.error('failed to share local video', err); - }).then(() => { + this.useVideoStream(stream); this.videoSwitchInProgress = false; - this.isSharingScreen = false; - APP.UI.updateDesktopSharingButtons(); + console.log('sharing local video'); + }).catch(function (err) { + this.useVideoStream(null); + this.videoSwitchInProgress = false; + console.error('failed to share local video', err); }); } else { // stop sharing video and share desktop @@ -541,19 +593,8 @@ export default { ); return room.addTrack(stream); }).then((stream) => { - if (localVideo) { - localVideo.stop(); - } - localVideo = stream; - - this.videoMuted = stream.isMuted(); - APP.UI.setVideoMuted(this.localId, this.videoMuted); - - APP.UI.addLocalStream(stream); - + this.useVideoStream(stream); this.videoSwitchInProgress = false; - this.isSharingScreen = true; - APP.UI.updateDesktopSharingButtons(); console.log('sharing local desktop'); }).catch((err) => { this.videoSwitchInProgress = false; @@ -907,6 +948,30 @@ export default { room.pinParticipant(id); }); + APP.UI.addListener( + UIEvents.VIDEO_DEVICE_CHANGED, + (cameraDeviceId) => { + APP.settings.setCameraDeviceId(cameraDeviceId); + createLocalTracks('video').then(([stream]) => { + room.addTrack(stream); + this.useVideoStream(stream); + console.log('switched local video device'); + }); + } + ); + + APP.UI.addListener( + UIEvents.AUDIO_DEVICE_CHANGED, + (micDeviceId) => { + APP.settings.setMicDeviceId(micDeviceId); + createLocalTracks('audio').then(([stream]) => { + room.addTrack(stream); + this.useAudioStream(stream); + console.log('switched local audio device'); + }); + } + ); + APP.UI.addListener( UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this) ); diff --git a/css/settingsmenu.css b/css/settingsmenu.css index 24fbfa4c4..3d342cca6 100644 --- a/css/settingsmenu.css +++ b/css/settingsmenu.css @@ -1,6 +1,7 @@ #settingsmenu { background: black; color: #00ccff; + overflow-y: auto; } #settingsmenu input, select { @@ -52,6 +53,10 @@ #startMutedOptions { padding-left: 10%; text-indent: -10%; + + /* clearfix */ + overflow: auto; + zoom: 1; } #startAudioMuted { @@ -66,3 +71,20 @@ width: 94%; float: left; } + +#devicesOptions { + display: none; +} + +#devicesOptions label { + display: block; + margin-top: 15px; +} + +#devicesOptions span { + padding-left: 10%; +} + +#devicesOptions select { + height: 40px; +} diff --git a/index.html b/index.html index c026f3f32..15462c16c 100644 --- a/index.html +++ b/index.html @@ -231,6 +231,16 @@ +
+ + +
diff --git a/lang/main.json b/lang/main.json index ebea89f20..9c5792827 100644 --- a/lang/main.json +++ b/lang/main.json @@ -84,7 +84,9 @@ "update": "Update", "name": "Name", "startAudioMuted": "start without audio", - "startVideoMuted": "start without video" + "startVideoMuted": "start without video", + "selectCamera": "select camera", + "selectMic": "select microphone" }, "videothumbnail": { diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 5883a7473..0727e7022 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -660,9 +660,9 @@ UI.askForNickname = function () { */ UI.setAudioMuted = function (id, muted) { VideoLayout.onAudioMute(id, muted); - if(APP.conference.isLocalId(id)) - UIUtil.buttonClick("#toolbar_button_mute", - "icon-microphone icon-mic-disabled"); + if (APP.conference.isLocalId(id)) { + Toolbar.markAudioIconAsMuted(muted); + } }; /** @@ -670,8 +670,9 @@ UI.setAudioMuted = function (id, muted) { */ UI.setVideoMuted = function (id, muted) { VideoLayout.onVideoMute(id, muted); - if(APP.conference.isLocalId(id)) - $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted); + if (APP.conference.isLocalId(id)) { + Toolbar.markVideoIconAsMuted(muted); + } }; UI.addListener = function (type, listener) { @@ -1040,6 +1041,14 @@ UI.onStartMutedChanged = function () { SettingsMenu.onStartMutedChanged(); }; +/** + * Update list of available physical devices. + * @param {object[]} devices new list of available devices + */ +UI.onAvailableDevicesChanged = function (devices) { + SettingsMenu.onAvailableDevicesChanged(devices); +}; + /** * Returns the id of the current video shown on large. * Currently used by tests (torture). diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js index 5bc06272d..950ee0968 100644 --- a/modules/UI/side_pannels/settings/SettingsMenu.js +++ b/modules/UI/side_pannels/settings/SettingsMenu.js @@ -21,6 +21,21 @@ function generateLanguagesSelectBox() { return html + ""; } +function generateDevicesOptions(items, selectedId) { + return items.map(function (item) { + let attrs = { + value: item.deviceId + }; + + if (item.deviceId === selectedId) { + attrs.selected = 'selected'; + } + + let attrsStr = UIUtil.attrsToString(attrs); + return ``; + }).join('\n'); +} + export default { init (emitter) { @@ -51,12 +66,23 @@ export default { startVideoMuted ); } + + let cameraDeviceId = $('#selectCamera').val(); + if (cameraDeviceId !== Settings.getCameraDeviceId()) { + emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId); + } + + let micDeviceId = $('#selectMic').val(); + if (micDeviceId !== Settings.getMicDeviceId()) { + emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId); + } } let startMutedBlock = $("#startMutedOptions"); startMutedBlock.before(generateLanguagesSelectBox()); APP.translation.translateElement($("#languages_selectbox")); + this.onAvailableDevicesChanged(); this.onRoleChanged(); this.onStartMutedChanged(); @@ -94,5 +120,25 @@ export default { changeAvatar (avatarUrl) { $('#avatar').attr('src', avatarUrl); + }, + + onAvailableDevicesChanged () { + let devices = APP.conference.availableDevices; + if (!devices.length) { + $('#devicesOptions').hide(); + return; + } + + let audio = devices.filter(device => device.kind === 'audioinput'); + let video = devices.filter(device => device.kind === 'videoinput'); + + $('#selectCamera').html( + generateDevicesOptions(video, Settings.getCameraDeviceId()) + ); + $('#selectMic').html( + generateDevicesOptions(audio, Settings.getMicDeviceId()) + ); + + $('#devicesOptions').show(); } }; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 1d471e8b6..e2b79b5bf 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -385,6 +385,22 @@ const Toolbar = { updateRecordingState (state) { setRecordingButtonState(state); + }, + + /** + * Marks video icon as muted or not. + * @param {boolean} muted if icon should look like muted or not + */ + markVideoIconAsMuted (muted) { + $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted); + }, + + /** + * Marks audio icon as muted or not. + * @param {boolean} muted if icon should look like muted or not + */ + markAudioIconAsMuted (muted) { + $('#toolbar_button_mute').toggleClass("icon-microphone", !muted).toggleClass("icon-mic-disabled", muted); } }; diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index 2a1f084a0..519d66f30 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -139,6 +139,17 @@ return document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen; + }, + + /** + * Create html attributes string out of object properties. + * @param {Object} attrs object with properties + * @returns {String} string of html element attributes + */ + attrsToString: function (attrs) { + return Object.keys(attrs).map( + key => ` ${key}="${attrs[key]}"` + ).join(' '); } }; diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js index 5302920f5..a01c1cde7 100644 --- a/modules/UI/videolayout/LocalVideo.js +++ b/modules/UI/videolayout/LocalVideo.js @@ -17,6 +17,11 @@ function LocalVideo(VideoLayout, emitter) { this.flipX = true; this.isLocal = true; this.emitter = emitter; + Object.defineProperty(this, 'id', { + get: function () { + return APP.conference.localId; + } + }); SmallVideo.call(this); } @@ -195,8 +200,4 @@ LocalVideo.prototype.changeVideo = function (stream) { stream.on(TrackEvents.TRACK_STOPPED, endedHandler); }; -LocalVideo.prototype.joined = function (id) { - this.id = id; -}; - export default LocalVideo; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index aab117ac2..07b3858b1 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -170,11 +170,8 @@ var VideoLayout = { * and setting them assume the id is already set. */ mucJoined () { - let id = APP.conference.localId; - localVideoThumbnail.joined(id); - if (largeVideo && !largeVideo.id) { - this.updateLargeVideo(id, true); + this.updateLargeVideo(APP.conference.localId, true); } }, diff --git a/modules/settings/Settings.js b/modules/settings/Settings.js index 330371c14..ced1aeeb0 100644 --- a/modules/settings/Settings.js +++ b/modules/settings/Settings.js @@ -1,9 +1,11 @@ import {generateUsername} from '../util/UsernameGenerator'; -var email = ''; -var displayName = ''; -var userId; -var language = null; +let email = ''; +let displayName = ''; +let userId; +let language = null; +let cameraDeviceId = ''; +let micDeviceId = ''; function supportsLocalStorage() { try { @@ -32,6 +34,8 @@ if (supportsLocalStorage()) { email = window.localStorage.email || ''; displayName = window.localStorage.displayname || ''; language = window.localStorage.language; + cameraDeviceId = window.localStorage.cameraDeviceId || ''; + micDeviceId = window.localStorage.micDeviceId || ''; } else { console.log("local storage is not supported"); userId = generateUniqueId(); @@ -86,5 +90,41 @@ export default { setLanguage: function (lang) { language = lang; window.localStorage.language = lang; + }, + + /** + * Get device id of the camera which is currently in use. + * Empty string stands for default device. + * @returns {String} + */ + getCameraDeviceId: function () { + return cameraDeviceId; + }, + /** + * Set device id of the camera which is currently in use. + * Empty string stands for default device. + * @param {string} newId new camera device id + */ + setCameraDeviceId: function (newId = '') { + cameraDeviceId = newId; + window.localStorage.cameraDeviceId = newId; + }, + + /** + * Get device id of the microphone which is currently in use. + * Empty string stands for default device. + * @returns {String} + */ + getMicDeviceId: function () { + return micDeviceId; + }, + /** + * Set device id of the microphone which is currently in use. + * Empty string stands for default device. + * @param {string} newId new microphone device id + */ + setMicDeviceId: function (newId = '') { + micDeviceId = newId; + window.localStorage.micDeviceId = newId; } }; diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 42ece3874..b915b3793 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -41,5 +41,7 @@ export default { LOGOUT: "UI.logout", RECORDING_TOGGLE: "UI.recording_toggle", SIP_DIAL: "UI.sip_dial", - SUBEJCT_CHANGED: "UI.subject_changed" + SUBEJCT_CHANGED: "UI.subject_changed", + VIDEO_DEVICE_CHANGED: "UI.video_device_changed", + AUDIO_DEVICE_CHANGED: "UI.audio_device_changed" };