Merge pull request #1447 from virtuacoplenny/device-picker

New device selection modal
This commit is contained in:
yanas 2017-04-10 17:31:59 -05:00 committed by GitHub
commit 98004c2328
22 changed files with 1643 additions and 220 deletions

View File

@ -30,6 +30,9 @@ import {
conferenceLeft, conferenceLeft,
EMAIL_COMMAND EMAIL_COMMAND
} from './react/features/base/conference'; } from './react/features/base/conference';
import {
updateDeviceList
} from './react/features/base/devices';
import { import {
isFatalJitsiConnectionError isFatalJitsiConnectionError
} from './react/features/base/lib-jitsi-meet'; } from './react/features/base/lib-jitsi-meet';
@ -1029,6 +1032,15 @@ export default {
}); });
}, },
/**
* Returns the current local video track in use.
*
* @returns {JitsiLocalTrack}
*/
getLocalVideoTrack() {
return room.getLocalVideoTrack();
},
/** /**
* Start using provided audio stream. * Start using provided audio stream.
* Stops previous audio stream. * Stops previous audio stream.
@ -1058,6 +1070,15 @@ export default {
}); });
}, },
/**
* Returns the current local audio track in use.
*
* @returns {JitsiLocalTrack}
*/
getLocalAudioTrack() {
return room.getLocalAudioTrack();
},
videoSwitchInProgress: false, videoSwitchInProgress: false,
toggleScreenSharing (shareScreen = !this.isSharingScreen) { toggleScreenSharing (shareScreen = !this.isSharingScreen) {
if (this.videoSwitchInProgress) { if (this.videoSwitchInProgress) {
@ -1622,7 +1643,6 @@ export default {
}) })
.catch((err) => { .catch((err) => {
APP.UI.showDeviceErrorDialog(null, err); APP.UI.showDeviceErrorDialog(null, err);
APP.UI.setSelectedCameraFromSettings();
}); });
} }
); );
@ -1644,7 +1664,6 @@ export default {
}) })
.catch((err) => { .catch((err) => {
APP.UI.showDeviceErrorDialog(err, null); APP.UI.showDeviceErrorDialog(err, null);
APP.UI.setSelectedMicFromSettings();
}); });
} }
); );
@ -1660,7 +1679,6 @@ export default {
logger.warn('Failed to change audio output device. ' + logger.warn('Failed to change audio output device. ' +
'Default or previously set audio output device ' + 'Default or previously set audio output device ' +
'will be used instead.', err); 'will be used instead.', err);
APP.UI.setSelectedAudioOutputFromSettings();
}); });
} }
); );
@ -1756,8 +1774,8 @@ export default {
} }
mediaDeviceHelper.setCurrentMediaDevices(devices); mediaDeviceHelper.setCurrentMediaDevices(devices);
APP.UI.onAvailableDevicesChanged(devices); APP.UI.onAvailableDevicesChanged(devices);
APP.store.dispatch(updateDeviceList(devices));
}); });
this.deviceChangeListener = (devices) => this.deviceChangeListener = (devices) =>

View File

@ -1,31 +0,0 @@
.settingsContent {
display: flex;
display: -webkit-flex;
#localVideoPreview {
width: 50%;
align-self: baseline;
}
.deviceSelection {
display: flex;
display: -webkit-flex;
-webkit-flex: 1;
flex: 1;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: left;
margin-left: 10px;
.device {
display: flex;
margin-bottom: 5px;
select {
flex: 1;
margin_right: 5px;
}
}
}
}

View File

@ -113,6 +113,12 @@
text-align: center; text-align: center;
} }
#deviceOptionsWrapper {
button {
float: none;
}
}
/** /**
* Profile * Profile
*/ */

View File

@ -38,6 +38,7 @@
@import 'inlay'; @import 'inlay';
@import 'reload_overlay/reload_overlay'; @import 'reload_overlay/reload_overlay';
@import 'modals/desktop-picker/desktop-picker'; @import 'modals/desktop-picker/desktop-picker';
@import 'modals/device-selection/device-selection';
@import 'modals/dialog'; @import 'modals/dialog';
@import 'modals/feedback/feedback'; @import 'modals/feedback/feedback';
@import 'modals/speaker_stats/speaker_stats'; @import 'modals/speaker_stats/speaker_stats';
@ -54,7 +55,6 @@
@import 'welcome_page'; @import 'welcome_page';
@import 'toolbars'; @import 'toolbars';
@import 'side_toolbar_container'; @import 'side_toolbar_container';
@import 'device_settings_dialog';
@import 'jquery.contextMenu'; @import 'jquery.contextMenu';
@import 'keyboard-shortcuts'; @import 'keyboard-shortcuts';
@import 'redirect_page'; @import 'redirect_page';

View File

@ -0,0 +1,84 @@
.device-selection {
color: $feedbackInputTextColor;
.device-selectors {
font-size: 14px;
> div {
margin-bottom: 10px;
}
> div:last-child {
margin-bottom: 5px;
}
}
.device-selection-column-selectors,
.device-selection-column-video {
padding: 10px;
display: inline-block;
vertical-align: top;
}
.device-selection-column-selectors {
width: 46%;
}
.device-selection-column-video {
width: 49%;
padding: 10px 0;
}
.device-selection-video-container {
background: black;
height: 156px;
margin: 15px 0 5px;
.video-input-preview {
position: relative;
.video-input-preview-muted {
color: $participantNameColor;
display: none;
left: 0;
position: absolute;
right: 0;
text-align: center;
top: 50%;
}
&.video-muted .video-input-preview-muted {
display: block;
}
.video-input-preview-display {
height: 100%;
overflow: hidden;
width: 100%;
}
}
}
.audio-output-preview {
text-align: right;
a {
cursor: pointer;
text-decoration: none;
}
}
.audio-input-preview {
background: #f4f5f7;
border-radius: 5px;
height: 6px;
.audio-input-preview-level {
background: #0052cc;
border-radius: 5px;
height: 100%;
-webkit-transition: width .1s ease-in-out;
-moz-transition: width .1s ease-in-out;
-o-transition: width .1s ease-in-out;
transition: width .1s ease-in-out;
}
}
}

