Merge pull request #492 from isymchych/device-selection

Allow user to select camera and microphone
This commit is contained in:
Paweł Domas 2016-02-10 15:51:36 -06:00
commit 61f4bb63ab
12 changed files with 281 additions and 60 deletions

View File

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

View File

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

View File

@ -231,6 +231,16 @@
<span data-i18n="settings.startVideoMuted"></span>
</label>
</div>
<div id="devicesOptions">
<label className="devicesOptionsLabel">
<span data-i18n="settings.selectCamera"></span>
<select id="selectCamera"></select>
</label>
<label className="devicesOptionsLabel">
<span data-i18n="settings.selectMic"></span>
<select id="selectMic"></select>
</label>
</div>
<button id="updateSettings" data-i18n="settings.update"></button>
<a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
</div>

View File

@ -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":
{

View File

@ -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).

View File

@ -21,6 +21,21 @@ function generateLanguagesSelectBox() {
return html + "</select>";
}
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 `<option ${attrsStr}>${item.label}</option>`;
}).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();
}
};

View File

@ -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);
}
};

View File

@ -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(' ');
}
};

View File

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

View File

@ -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);
}
},

View File

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

View File

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