mirror of https://github.com/jitsi/jitsi-meet
The Device Selection modal consists of: - DeviceSelection, an overly smart component responsible for triggering stream creation and cleanup. - DeviceSelector for selector elements. - VideoInputPreview for displaying a video preview. - AudioInputPreview for displaying a volume meter. - AudioOutputPreview for a test sound output link. Store changes include is primarily storing the list of available devices in redux. Other app state has been left alone for future refactoring.pull/1447/head
parent
2ffef3bdda
commit
2f994b1227
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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'); |
@ -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 |
||||
}; |
||||
} |
@ -0,0 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './actionTypes'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -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); |
||||
}); |
@ -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') |
||||
}; |
||||
} |
@ -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; |
@ -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); |
@ -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)); |
@ -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); |
@ -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); |
@ -0,0 +1 @@ |
||||
export { default as DeviceSelectionDialog } from './DeviceSelectionDialog'; |
@ -0,0 +1 @@ |
||||
export * from './components'; |
Loading…
Reference in new issue