View File

@ -422,5 +422,11 @@
"seconds": "__count__s", "seconds": "__count__s",
"speakerStats": "Speaker Stats", "speakerStats": "Speaker Stats",
"speakerTime": "Speaker Time" "speakerTime": "Speaker Time"
},
"deviceSelection": {
"deviceSettings": "Device settings",
"noOtherDevices": "No other devices available",
"selectADevice": "Select a device",
"testAudio": "Test sound"
} }
} }

View File

@ -4,6 +4,10 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
var UI = {}; var UI = {};
import {
updateDeviceList
} from '../../react/features/base/devices';
import Chat from "./side_pannels/chat/Chat"; import Chat from "./side_pannels/chat/Chat";
import SidePanels from "./side_pannels/SidePanels"; import SidePanels from "./side_pannels/SidePanels";
import Avatar from "./avatar/Avatar"; import Avatar from "./avatar/Avatar";
@ -1080,29 +1084,7 @@ UI.onLocalRaiseHandChanged = function (isRaisedHand) {
* @param {object[]} devices new list of available devices * @param {object[]} devices new list of available devices
*/ */
UI.onAvailableDevicesChanged = function (devices) { UI.onAvailableDevicesChanged = function (devices) {
SettingsMenu.changeDevicesList(devices); APP.store.dispatch(updateDeviceList(devices));
};
/**
* Sets microphone's <select> element to select microphone ID from settings.
*/
UI.setSelectedMicFromSettings = function () {
SettingsMenu.setSelectedMicFromSettings();
};
/**
* Sets camera's <select> element to select camera ID from settings.
*/
UI.setSelectedCameraFromSettings = function () {
SettingsMenu.setSelectedCameraFromSettings();
};
/**
* Sets audio outputs's <select> element to select audio output ID from
* settings.
*/
UI.setSelectedAudioOutputFromSettings = function () {
SettingsMenu.setSelectedAudioOutputFromSettings();
}; };
/** /**

View File

@ -1,12 +1,15 @@
/* global $, APP, AJS, interfaceConfig, JitsiMeetJS */ /* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
import { openDialog } from '../../../../react/features/base/dialog';
import { LANGUAGES } from "../../../../react/features/base/i18n"; import { LANGUAGES } from "../../../../react/features/base/i18n";
import { DeviceSelectionDialog }
from '../../../../react/features/device-selection';
import UIUtil from "../../util/UIUtil"; import UIUtil from "../../util/UIUtil";
import UIEvents from "../../../../service/UI/UIEvents"; import UIEvents from "../../../../service/UI/UIEvents";
import Settings from '../../../settings/Settings';
const sidePanelsContainerId = 'sideToolbarContainer'; const sidePanelsContainerId = 'sideToolbarContainer';
const deviceSelectionButtonClasses
= 'button-control button-control_primary button-control_full-width';
const htmlStr = ` const htmlStr = `
<div id="settings_container" class="sideToolbarContainer__inner"> <div id="settings_container" class="sideToolbarContainer__inner">
<div class="title" data-i18n="settings.title"></div> <div class="title" data-i18n="settings.title"></div>
@ -19,17 +22,11 @@ const htmlStr = `
<div id="deviceOptionsTitle" class="subTitle hide" <div id="deviceOptionsTitle" class="subTitle hide"
data-i18n="settings.audioVideo"></div> data-i18n="settings.audioVideo"></div>
<div class="sideToolbarBlock first"> <div class="sideToolbarBlock first">
<label class="first" data-i18n="settings.selectCamera"> <button
</label> class="${deviceSelectionButtonClasses}"
<select id="selectCamera"></select> data-i18n="deviceSelection.deviceSettings"
</div> id="deviceSelection"
<div class="sideToolbarBlock"> type="button"></button>
<label data-i18n="settings.selectMic"></label>
<select id="selectMic"></select>
</div>
<div class="sideToolbarBlock">
<label data-i18n="settings.selectAudioOutput"></label>
<select id="selectAudioOutput"></select>
</div> </div>
</div> </div>
<div id="moderatorOptionsWrapper" class="hide"> <div id="moderatorOptionsWrapper" class="hide">
@ -89,40 +86,6 @@ function generateLanguagesOptions(items, currentLang) {
}).join(''); }).join('');
} }
/**
* 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, permissionGranted) {
if (!permissionGranted && items.length) {
return '<option data-i18n="settings.noPermission"></option>';
}
var options = 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>`;
});
if (!items.length) {
options.unshift('<option data-i18n="settings.noDevice"></option>');
}
return options.join('');
}
/** /**
* Replace html select element to select2 custom dropdown * Replace html select element to select2 custom dropdown
* *
@ -138,6 +101,34 @@ function initSelect2($el, onSelectedCb) {
} }
} }
/**
* Open DeviceSelectionDialog with a configuration based on the environment's
* supported abilities.
*
* @param {boolean} isDeviceListAvailable - Whether or not device enumeration
* is possible. This is a value obtained through an async operation whereas all
* other configurations for the modal are obtained synchronously.
* @private
* @returns {void}
*/
function _openDeviceSelectionModal(isDeviceListAvailable) {
APP.store.dispatch(openDialog(DeviceSelectionDialog, {
currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
currentAudioTrack: APP.conference.getLocalAudioTrack(),
currentVideoTrack: APP.conference.getLocalVideoTrack(),
disableAudioInputChange: !JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange: !isDeviceListAvailable
|| !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
hasAudioPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted('audio'),
hasVideoPermission: JitsiMeetJS.mediaDevices
.isDevicePermissionGranted('video'),
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
.isDeviceChangeAvailable('output')
}));
}
export default { export default {
init (emitter) { init (emitter) {
initHTML(); initHTML();
@ -181,11 +172,11 @@ export default {
JitsiMeetJS.mediaDevices.isDeviceListAvailable() JitsiMeetJS.mediaDevices.isDeviceListAvailable()
.then((isDeviceListAvailable) => { .then((isDeviceListAvailable) => {
if (isDeviceListAvailable && $('#deviceSelection').on('click', () => {
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable()) { _openDeviceSelectionModal(isDeviceListAvailable);
this._initializeDeviceSelectionSettings(emitter);
}
}); });
});
// Only show the subtitle if this isn't the only setting section. // Only show the subtitle if this isn't the only setting section.
if (interfaceConfig.SETTINGS_SECTIONS.length > 1) if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
UIUtil.setVisible("deviceOptionsTitle", true); UIUtil.setVisible("deviceOptionsTitle", true);
@ -219,30 +210,6 @@ export default {
} }
}, },
_initializeDeviceSelectionSettings(emitter) {
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 start audio muted/start video muted options should be visible or not. * If start audio muted/start video muted options should be visible or not.
* @param {boolean} show * @param {boolean} show
@ -286,91 +253,5 @@ export default {
*/ */
isVisible () { isVisible () {
return UIUtil.isVisible(document.getElementById("settings_container")); return UIUtil.isVisible(document.getElementById("settings_container"));
},
/**
* Sets microphone's <select> element to select microphone ID from settings.
*/
setSelectedMicFromSettings () {
$('#selectMic').val(Settings.getMicDeviceId());
},
/**
* Sets camera's <select> element to select camera ID from settings.
*/
setSelectedCameraFromSettings () {
$('#selectCamera').val(Settings.getCameraDeviceId());
},
/**
* Sets audio outputs's <select> element to select audio output ID from
* settings.
*/
setSelectedAudioOutputFromSettings () {
$('#selectAudioOutput').val(Settings.getAudioOutputDeviceId());
},
/**
* Change available cameras/microphones or hide selects completely if
* no devices available.
* @param {{ deviceId, label, kind }[]} devices list of available devices
*/
changeDevicesList (devices) {
let $selectCamera= AJS.$('#selectCamera'),
$selectMic = AJS.$('#selectMic'),
$selectAudioOutput = AJS.$('#selectAudioOutput'),
$selectAudioOutputParent = $selectAudioOutput.parent();
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,
selectedVideoDevice ? selectedVideoDevice.deviceId : '',
videoPermissionGranted))
.prop('disabled', !video.length || !videoPermissionGranted);
initSelect2($selectCamera);
$selectMic
.html(generateDevicesOptions(
audio,
selectedAudioDevice ? selectedAudioDevice.deviceId : '',
audioPermissionGranted))
.prop('disabled', !audio.length || !audioPermissionGranted);
initSelect2($selectMic);
if (JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
$selectAudioOutput
.html(generateDevicesOptions(
audioOutput,
selectedAudioOutputDevice
? selectedAudioOutputDevice.deviceId
: 'default',
videoPermissionGranted || audioPermissionGranted))
.prop('disabled', !audioOutput.length ||
(!videoPermissionGranted && !audioPermissionGranted));
initSelect2($selectAudioOutput);
$selectAudioOutputParent.show();
} else {
$selectAudioOutputParent.hide();
}
APP.translation.translateElement($('#settings_container option'));
} }
}; };

View File

@ -21,6 +21,7 @@
"@atlaskit/button-group": "1.0.0", "@atlaskit/button-group": "1.0.0",
"@atlaskit/field-text": "2.0.3", "@atlaskit/field-text": "2.0.3",
"@atlaskit/modal-dialog": "1.2.4", "@atlaskit/modal-dialog": "1.2.4",
"@atlaskit/single-select": "1.6.1",
"@atlaskit/tabs": "1.2.5", "@atlaskit/tabs": "1.2.5",
"async": "0.9.0", "async": "0.9.0",
"autosize": "1.18.13", "autosize": "1.18.13",

View File

@ -0,0 +1,45 @@
import { Symbol } from '../react';
/**
* The type of Redux action which signals that the currently used audio
* input device should be changed.
*
* {
* type: SET_AUDIO_INPUT_DEVICE,
* deviceId: string,
* }
*/
export const SET_AUDIO_INPUT_DEVICE = Symbol('SET_AUDIO_INPUT_DEVICE');
/**
* The type of Redux action which signals that the currently used audio
* output device should be changed.
*
* {
* type: SET_AUDIO_OUTPUT_DEVICE,
* deviceId: string,
* }
*/
export const SET_AUDIO_OUTPUT_DEVICE = Symbol('SET_AUDIO_OUTPUT_DEVICE');
/**
* The type of Redux action which signals that the currently used video
* input device should be changed.
*
* {
* type: SET_VIDEO_INPUT_DEVICE,
* deviceId: string,
* }
*/
export const SET_VIDEO_INPUT_DEVICE = Symbol('SET_VIDEO_INPUT_DEVICE');
/**
* The type of Redux action which signals that the list of known available
* audio and video sources has changed.
*
* {
* type: UPDATE_DEVICE_LIST,
* devices: Array<MediaDeviceInfo>,
* }
*/
export const UPDATE_DEVICE_LIST = Symbol('UPDATE_DEVICE_LIST');

View File

@ -0,0 +1,71 @@
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
/**
* Signals to update the currently used audio input device.
*
* @param {string} deviceId - The id of the new audio input device.
* @returns {{
* type: SET_AUDIO_INPUT_DEVICE,
* deviceId: string
* }}
*/
export function setAudioInputDevice(deviceId) {
return {
type: SET_AUDIO_INPUT_DEVICE,
deviceId
};
}
/**
* Signals to update the currently used audio output device.
*
* @param {string} deviceId - The id of the new audio ouput device.
* @returns {{
* type: SET_AUDIO_OUTPUT_DEVICE,
* deviceId: string
* }}
*/
export function setAudioOutputDevice(deviceId) {
return {
type: SET_AUDIO_OUTPUT_DEVICE,
deviceId
};
}
/**
* Signals to update the currently used video input device.
*
* @param {string} deviceId - The id of the new video input device.
* @returns {{
* type: SET_VIDEO_INPUT_DEVICE,
* deviceId: string
* }}
*/
export function setVideoInputDevice(deviceId) {
return {
type: SET_VIDEO_INPUT_DEVICE,
deviceId
};
}
/**
* Signals to update the list of known audio and video devices.
*
* @param {Array<MediaDeviceInfo>} devices - All known available audio input,
* audio output, and video input devices.
* @returns {{
* type: UPDATE_DEVICE_LIST,
* devices: Array<MediaDeviceInfo>
* }}
*/
export function updateDeviceList(devices) {
return {
type: UPDATE_DEVICE_LIST,
devices
};
}

View File

@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

View File

@ -0,0 +1,34 @@
/* global APP */
import UIEvents from '../../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../redux';
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE
} from './actionTypes';
/**
* Implements the middleware of the feature base/devices.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_AUDIO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
break;
case SET_AUDIO_OUTPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_OUTPUT_DEVICE_CHANGED, action.deviceId);
break;
case SET_VIDEO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.VIDEO_DEVICE_CHANGED, action.deviceId);
break;
}
return next(action);
});

View File

@ -0,0 +1,64 @@
import {
SET_AUDIO_INPUT_DEVICE,
SET_AUDIO_OUTPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
import { ReducerRegistry } from '../redux';
const DEFAULT_STATE = {
audioInput: [],
audioOutput: [],
videoInput: []
};
/**
* Listen for actions which changes the state of known and used devices.
*
* @param {Object} state - The Redux state of the feature features/base/devices.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @param {Array<MediaDeviceInfo>} action.devices - All available audio and
* video devices.
* @returns {Object}
*/
ReducerRegistry.register(
'features/base/devices',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case UPDATE_DEVICE_LIST: {
const deviceList = _groupDevicesByKind(action.devices);
return {
...deviceList
};
}
// TODO: Changing of current audio and video device id is currently
// handled outside of react/redux. Fall through to default logic for
// now.
case SET_AUDIO_INPUT_DEVICE:
case SET_VIDEO_INPUT_DEVICE:
case SET_AUDIO_OUTPUT_DEVICE:
default:
return state;
}
});
/**
* Converts an array of media devices into an object organized by device kind.
*
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
* @private
* @returns {Object} An object with the media devices split by type. The keys
* are device type and the values are arrays with devices matching the device
* type.
*/
function _groupDevicesByKind(devices) {
return {
audioInput: devices.filter(device => device.kind === 'audioinput'),
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
videoInput: devices.filter(device => device.kind === 'videoinput')
};
}

