From b270256a7abd0e0cbec6b3be75199d4bada88932 Mon Sep 17 00:00:00 2001 From: Kostiantyn Tsaregradskyi Date: Tue, 17 May 2016 18:58:25 +0300 Subject: [PATCH 1/2] Switch local audio and video track when list of available devices changes --- conference.js | 231 +++++++++++++++++- css/main.css | 4 + lang/main.json | 8 +- modules/UI/UI.js | 28 +++ .../UI/side_pannels/settings/SettingsMenu.js | 125 ++++++---- modules/UI/toolbars/Toolbar.js | 50 +++- modules/settings/Settings.js | 17 +- 7 files changed, 394 insertions(+), 69 deletions(-) diff --git a/conference.js b/conference.js index 5a6e9f0d5..952490c99 100644 --- a/conference.js +++ b/conference.js @@ -170,16 +170,25 @@ function hangup (requestFeedback = false) { /** * Create local tracks of specified types. - * @param {string[]} devices required track types ('audio', 'video' etc.) + * @param {string[]} devices - required track types ('audio', 'video' etc.) + * @param {string|null} [cameraDeviceId] - camera device id, if undefined - one + * from settings will be used + * @param {string|null} [micDeviceId] - microphone device id, if undefined - one + * from settings will be used * @returns {Promise} */ -function createLocalTracks (...devices) { +function createLocalTracks (devices, cameraDeviceId, micDeviceId) { return JitsiMeetJS.createLocalTracks({ // copy array to avoid mutations inside library devices: devices.slice(0), resolution: config.resolution, - cameraDeviceId: APP.settings.getCameraDeviceId(), - micDeviceId: APP.settings.getMicDeviceId(), + cameraDeviceId: typeof cameraDeviceId === 'undefined' + || cameraDeviceId === null + ? APP.settings.getCameraDeviceId() + : cameraDeviceId, + micDeviceId: typeof micDeviceId === 'undefined' || micDeviceId === null + ? APP.settings.getMicDeviceId() + : micDeviceId, // adds any ff fake device settings if any firefox_fake_device: config.firefox_fake_device }).catch(function (err) { @@ -327,6 +336,7 @@ export default { * @returns {Promise} */ init(options) { + let self = this; this.roomName = options.roomName; JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE); @@ -356,9 +366,9 @@ export default { return JitsiMeetJS.init(config).then(() => { return Promise.all([ // try to retrieve audio and video - createLocalTracks('audio', 'video') + createLocalTracks(['audio', 'video']) // if failed then try to retrieve only audio - .catch(() => createLocalTracks('audio')) + .catch(() => createLocalTracks(['audio'])) // if audio also failed then just return empty array .catch(() => []), connect(options.roomName) @@ -370,15 +380,49 @@ export default { this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); + // if user didn't give access to mic or camera or doesn't have + // them at all, we disable corresponding toolbar buttons + if (!tracks.find((t) => t.isAudioTrack())) { + APP.UI.disableMicrophoneButton(); + } + + if (!tracks.find((t) => t.isVideoTrack())) { + APP.UI.disableCameraButton(); + } + // update list of available devices if (JitsiMeetJS.mediaDevices.isDeviceListAvailable() && JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) { - JitsiMeetJS.mediaDevices.enumerateDevices( - APP.UI.onAvailableDevicesChanged); + JitsiMeetJS.mediaDevices.enumerateDevices(function(devices) { + // Ugly way to synchronize real device IDs with local + // storage and settings menu. This is a workaround until + // getConstraints() method will be implemented in browsers. + if (localAudio) { + localAudio._setRealDeviceIdFromDeviceList(devices); + APP.settings.setMicDeviceId(localAudio.getDeviceId()); + } + + if (localVideo) { + localVideo._setRealDeviceIdFromDeviceList(devices); + APP.settings.setCameraDeviceId( + localVideo.getDeviceId()); + } + + APP.UI.onAvailableDevicesChanged(devices); + }); JitsiMeetJS.mediaDevices.addEventListener( JitsiMeetJS.events.mediaDevices.DEVICE_LIST_CHANGED, - APP.UI.onAvailableDevicesChanged); + (devices) => { + // Just defer callback until other event callbacks are + // processed. + window.setTimeout(() => { + checkLocalDevicesAfterDeviceListChanged(devices) + .then(() => { + APP.UI.onAvailableDevicesChanged(devices); + }); + }, 0); + }); } if (config.iAmRecorder) this.recorder = new Recorder(); @@ -388,6 +432,164 @@ export default { return new Promise((resolve, reject) => { (new ConferenceConnector(resolve, reject)).connect(); }); + + function checkAudioOutputDeviceAfterDeviceListChanged(newDevices) { + if (!JitsiMeetJS.mediaDevices + .isDeviceChangeAvailable('output')) { + return; + } + + var selectedAudioOutputDeviceId = + APP.settings.getAudioOutputDeviceId(), + availableAudioOutputDevices = newDevices.filter(d => { + return d.kind === 'audiooutput'; + }); + + if (selectedAudioOutputDeviceId !== 'default' && + !availableAudioOutputDevices.find(d => + d.deviceId === selectedAudioOutputDeviceId)) { + APP.settings.setAudioOutputDeviceId('default'); + } + } + + function checkLocalDevicesAfterDeviceListChanged(newDevices) { + checkAudioOutputDeviceAfterDeviceListChanged(newDevices); + + let availableAudioInputDevices = newDevices.filter( + d => d.kind === 'audioinput'), + availableVideoInputDevices = newDevices.filter( + d => d.kind === 'videoinput'), + selectedAudioInputDeviceId = APP.settings.getMicDeviceId(), + selectedVideoInputDeviceId = + APP.settings.getCameraDeviceId(), + selectedAudioInputDevice = availableAudioInputDevices.find( + d => d.deviceId === selectedAudioInputDeviceId), + selectedVideoInputDevice = availableVideoInputDevices.find( + d => d.deviceId === selectedVideoInputDeviceId), + tracksToCreate = [], + micIdToUse = null, + cameraIdToUse = null; + + // Here we handle case when no device was initially plugged, but + // then it's connected OR new device was connected when previous + // track has ended. + if (!localAudio || localAudio.disposed || localAudio.isEnded()){ + if (availableAudioInputDevices.length + && availableAudioInputDevices[0].label !== '') { + tracksToCreate.push('audio'); + micIdToUse = availableAudioInputDevices[0].deviceId; + } else { + APP.UI.disableMicrophoneButton(); + } + } + + if ((!localVideo || localVideo.disposed || localVideo.isEnded()) + && !self.isSharingScreen){ + if (availableVideoInputDevices.length + && availableVideoInputDevices[0].label !== '') { + tracksToCreate.push('video'); + cameraIdToUse = availableVideoInputDevices[0].deviceId; + } else { + APP.UI.disableCameraButton(); + } + } + + if (localAudio && !localAudio.disposed && !localAudio.isEnded() + && selectedAudioInputDevice + && selectedAudioInputDeviceId !== localAudio.getDeviceId() + && tracksToCreate.indexOf('audio') === -1) { + tracksToCreate.push('audio'); + micIdToUse = selectedAudioInputDeviceId; + } + + if (localVideo && !localVideo.disposed && !localVideo.isEnded() + && selectedVideoInputDevice + && selectedVideoInputDeviceId !== localVideo.getDeviceId() + && tracksToCreate.indexOf('video') === -1 + && !self.isSharingScreen) { + tracksToCreate.push('video'); + cameraIdToUse = selectedVideoInputDeviceId; + } + + if (tracksToCreate.length) { + return createNewTracks( + tracksToCreate, cameraIdToUse, micIdToUse); + } else { + return Promise.resolve(); + } + + function createNewTracks(type, cameraDeviceId, micDeviceId) { + return createLocalTracks(type, cameraDeviceId, micDeviceId) + .then(onTracksCreated) + .catch(() => { + // if we tried to create both audio and video tracks + // at once and failed, let's try again only with + // audio. Such situation may happen in case if we + // granted access only to microphone, but not to + // camera. + if (type.indexOf('audio') !== -1 + && type.indexOf('video') !== -1) { + return createLocalTracks(['audio'], null, + micDeviceId); + } + + }) + .then(onTracksCreated) + .catch(() => { + // if we tried to create both audio and video tracks + // at once and failed, let's try again only with + // video. Such situation may happen in case if we + // granted access only to camera, but not to + // microphone. + if (type.indexOf('audio') !== -1 + && type.indexOf('video') !== -1) { + return createLocalTracks(['video'], + cameraDeviceId, + null); + } + }) + .then(onTracksCreated) + .catch(() => { + // can't do anything in this case, so just ignore; + }); + } + + function onTracksCreated(tracks) { + tracks && tracks.forEach(track => { + if (track.isAudioTrack()) { + self.useAudioStream(track).then(() => { + console.log('switched local audio'); + + // If we have more than 1 device - mute. + // We check with 2 for audio, because + // it always has 'default' if device is + // available at all. + // TODO: this is not 100% solution - need + // to investigate more + if (availableAudioInputDevices.length > 2) { + muteLocalAudio(true); + } + }); + } else if (track.isVideoTrack()) { + self.useVideoStream(track).then(() => { + console.log('switched local video'); + + // TODO: maybe make video large if we + // are not in conference yet + // If we have more than 1 device - mute. + // TODO: this is not 100% solution - need + // to investigate more + if (availableVideoInputDevices.length > 1) { + muteLocalVideo(true); + } + }); + } else { + console.error("Ignored not an audio nor a " + + "video track: ", track); + } + }); + } + } }); }, /** @@ -667,6 +869,8 @@ export default { this.isSharingScreen = false; } + stream.videoType === 'camera' && APP.UI.enableCameraButton(); + APP.UI.setVideoMuted(this.localId, this.videoMuted); APP.UI.updateDesktopSharingButtons(); @@ -701,6 +905,7 @@ export default { this.audioMuted = false; } + APP.UI.enableMicrophoneButton(); APP.UI.setAudioMuted(this.localId, this.audioMuted); }); }, @@ -719,7 +924,7 @@ export default { this.videoSwitchInProgress = true; if (shareScreen) { - createLocalTracks('desktop').then(([stream]) => { + createLocalTracks(['desktop']).then(([stream]) => { stream.on( TrackEvents.LOCAL_TRACK_STOPPED, () => { @@ -767,7 +972,7 @@ export default { ); }); } else { - createLocalTracks('video').then( + createLocalTracks(['video']).then( ([stream]) => this.useVideoStream(stream) ).then(() => { this.videoSwitchInProgress = false; @@ -1118,7 +1323,7 @@ export default { UIEvents.VIDEO_DEVICE_CHANGED, (cameraDeviceId) => { APP.settings.setCameraDeviceId(cameraDeviceId); - createLocalTracks('video').then(([stream]) => { + createLocalTracks(['video']).then(([stream]) => { this.useVideoStream(stream); console.log('switched local video device'); }); @@ -1129,7 +1334,7 @@ export default { UIEvents.AUDIO_DEVICE_CHANGED, (micDeviceId) => { APP.settings.setMicDeviceId(micDeviceId); - createLocalTracks('audio').then(([stream]) => { + createLocalTracks(['audio']).then(([stream]) => { this.useAudioStream(stream); console.log('switched local audio device'); }); diff --git a/css/main.css b/css/main.css index c49148a8b..065be9774 100644 --- a/css/main.css +++ b/css/main.css @@ -58,6 +58,10 @@ html, body{ vertical-align: middle; } +.button[disabled] { + opacity: 0.5; +} + .toolbar_span>span { display: inline-block; position: absolute; diff --git a/lang/main.json b/lang/main.json index caa03c781..62539cad0 100644 --- a/lang/main.json +++ b/lang/main.json @@ -66,7 +66,9 @@ "dialpad": "Show dialpad", "sharedVideoMutedPopup": "Your shared video has been muted so
that you can talk to the other participants.", "micMutedPopup": "Your microphone has been muted so that you
would fully enjoy your shared video.", - "unableToUnmutePopup": "You cannot un-mute while the shared video is on." + "unableToUnmutePopup": "You cannot un-mute while the shared video is on.", + "cameraDisabled": "Camera is not available", + "micDisabled": "Microphone is not available" }, "bottomtoolbar": { "chat": "Open / close chat", @@ -90,7 +92,9 @@ "selectCamera": "Select camera", "selectMic": "Select microphone", "selectAudioOutput": "Select audio output", - "followMe": "Enable follow me" + "followMe": "Enable follow me", + "noDevice": "None", + "noPermission": "Permission to use device is not granted" }, "videothumbnail": { diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 3b3a88c98..612f407c8 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1142,4 +1142,32 @@ UI.onSharedVideoStop = function (id, attributes) { sharedVideoManager.onSharedVideoStop(id, attributes); }; +/** + * Disables camera toolbar button. + */ +UI.disableCameraButton = function () { + Toolbar.markVideoIconAsDisabled(true); +}; + +/** + * Enables camera toolbar button. + */ +UI.enableCameraButton = function () { + Toolbar.markVideoIconAsDisabled(false); +}; + +/** + * Disables microphone toolbar button. + */ +UI.disableMicrophoneButton = function () { + Toolbar.markAudioIconAsDisabled(true); +}; + +/** + * Enables microphone toolbar button. + */ +UI.enableMicrophoneButton = function () { + Toolbar.markAudioIconAsDisabled(false); +}; + module.exports = UI; diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js index ce9831d96..bee62594f 100644 --- a/modules/UI/side_pannels/settings/SettingsMenu.js +++ b/modules/UI/side_pannels/settings/SettingsMenu.js @@ -30,10 +30,16 @@ function generateLanguagesOptions(items, currentLang) { * Generate html select options for available physical devices. * @param {{ deviceId, label }[]} items available devices * @param {string} [selectedId] id of selected device + * @param {boolean} permissionGranted if permission to use selected device type + * is granted * @returns {string} */ -function generateDevicesOptions(items, selectedId) { - return items.map(function (item) { +function generateDevicesOptions(items, selectedId, permissionGranted) { + if (!permissionGranted && items.length) { + return ''; + } + + var options = items.map(function (item) { let attrs = { value: item.deviceId }; @@ -44,7 +50,13 @@ function generateDevicesOptions(items, selectedId) { let attrsStr = UIUtil.attrsToString(attrs); return ``; - }).join('\n'); + }); + + if (!items.length) { + options.unshift(''); + } + + return options.join(''); } @@ -111,26 +123,30 @@ export default { // DEVICES LIST - this.changeDevicesList([]); - $('#selectCamera').change(function () { - let cameraDeviceId = $(this).val(); - if (cameraDeviceId !== Settings.getCameraDeviceId()) { - emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId); - } - }); - $('#selectMic').change(function () { - let micDeviceId = $(this).val(); - if (micDeviceId !== Settings.getMicDeviceId()) { - emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId); - } - }); - $('#selectAudioOutput').change(function () { - let audioOutputDeviceId = $(this).val(); - if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) { - emitter.emit(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, - audioOutputDeviceId); - } - }); + if (JitsiMeetJS.mediaDevices.isDeviceListAvailable() && + JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) { + this.changeDevicesList([]); + + $('#selectCamera').change(function () { + let cameraDeviceId = $(this).val(); + if (cameraDeviceId !== Settings.getCameraDeviceId()) { + emitter.emit(UIEvents.VIDEO_DEVICE_CHANGED, cameraDeviceId); + } + }); + $('#selectMic').change(function () { + let micDeviceId = $(this).val(); + if (micDeviceId !== Settings.getMicDeviceId()) { + emitter.emit(UIEvents.AUDIO_DEVICE_CHANGED, micDeviceId); + } + }); + $('#selectAudioOutput').change(function () { + let audioOutputDeviceId = $(this).val(); + if (audioOutputDeviceId !== Settings.getAudioOutputDeviceId()) { + emitter.emit(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, + audioOutputDeviceId); + } + }); + } }, /** @@ -193,41 +209,58 @@ export default { * @param {{ deviceId, label, kind }[]} devices list of available devices */ changeDevicesList (devices) { - let $devicesOptions = $('#devicesOptions'); - - if (!devices.length) { - $devicesOptions.hide(); - return; - } - let $selectCamera= $('#selectCamera'), $selectMic = $('#selectMic'), $selectAudioOutput = $('#selectAudioOutput'), $selectAudioOutputParent = $selectAudioOutput.parent(); - let audio = devices.filter(device => device.kind === 'audioinput'); - let video = devices.filter(device => device.kind === 'videoinput'); - let audioOutput = devices - .filter(device => device.kind === 'audiooutput'); + let audio = devices.filter(device => device.kind === 'audioinput'), + video = devices.filter(device => device.kind === 'videoinput'), + audioOutput = devices + .filter(device => device.kind === 'audiooutput'), + selectedAudioDevice = audio.find( + d => d.deviceId === Settings.getMicDeviceId()) || audio[0], + selectedVideoDevice = video.find( + d => d.deviceId === Settings.getCameraDeviceId()) || video[0], + selectedAudioOutputDevice = audioOutput.find( + d => d.deviceId === Settings.getAudioOutputDeviceId()), + videoPermissionGranted = + JitsiMeetJS.mediaDevices.isDevicePermissionGranted('video'), + audioPermissionGranted = + JitsiMeetJS.mediaDevices.isDevicePermissionGranted('audio'); - $selectCamera.html( - generateDevicesOptions(video, Settings.getCameraDeviceId()) - ); - $selectMic.html( - generateDevicesOptions(audio, Settings.getMicDeviceId()) - ); + $selectCamera + .html(generateDevicesOptions( + video, + selectedVideoDevice ? selectedVideoDevice.deviceId : '', + videoPermissionGranted)) + .prop('disabled', !video.length || !videoPermissionGranted); - if (audioOutput.length && - JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) { - $selectAudioOutput.html( - generateDevicesOptions(audioOutput, - Settings.getAudioOutputDeviceId())); + $selectMic + .html(generateDevicesOptions( + audio, + selectedAudioDevice ? selectedAudioDevice.deviceId : '', + audioPermissionGranted)) + .prop('disabled', !audio.length || !audioPermissionGranted); + + if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) { + $selectAudioOutput + .html(generateDevicesOptions( + audioOutput, + selectedAudioOutputDevice + ? selectedAudioOutputDevice.deviceId + : 'default', + videoPermissionGranted || audioPermissionGranted)) + .prop('disabled', !audioOutput.length || + (!videoPermissionGranted && !audioPermissionGranted)); $selectAudioOutputParent.show(); } else { $selectAudioOutputParent.hide(); } - $devicesOptions.show(); + $('#devicesOptions').show(); + + APP.translation.translateElement($('#settingsmenu option')); } }; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index e98abc9cc..221de3ae1 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -191,7 +191,9 @@ const Toolbar = { UIUtil.hideDisabledButtons(defaultToolbarButtons); Object.keys(buttonHandlers).forEach( - buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId]) + buttonId => $(`#${buttonId}`).click(function(event) { + !$(this).prop('disabled') && buttonHandlers[buttonId](event); + }) ); }, /** @@ -361,6 +363,29 @@ const Toolbar = { $('#toolbar_button_camera').toggleClass("icon-camera-disabled", muted); }, + /** + * Marks video icon as disabled or not. + * @param {boolean} disabled if icon should look like disabled or not + */ + markVideoIconAsDisabled (disabled) { + var $btn = $('#toolbar_button_camera'); + + $btn + .prop("disabled", disabled) + .attr("data-i18n", disabled + ? "[content]toolbar.cameraDisabled" + : "[content]toolbar.videomute") + .attr("shortcut", disabled ? "" : "toggleVideoPopover"); + + disabled + ? $btn.attr("disabled", "disabled") + : $btn.removeAttr("disabled"); + + APP.translation.translateElement($btn); + + disabled && this.markVideoIconAsMuted(disabled); + }, + /** * Marks audio icon as muted or not. * @param {boolean} muted if icon should look like muted or not @@ -370,6 +395,29 @@ const Toolbar = { !muted).toggleClass("icon-mic-disabled", muted); }, + /** + * Marks audio icon as disabled or not. + * @param {boolean} disabled if icon should look like disabled or not + */ + markAudioIconAsDisabled (disabled) { + var $btn = $('#toolbar_button_mute'); + + $btn + .prop("disabled", disabled) + .attr("data-i18n", disabled + ? "[content]toolbar.micDisabled" + : "[content]toolbar.mute") + .attr("shortcut", disabled ? "" : "mutePopover"); + + disabled + ? $btn.attr("disabled", "disabled") + : $btn.removeAttr("disabled"); + + APP.translation.translateElement($btn); + + disabled && this.markAudioIconAsMuted(disabled); + }, + /** * Indicates if the toolbar is currently hovered. * @return {true} if the toolbar is currently hovered, {false} otherwise diff --git a/modules/settings/Settings.js b/modules/settings/Settings.js index 798de4515..7cdfe3a14 100644 --- a/modules/settings/Settings.js +++ b/modules/settings/Settings.js @@ -43,12 +43,15 @@ if (supportsLocalStorage()) { window.localStorage.welcomePageDisabled || false ); - var audioOutputDeviceId = window.localStorage.audioOutputDeviceId; + // Currently audio output device change is supported only in Chrome and + // default output always has 'default' device ID + var audioOutputDeviceId = window.localStorage.audioOutputDeviceId + || 'default'; - if (typeof audioOutputDeviceId !== 'undefined' && audioOutputDeviceId !== - JitsiMeetJS.mediaDevices.getAudioOutputDevice()) { - JitsiMeetJS.mediaDevices.setAudioOutputDevice( - window.localStorage.audioOutputDeviceId).catch((ex) => { + if (audioOutputDeviceId !== + JitsiMeetJS.mediaDevices.getAudioOutputDevice()) { + JitsiMeetJS.mediaDevices.setAudioOutputDevice(audioOutputDeviceId) + .catch((ex) => { console.error('failed to set audio output device from local ' + 'storage', ex); }); @@ -166,10 +169,10 @@ export default { /** * Set device id of the audio output device which is currently in use. * Empty string stands for default device. - * @param {string} newId new audio output device id + * @param {string} newId='default' - new audio output device id * @returns {Promise} */ - setAudioOutputDeviceId: function (newId = '') { + setAudioOutputDeviceId: function (newId = 'default') { return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId) .then(() => window.localStorage.audioOutputDeviceId = newId); }, From c1807c3649353928a8e053798b5588156d8f9462 Mon Sep 17 00:00:00 2001 From: Kostiantyn Tsaregradskyi Date: Wed, 18 May 2016 14:23:12 +0300 Subject: [PATCH 2/2] Handle cases when new media devices are added/removed more precisely and more predictable --- conference.js | 62 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 15 deletions(-) diff --git a/conference.js b/conference.js index 952490c99..858093133 100644 --- a/conference.js +++ b/conference.js @@ -22,6 +22,7 @@ const TrackEvents = JitsiMeetJS.events.track; const TrackErrors = JitsiMeetJS.errors.track; let room, connection, localAudio, localVideo, roomLocker; +let currentAudioInputDevices, currentVideoInputDevices; import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo"; @@ -197,6 +198,17 @@ function createLocalTracks (devices, cameraDeviceId, micDeviceId) { }); } +/** + * Stores lists of current 'audioinput' and 'videoinput' devices + * @param {MediaDeviceInfo[]} devices + */ +function setCurrentMediaDevices(devices) { + currentAudioInputDevices = devices.filter( + d => d.kind === 'audioinput'); + currentVideoInputDevices = devices.filter( + d => d.kind === 'videoinput'); +} + class ConferenceConnector { constructor(resolve, reject) { this._resolve = resolve; @@ -408,6 +420,8 @@ export default { localVideo.getDeviceId()); } + setCurrentMediaDevices(devices); + APP.UI.onAvailableDevicesChanged(devices); }); @@ -419,6 +433,8 @@ export default { window.setTimeout(() => { checkLocalDevicesAfterDeviceListChanged(devices) .then(() => { + setCurrentMediaDevices(devices); + APP.UI.onAvailableDevicesChanged(devices); }); }, 0); @@ -453,6 +469,12 @@ export default { } function checkLocalDevicesAfterDeviceListChanged(newDevices) { + // Event handler can be fire before direct enumerateDevices() + // call, so handle this situation here. + if (!currentAudioInputDevices && !currentVideoInputDevices) { + setCurrentMediaDevices(newDevices); + } + checkAudioOutputDeviceAfterDeviceListChanged(newDevices); let availableAudioInputDevices = newDevices.filter( @@ -555,39 +577,49 @@ export default { } function onTracksCreated(tracks) { - tracks && tracks.forEach(track => { + return Promise.all((tracks || []).map(track => { if (track.isAudioTrack()) { - self.useAudioStream(track).then(() => { + let audioWasMuted = self.audioMuted; + + return self.useAudioStream(track).then(() => { console.log('switched local audio'); - // If we have more than 1 device - mute. - // We check with 2 for audio, because - // it always has 'default' if device is - // available at all. - // TODO: this is not 100% solution - need - // to investigate more - if (availableAudioInputDevices.length > 2) { + // If we plugged-in new device (and switched to + // it), but video was muted before, or we + // unplugged current device and selected new + // one, then mute new video track. + if (audioWasMuted || + currentAudioInputDevices.length > + availableAudioInputDevices.length) { muteLocalAudio(true); } }); } else if (track.isVideoTrack()) { - self.useVideoStream(track).then(() => { + let videoWasMuted = self.videoMuted; + + return self.useVideoStream(track).then(() => { console.log('switched local video'); // TODO: maybe make video large if we // are not in conference yet - // If we have more than 1 device - mute. - // TODO: this is not 100% solution - need - // to investigate more - if (availableVideoInputDevices.length > 1) { + + // If we plugged-in new device (and switched to + // it), but video was muted before, or we + // unplugged current device and selected new + // one, then mute new video track. + if (videoWasMuted || + (currentVideoInputDevices.length > + availableVideoInputDevices.length)) { muteLocalVideo(true); } }); } else { console.error("Ignored not an audio nor a " + "video track: ", track); + + return Promise.resolve(); } - }); + })); } } });