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