View File

@ -60,3 +60,24 @@ export function loadConfig(host: string, path: string = '/config.js') {
throw err; throw err;
}); });
} }
/**
* Creates a JitsiLocalTrack model from the given device id.
*
* @param {string} type - The media type of track being created. Expected values
* are "video" or "audio".
* @param {string} deviceId - The id of the target media source.
* @returns {Promise<JitsiLocalTrack>}
*/
export function createLocalTrack(type, deviceId) {
return JitsiMeetJS
.createLocalTracks({
devices: [ type ],
micDeviceId: deviceId,
cameraDeviceId: deviceId,
// eslint-disable-next-line camelcase
firefox_fake_device: window.config
&& window.config.firefox_fake_device
}).then(([ jitsiLocalTrack ]) => jitsiLocalTrack);
}

View File

@ -0,0 +1,132 @@
import React, { PureComponent } from 'react';
import { JitsiTrackEvents } from '../../base/lib-jitsi-meet';
/**
* React component for displaying a audio level meter for a JitsiLocalTrack.
*/
class AudioInputPreview extends PureComponent {
/**
* AudioInputPreview component's property types.
*
* @static
*/
static propTypes = {
/*
* The JitsiLocalTrack to show an audio level meter for.
*/
track: React.PropTypes.object
}
/**
* Initializes a new AudioInputPreview instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
audioLevel: 0
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
}
/**
* Starts listening for audio level updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates(this.props.track);
}
/**
* Stops listening for audio level updates on the old track and starts
* listening instead on the new track.
*
* @inheritdoc
* @returns {void}
*/
componentWillReceiveProps(nextProps) {
this._listenForAudioUpdates(nextProps.track);
this._updateAudioLevel(0);
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const audioMeterFill = {
width: `${Math.floor(this.state.audioLevel * 100)}%`
};
return (
<div className = 'audio-input-preview' >
<div
className = 'audio-input-preview-level'
style = { audioMeterFill } />
</div>
);
}
/**
* Starts listening for audio level updates from the library.
*
* @param {JitstiLocalTrack} track - The track to listen to for audio level
* updates.
* @private
* @returns {void}
*/
_listenForAudioUpdates(track) {
this._stopListeningForAudioUpdates();
track && track.on(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateAudioLevel);
}
/**
* Stops listening to further updates from the current track.
*
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates() {
this.props.track && this.props.track.off(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateAudioLevel);
}
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
}
export default AudioInputPreview;

View File

@ -0,0 +1,122 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
const TEST_SOUND_PATH = 'sounds/ring.wav';
/**
* React component for playing a test sound through a specified audio device.
*
* @extends Component
*/
class AudioOutputPreview extends Component {
/**
* AudioOutputPreview component's property types.
*
* @static
*/
static propTypes = {
/**
* The device id of the audio output device to use.
*/
deviceId: React.PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
}
/**
* Initializes a new AudioOutputPreview instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this._audioElement = null;
this._onClick = this._onClick.bind(this);
this._setAudioElement = this._setAudioElement.bind(this);
}
/**
* Sets the target output device on the component's audio element after
* initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._setAudioSink();
}
/**
* Updates the audio element when the target output device changes and the
* audio element has re-rendered.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate() {
this._setAudioSink();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'audio-output-preview'>
<a onClick = { this._onClick }>
{ this.props.t('deviceSelection.testAudio') }
</a>
<audio
preload = 'auto'
ref = { this._setAudioElement }
src = { TEST_SOUND_PATH } />
</div>
);
}
/**
* Plays a test sound.
*
* @private
* @returns {void}
*/
_onClick() {
this._audioElement
&& this._audioElement.play();
}
/**
* Sets the instance variable for the component's audio element so it can be
* accessed directly.
*
* @param {Object} element - The DOM element for the component's audio.
* @private
* @returns {void}
*/
_setAudioElement(element) {
this._audioElement = element;
}
/**
* Updates the target output device for playing the test sound.
*
* @private
* @returns {void}
*/
_setAudioSink() {
this._audioElement
&& this._audioElement.setSinkId(this.props.deviceId);
}
}
export default translate(AudioOutputPreview);

View File

@ -0,0 +1,597 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from '../../base/devices';
import {
Dialog,
hideDialog
} from '../../base/dialog';
import { translate } from '../../base/i18n';
import { createLocalTrack } from '../../base/lib-jitsi-meet';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
/**
* React component for previewing and selecting new audio and video sources.
*
* @extends Component
*/
class DeviceSelectionDialog extends Component {
/**
* DeviceSelectionDialog component's property types.
*
* @static
*/
static propTypes = {
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
_devices: React.PropTypes.object,
/**
* Device id for the current audio output device.
*/
currentAudioOutputId: React.PropTypes.string,
/**
* JitsiLocalTrack for the current local audio.
*
* JitsiLocalTracks for the current audio and video, if any, should be
* passed in for re-use in the previews. This is needed for Internet
* Explorer, which cannot get multiple tracks from the same device, even
* across tabs.
*/
currentAudioTrack: React.PropTypes.object,
/**
* JitsiLocalTrack for the current local video.
*
* Needed for reuse. See comment for propTypes.currentAudioTrack.
*/
currentVideoTrack: React.PropTypes.object,
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: React.PropTypes.bool,
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: React.PropTypes.bool,
/**
* Invoked to notify the store of app state changes.
*/
dispatch: React.PropTypes.func,
/**
* Whether or not new audio input source can be selected.
*/
hasAudioPermission: React.PropTypes.bool,
/**
* Whether or not new video input sources can be selected.
*/
hasVideoPermission: React.PropTypes.bool,
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: React.PropTypes.bool,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered. This is specifically used for hiding audio output on
* temasys browsers which do not support such change.
*/
hideAudioOutputSelect: React.PropTypes.bool,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
}
/**
* Initializes a new DeviceSelectionDialog instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
// JitsiLocalTracks to use for live previewing.
previewAudioTrack: null,
previewVideoTrack: null,
// Device ids to keep track of new selections.
videInput: null,
audioInput: null,
audioOutput: null
};
// Preventing closing while cleaning up previews is important for
// supporting temasys video cleanup. Temasys requires its video object
// to be in the dom and visible for proper detaching of tracks. Delaying
// closure until cleanup is complete ensures no errors in the process.
this._isClosing = false;
this._closeModal = this._closeModal.bind(this);
this._getAndSetAudioOutput = this._getAndSetAudioOutput.bind(this);
this._getAndSetAudioTrack = this._getAndSetAudioTrack.bind(this);
this._getAndSetVideoTrack = this._getAndSetVideoTrack.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Clean up any preview tracks that might not have been cleaned up already.
*
* @inheritdoc
*/
componentWillUnmount() {
// This handles the case where neither submit nor cancel were triggered,
// such as on modal switch. In that case, make a dying attempt to clean
// up previews.
if (!this._isClosing) {
this._attemptPreviewTrackCleanup();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<Dialog
cancelTitleKey = { 'dialog.Cancel' }
okTitleKey = { 'dialog.Save' }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'deviceSelection.deviceSettings' >
<div className = 'device-selection'>
<div className = 'device-selection-column-selectors'>
<div className = 'device-selectors'>
{ this._renderSelectors() }
</div>
{ this._renderAudioOutputPreview() }
</div>
<div className = 'device-selection-column-video'>
<div className = 'device-selection-video-container'>
<VideoInputPreview
track = { this.state.previewVideoTrack
|| this.props.currentVideoTrack } />
</div>
{ this._renderAudioInputPreview() }
</div>
</div>
</Dialog>
);
}
/**
* Cleans up preview tracks if they are not active tracks.
*
* @private
* @returns {Array<Promise>} Zero to two promises will be returned. One
* promise can be for video cleanup and another for audio cleanup.
*/
_attemptPreviewTrackCleanup() {
const cleanupPromises = [];
if (!this._isPreviewingCurrentVideoTrack()) {
cleanupPromises.push(this._disposeVideoPreview());
}
if (!this._isPreviewingCurrentAudioTrack()) {
cleanupPromises.push(this._disposeAudioPreview());
}
return cleanupPromises;
}
/**
* Signals to close DeviceSelectionDialog.
*
* @private
* @returns {void}
*/
_closeModal() {
this.props.dispatch(hideDialog());
}
/**
* Utility function for disposing the current audio preview.
*
* @private
* @returns {Promise}
*/
_disposeAudioPreview() {
return this.state.previewAudioTrack
? this.state.previewAudioTrack.dispose() : Promise.resolve();
}
/**
* Utility function for disposing the current video preview.
*
* @private
* @returns {Promise}
*/
_disposeVideoPreview() {
return this.state.previewVideoTrack
? this.state.previewVideoTrack.dispose() : Promise.resolve();
}
/**
* Callback invoked when a new audio output device has been selected.
* Updates the internal state of the user's selection.
*
* @param {string} deviceId - The id of the chosen audio output device.
* @private
* @returns {void}
*/
_getAndSetAudioOutput(deviceId) {
this.setState({
audioOutput: deviceId
});
}
/**
* Callback invoked when a new audio input device has been selected.
* Updates the internal state of the user's selection as well as the audio
* track that should display in the preview. Will reuse the current local
* audio track if it has been selected.
*
* @param {string} deviceId - The id of the chosen audio input device.
* @private
* @returns {void}
*/
_getAndSetAudioTrack(deviceId) {
this.setState({
audioInput: deviceId
}, () => {
const cleanupPromise = this._isPreviewingCurrentAudioTrack()
? Promise.resolve() : this._disposeAudioPreview();
if (this._isCurrentAudioTrack(deviceId)) {
cleanupPromise
.then(() => {
this.setState({
previewAudioTrack: this.props.currentAudioTrack
});
});
} else {
cleanupPromise
.then(() => createLocalTrack('audio', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewAudioTrack: jitsiLocalTrack
});
});
}
});
}
/**
* Callback invoked when a new video input device has been selected. Updates
* the internal state of the user's selection as well as the video track
* that should display in the preview. Will reuse the current local video
* track if it has been selected.
*
* @param {string} deviceId - The id of the chosen video input device.
* @private
* @returns {void}
*/
_getAndSetVideoTrack(deviceId) {
this.setState({
videoInput: deviceId
}, () => {
const cleanupPromise = this._isPreviewingCurrentVideoTrack()
? Promise.resolve() : this._disposeVideoPreview();
if (this._isCurrentVideoTrack(deviceId)) {
cleanupPromise
.then(() => {
this.setState({
previewVideoTrack: this.props.currentVideoTrack
});
});
} else {
cleanupPromise
.then(() => createLocalTrack('video', deviceId))
.then(jitsiLocalTrack => {
this.setState({
previewVideoTrack: jitsiLocalTrack
});
});
}
});
}
/**
* Utility function for determining if the current local audio track has the
* passed in device id.
*
* @param {string} deviceId - The device id to match against.
* @private
* @returns {boolean} True if the device id is being used by the local audio
* track.
*/
_isCurrentAudioTrack(deviceId) {
return this.props.currentAudioTrack
&& this.props.currentAudioTrack.getDeviceId() === deviceId;
}
/**
* Utility function for determining if the current local video track has the
* passed in device id.
*
* @param {string} deviceId - The device id to match against.
* @private
* @returns {boolean} True if the device id is being used by the local
* video track.
*/
_isCurrentVideoTrack(deviceId) {
return this.props.currentVideoTrack
&& this.props.currentVideoTrack.getDeviceId() === deviceId;
}
/**
* Utility function for detecting if the current audio preview track is not
* the currently used audio track.
*
* @private
* @returns {boolean} True if the current audio track is being used for
* the preview.
*/
_isPreviewingCurrentAudioTrack() {
return !this.state.previewAudioTrack
|| this.state.previewAudioTrack === this.props.currentAudioTrack;
}
/**
* Utility function for detecting if the current video preview track is not
* the currently used video track.
*
* @private
* @returns {boolean} True if the current video track is being used as the
* preview.
*/
_isPreviewingCurrentVideoTrack() {
return !this.state.previewVideoTrack
|| this.state.previewVideoTrack === this.props.currentVideoTrack;
}
/**
* Cleans existing preview tracks and signal to closeDeviceSelectionDialog.
*
* @private
* @returns {boolean} Returns false to prevent closure until cleanup is
* complete.
*/
_onCancel() {
if (this._isClosing) {
return false;
}
this._isClosing = true;
const cleanupPromises = this._attemptPreviewTrackCleanup();
Promise.all(cleanupPromises)
.then(this._closeModal)
.catch(this._closeModal);
return false;
}
/**
* Identify changes to the preferred input/output devices and perform
* necessary cleanup and requests to use those devices. Closes the modal
* after cleanup and device change requests complete.
*
* @private
* @returns {boolean} Returns false to prevent closure until cleanup is
* complete.
*/
_onSubmit() {
if (this._isClosing) {
return false;
}
this._isClosing = true;
const deviceChangePromises = [];
if (this.state.videoInput && !this._isPreviewingCurrentVideoTrack()) {
const changeVideoPromise = this._disposeVideoPreview()
.then(() => {
this.props.dispatch(setVideoInputDevice(
this.state.videoInput));
});
deviceChangePromises.push(changeVideoPromise);
}
if (this.state.audioInput && !this._isPreviewingCurrentAudioTrack()) {
const changeAudioPromise = this._disposeAudioPreview()
.then(() => {
this.props.dispatch(setAudioInputDevice(
this.state.audioInput));
});
deviceChangePromises.push(changeAudioPromise);
}
if (this.state.audioOutput
&& this.state.audioOutput !== this.props.currentAudioOutputId) {
this.props.dispatch(setAudioOutputDevice(this.state.audioOutput));
}
Promise.all(deviceChangePromises)
.then(this._closeModal)
.catch(this._closeModal);
return false;
}
/**
* Creates an AudioInputPreview for previewing if audio is being received.
* Null will be returned if local stats for tracking audio input levels
* cannot be obtained.
*
* @private
* @returns {ReactComponent|null}
*/
_renderAudioInputPreview() {
if (this.props.hideAudioInputPreview) {
return null;
}
return (
<AudioInputPreview
track = { this.state.previewAudioTrack
|| this.props.currentAudioTrack } />
);
}
/**
* Creates an AudioOutputPreview instance for playing a test sound with the
* passed in device id. Null will be returned if hideAudioOutput is truthy.
*
* @private
* @returns {ReactComponent|null}
*/
_renderAudioOutputPreview() {
if (this.props.hideAudioOutputSelect) {
return null;
}
return (
<AudioOutputPreview
deviceId = { this.state.audioOutput
|| this.props.currentAudioOutputId } />
);
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @param {Object} props - The props for the DeviceSelector.
* @returns {ReactElement}
*/
_renderSelector(props) {
return (
<DeviceSelector { ...props } />
);
}
/**
* Creates DeviceSelector instances for video output, audio input, and audio
* output.
*
* @private
* @returns {Array<ReactElement>} DeviceSelector instances.
*/
_renderSelectors() {
const availableDevices = this.props._devices;
const currentAudioId = this.state.audioInput
|| (this.props.currentAudioTrack
&& this.props.currentAudioTrack.getDeviceId());
const currentAudioOutId = this.state.audioOutput
|| this.props.currentAudioOutputId;
// FIXME: On temasys, without a device selected and put into local
// storage as the default device to use, the current video device id is
// a blank string. This is because the library gets a local video track
// and then maps the track's device id by matching the track's label to
// the MediaDeviceInfos returned from enumerateDevices. In WebRTC, the
// track label is expected to return the camera device label. However,
// temasys video track labels refer to track id, not device label, so
// the library cannot match the track to a device. The workaround of
// defaulting to the first videoInput available has been re-used from
// the previous device settings implementation.
const currentVideoId = this.state.videoInput
|| (this.props.currentVideoTrack
&& this.props.currentVideoTrack.getDeviceId())
|| (availableDevices.videoInput[0]
&& availableDevices.videoInput[0].deviceId)
|| ''; // DeviceSelector expects a string for prop selectedDeviceId.
const configurations = [
{
devices: availableDevices.videoInput,
hasPermission: this.props.hasVideoPermission,
isDisabled: this.props.disableDeviceChange,
key: 'videoInput',
label: 'settings.selectCamera',
onSelect: this._getAndSetVideoTrack,
selectedDeviceId: currentVideoId
},
{
devices: availableDevices.audioInput,
hasPermission: this.props.hasAudioPermission,
isDisabled: this.props.disableAudioInputChange
|| this.props.disableDeviceChange,
key: 'audioInput',
label: 'settings.selectMic',
onSelect: this._getAndSetAudioTrack,
selectedDeviceId: currentAudioId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
devices: availableDevices.audioOutput,
hasPermission: this.props.hasAudioPermission
|| this.props.hasVideoPermission,
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: this._getAndSetAudioOutput,
selectedDeviceId: currentAudioOutId
});
}
return configurations.map(this._renderSelector);
}
}
/**
* Maps (parts of) the Redux state to the associated DeviceSelectionDialog's
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _devices: Object
* }}
*/
function _mapStateToProps(state) {
return {
_devices: state['features/base/devices']
};
}
export default translate(connect(_mapStateToProps)(DeviceSelectionDialog));

View File

@ -0,0 +1,180 @@
import Select from '@atlaskit/single-select';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
/**
* React component for selecting a device from a select element. Wraps Select
* with device selection specific logic.
*
* @extends Component
*/
class DeviceSelector extends Component {
/**
* DeviceSelector component's property types.
*
* @static
*/
static propTypes = {
/**
* MediaDeviceInfos used for display in the select element.
*/
devices: React.PropTypes.array,
/**
* If false, will return a selector with no selection options.
*/
hasPermission: React.PropTypes.bool,
/**
* If true, will render the selector disabled with a default selection.
*/
isDisabled: React.PropTypes.bool,
/**
* The translation key to display as a menu label.
*/
label: React.PropTypes.string,
/**
* The callback to invoke when a selection is made.
*/
onSelect: React.PropTypes.func,
/**
* The default device to display as selected.
*/
selectedDeviceId: React.PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
}
/**
* Initializes a new DeviceSelector instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this._onSelect = this._onSelect.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
if (!this.props.hasPermission) {
return this._renderNoPermission();
}
if (!this.props.devices.length) {
return this._renderNoDevices();
}
const items = this.props.devices.map(this._createSelectItem);
const defaultSelected = items.find(item =>
item.value === this.props.selectedDeviceId
);
return this._createSelector({
defaultSelected,
isDisabled: this.props.isDisabled,
items,
placeholder: 'deviceSelection.selectADevice'
});
}
/**
* Creates an object in the format expected by Select for an option element.
*
* @param {MediaDeviceInfo} device - An object with a label and a deviceId.
* @private
* @returns {Object} The passed in media device description converted to a
* format recognized as a valid Select item.
*/
_createSelectItem(device) {
return {
content: device.label,
value: device.deviceId
};
}
/**
* Creates a Select Component using passed in props and options.
*
* @param {Object} options - Additional configuration for display Select.
* @param {Object} options.defaultSelected - The option that should be set
* as currently chosen.
* @param {boolean} options.isDisabled - If true Select will not open on
* click.
* @param {Array} options.items - All the selectable options to display.
* @param {string} options.placeholder - The translation key to display when
* no selection has been made.
* @private
* @returns {ReactElement}
*/
_createSelector(options) {
return (
<Select
defaultSelected = { options.defaultSelected }
isDisabled = { options.isDisabled }
isFirstChild = { true }
items = { [ { items: options.items || [] } ] }
label = { this.props.t(this.props.label) }
noMatchesFound
= { this.props.t('deviceSelection.noOtherDevices') }
onSelected = { this._onSelect }
placeholder = { this.props.t(options.placeholder) }
shouldFitContainer = { true } />
);
}
/**
* Invokes the passed in callback to notify of selection changes.
*
* @param {Object} selection - Event returned from Select.
* @private
* @returns {void}
*/
_onSelect(selection) {
this.props.onSelect(selection.item.value);
}
/**
* Creates a Select Component that is disabled and has a placeholder
* indicating there are no devices to select.
*
* @private
* @returns {ReactElement}
*/
_renderNoDevices() {
return this._createSelector({
isDisabled: true,
placeholder: 'settings.noDevice'
});
}
/**
* Creates a Select Component that is disabled and has a placeholder stating
* there is no permission to display the devices.
*
* @private
* @returns {ReactElement}
*/
_renderNoPermission() {
return this._createSelector({
isDisabled: true,
placeholder: 'settings.noPermission'
});
}
}
export default translate(DeviceSelector);

View File

@ -0,0 +1,203 @@
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
const VIDEO_MUTE_CLASS = 'video-muted';
/**
* React component for displaying video. This component defers to lib-jitsi-meet
* logic for rendering the video.
*
* @extends Component
*/
class VideoInputPreview extends Component {
/**
* VideoInputPreview component's property types.
*
* @static
*/
static propTypes = {
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func,
/*
* The JitsiLocalTrack to display.
*/
track: React.PropTypes.object
}
/**
* Initializes a new VideoInputPreview instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
super(props);
this._rootElement = null;
this._videoElement = null;
this._setRootElement = this._setRootElement.bind(this);
this._setVideoElement = this._setVideoElement.bind(this);
}
/**
* Invokes the library for rendering the video on initial display.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._attachTrack(this.props.track);
}
/**
* Remove any existing associations between the current previewed track and
* the component's video element.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._detachTrack(this.props.track);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'video-input-preview'
ref = { this._setRootElement }>
<video
autoPlay = { true }
className = 'video-input-preview-display flipVideoX'
ref = { this._setVideoElement } />
<div className = 'video-input-preview-muted'>
{ this.props.t('videothumbnail.muted') }
</div>
</div>
);
}
/**
* Only update when the deviceId has changed. This component is somewhat
* black-boxed from React's rendering so lib-jitsi-meet can instead handle
* updating of the video preview, which takes browser differences into
* consideration. For example, temasys's video object must be visible to
* update the displayed track, but React's re-rendering could potentially
* remove the video object from the page.
*
* @inheritdoc
* @returns {void}
*/
shouldComponentUpdate(nextProps) {
if (nextProps.track !== this.props.track) {
this._detachTrack(this.props.track);
this._attachTrack(nextProps.track);
}
return false;
}
/**
* Calls into the passed in track to associate the track with the
* component's video element and render video. Also sets the instance
* variable for the video element as the element the track attached to,
* which could be an Object if on a temasys supported browser.
*
* @param {JitsiLocalTrack} track - The library's track model which will be
* displayed.
* @private
* @returns {void}
*/
_attachTrack(track) {
if (!track) {
return;
}
// Do not attempt to display a preview if the track is muted, as the
// library will simply return a falsy value for the element anyway.
if (track.isMuted()) {
this._showMuteOverlay(true);
} else {
this._showMuteOverlay(false);
const updatedVideoElement = track.attach(this._videoElement);
this._setVideoElement(updatedVideoElement);
}
}
/**
* Removes the association to the component's video element from the passed
* in JitsiLocalTrack to stop the track from rendering. With temasys, the
* video element must still be visible for detaching to complete.
*
* @param {JitsiLocalTrack} track - The library's track model which needs
* to stop previewing in the video element.
* @private
* @returns {void}
*/
_detachTrack(track) {
// Detach the video element from the track only if it has already
// been attached. This accounts for a special case with temasys
// where if detach is being called before attach, the video
// element is converted to Object without updating this
// component's reference to the video element.
if (this._videoElement
&& track
&& track.containers.includes(this._videoElement)) {
track.detach(this._videoElement);
}
}
/**
* Sets the component's root element.
*
* @param {Object} element - The highest DOM element in the component.
* @private
* @returns {void}
*/
_setRootElement(element) {
this._rootElement = element;
}
/**
* Sets an instance variable for the component's video element so it can be
* referenced later for attaching and detaching a JitsiLocalTrack.
*
* @param {Object} element - DOM element for the component's video display.
* @private
* @returns {void}
*/
_setVideoElement(element) {
this._videoElement = element;
}
/**
* Adds or removes a class to the component's parent node to indicate mute
* status.
*
* @param {boolean} shouldShow - True if the mute class should be added and
* false if the class should be removed.
* @private
* @returns {void}
*/
_showMuteOverlay(shouldShow) {
if (shouldShow) {
this._rootElement.classList.add(VIDEO_MUTE_CLASS);
} else {
this._rootElement.classList.remove(VIDEO_MUTE_CLASS);
}
}
}
export default translate(VideoInputPreview);

View File

@ -0,0 +1 @@
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog';

View File

@ -0,0 +1 @@
export * from './components';