You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DeviceSelectionDialogBase.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import React, { Component } from 'react';
  2. import { StatelessDialog } from '../../base/dialog';
  3. import { translate } from '../../base/i18n';
  4. import { createLocalTrack } from '../../base/lib-jitsi-meet';
  5. import AudioInputPreview from './AudioInputPreview';
  6. import AudioOutputPreview from './AudioOutputPreview';
  7. import DeviceSelector from './DeviceSelector';
  8. import VideoInputPreview from './VideoInputPreview';
  9. /**
  10. * React component for previewing and selecting new audio and video sources.
  11. *
  12. * @extends Component
  13. */
  14. class DeviceSelectionDialogBase extends Component {
  15. /**
  16. * DeviceSelectionDialogBase component's property types.
  17. *
  18. * @static
  19. */
  20. static propTypes = {
  21. /**
  22. * All known audio and video devices split by type. This prop comes from
  23. * the app state.
  24. */
  25. availableDevices: React.PropTypes.object,
  26. /**
  27. * Closes the dialog.
  28. */
  29. closeModal: React.PropTypes.func,
  30. /**
  31. * Device id for the current audio input device. This device will be set
  32. * as the default audio input device to preview.
  33. */
  34. currentAudioInputId: React.PropTypes.string,
  35. /**
  36. * Device id for the current audio output device. This device will be
  37. * set as the default audio output device to preview.
  38. */
  39. currentAudioOutputId: React.PropTypes.string,
  40. /**
  41. * Device id for the current video input device. This device will be set
  42. * as the default video input device to preview.
  43. */
  44. currentVideoInputId: React.PropTypes.string,
  45. /**
  46. * Whether or not the audio selector can be interacted with. If true,
  47. * the audio input selector will be rendered as disabled. This is
  48. * specifically used to prevent audio device changing in Firefox, which
  49. * currently does not work due to a browser-side regression.
  50. */
  51. disableAudioInputChange: React.PropTypes.bool,
  52. /**
  53. * Disables dismissing the dialog when the blanket is clicked. Enabled
  54. * by default.
  55. */
  56. disableBlanketClickDismiss: React.PropTypes.bool,
  57. /**
  58. * True if device changing is configured to be disallowed. Selectors
  59. * will display as disabled.
  60. */
  61. disableDeviceChange: React.PropTypes.bool,
  62. /**
  63. * Function that checks whether or not a new audio input source can be
  64. * selected.
  65. */
  66. hasAudioPermission: React.PropTypes.func,
  67. /**
  68. * Function that checks whether or not a new video input sources can be
  69. * selected.
  70. */
  71. hasVideoPermission: React.PropTypes.func,
  72. /**
  73. * If true, the audio meter will not display. Necessary for browsers or
  74. * configurations that do not support local stats to prevent a
  75. * non-responsive mic preview from displaying.
  76. */
  77. hideAudioInputPreview: React.PropTypes.bool,
  78. /**
  79. * Whether or not the audio output source selector should display. If
  80. * true, the audio output selector and test audio link will not be
  81. * rendered. This is specifically used for hiding audio output on
  82. * temasys browsers which do not support such change.
  83. */
  84. hideAudioOutputSelect: React.PropTypes.bool,
  85. /**
  86. * Function that sets the audio input device.
  87. */
  88. setAudioInputDevice: React.PropTypes.func,
  89. /**
  90. * Function that sets the audio output device.
  91. */
  92. setAudioOutputDevice: React.PropTypes.func,
  93. /**
  94. * Function that sets the video input device.
  95. */
  96. setVideoInputDevice: React.PropTypes.func,
  97. /**
  98. * Invoked to obtain translated strings.
  99. */
  100. t: React.PropTypes.func
  101. };
  102. /**
  103. * Initializes a new DeviceSelectionDialogBase instance.
  104. *
  105. * @param {Object} props - The read-only React Component props with which
  106. * the new instance is to be initialized.
  107. */
  108. constructor(props) {
  109. super(props);
  110. const { availableDevices } = this.props;
  111. this.state = {
  112. // JitsiLocalTrack to use for live previewing of audio input.
  113. previewAudioTrack: null,
  114. // JitsiLocalTrack to use for live previewing of video input.
  115. previewVideoTrack: null,
  116. // An message describing a problem with obtaining a video preview.
  117. previewVideoTrackError: null,
  118. // The audio input device id to show as selected by default.
  119. selectedAudioInputId: this.props.currentAudioInputId || '',
  120. // The audio output device id to show as selected by default.
  121. selectedAudioOutputId: this.props.currentAudioOutputId || '',
  122. // The video input device id to show as selected by default.
  123. // FIXME: On temasys, without a device selected and put into local
  124. // storage as the default device to use, the current video device id
  125. // is a blank string. This is because the library gets a local video
  126. // track and then maps the track's device id by matching the track's
  127. // label to the MediaDeviceInfos returned from enumerateDevices. In
  128. // WebRTC, the track label is expected to return the camera device
  129. // label. However, temasys video track labels refer to track id, not
  130. // device label, so the library cannot match the track to a device.
  131. // The workaround of defaulting to the first videoInput available
  132. // is re-used from the previous device settings implementation.
  133. selectedVideoInputId: this.props.currentVideoInputId
  134. || (availableDevices.videoInput
  135. && availableDevices.videoInput[0]
  136. && availableDevices.videoInput[0].deviceId)
  137. || ''
  138. };
  139. // Preventing closing while cleaning up previews is important for
  140. // supporting temasys video cleanup. Temasys requires its video object
  141. // to be in the dom and visible for proper detaching of tracks. Delaying
  142. // closure until cleanup is complete ensures no errors in the process.
  143. this._isClosing = false;
  144. this._setDevicesAndClose = this._setDevicesAndClose.bind(this);
  145. this._onCancel = this._onCancel.bind(this);
  146. this._onSubmit = this._onSubmit.bind(this);
  147. this._updateAudioOutput = this._updateAudioOutput.bind(this);
  148. this._updateAudioInput = this._updateAudioInput.bind(this);
  149. this._updateVideoInput = this._updateVideoInput.bind(this);
  150. }
  151. /**
  152. * Sets default device choices so a choice is pre-selected in the dropdowns
  153. * and live previews are created.
  154. *
  155. * @inheritdoc
  156. */
  157. componentDidMount() {
  158. this._updateAudioOutput(this.state.selectedAudioOutputId);
  159. this._updateAudioInput(this.state.selectedAudioInputId);
  160. this._updateVideoInput(this.state.selectedVideoInputId);
  161. }
  162. /**
  163. * Disposes preview tracks that might not already be disposed.
  164. *
  165. * @inheritdoc
  166. */
  167. componentWillUnmount() {
  168. // This handles the case where neither submit nor cancel were triggered,
  169. // such as on modal switch. In that case, make a dying attempt to clean
  170. // up previews.
  171. if (!this._isClosing) {
  172. this._attemptPreviewTrackCleanup();
  173. }
  174. }
  175. /**
  176. * Implements React's {@link Component#render()}.
  177. *
  178. * @inheritdoc
  179. */
  180. render() {
  181. return (
  182. <StatelessDialog
  183. cancelTitleKey = { 'dialog.Cancel' }
  184. disableBlanketClickDismiss
  185. = { this.props.disableBlanketClickDismiss }
  186. okTitleKey = { 'dialog.Save' }
  187. onCancel = { this._onCancel }
  188. onSubmit = { this._onSubmit }
  189. titleKey = 'deviceSelection.deviceSettings'>
  190. <div className = 'device-selection'>
  191. <div className = 'device-selection-column column-video'>
  192. <div className = 'device-selection-video-container'>
  193. <VideoInputPreview
  194. error = { this.state.previewVideoTrackError }
  195. track = { this.state.previewVideoTrack } />
  196. </div>
  197. { this._renderAudioInputPreview() }
  198. </div>
  199. <div className = 'device-selection-column column-selectors'>
  200. <div className = 'device-selectors'>
  201. { this._renderSelectors() }
  202. </div>
  203. { this._renderAudioOutputPreview() }
  204. </div>
  205. </div>
  206. </StatelessDialog>
  207. );
  208. }
  209. /**
  210. * Cleans up preview tracks if they are not active tracks.
  211. *
  212. * @private
  213. * @returns {Array<Promise>} Zero to two promises will be returned. One
  214. * promise can be for video cleanup and another for audio cleanup.
  215. */
  216. _attemptPreviewTrackCleanup() {
  217. return Promise.all([
  218. this._disposeVideoPreview(),
  219. this._disposeAudioPreview()
  220. ]);
  221. }
  222. /**
  223. * Utility function for disposing the current audio preview.
  224. *
  225. * @private
  226. * @returns {Promise}
  227. */
  228. _disposeAudioPreview() {
  229. return this.state.previewAudioTrack
  230. ? this.state.previewAudioTrack.dispose() : Promise.resolve();
  231. }
  232. /**
  233. * Utility function for disposing the current video preview.
  234. *
  235. * @private
  236. * @returns {Promise}
  237. */
  238. _disposeVideoPreview() {
  239. return this.state.previewVideoTrack
  240. ? this.state.previewVideoTrack.dispose() : Promise.resolve();
  241. }
  242. /**
  243. * Disposes preview tracks and signals to
  244. * close DeviceSelectionDialogBase.
  245. *
  246. * @private
  247. * @returns {boolean} Returns false to prevent closure until cleanup is
  248. * complete.
  249. */
  250. _onCancel() {
  251. if (this._isClosing) {
  252. return false;
  253. }
  254. this._isClosing = true;
  255. const cleanupPromises = this._attemptPreviewTrackCleanup();
  256. Promise.all(cleanupPromises)
  257. .then(this.props.closeModal)
  258. .catch(this.props.closeModal);
  259. return false;
  260. }
  261. /**
  262. * Identifies changes to the preferred input/output devices and perform
  263. * necessary cleanup and requests to use those devices. Closes the modal
  264. * after cleanup and device change requests complete.
  265. *
  266. * @private
  267. * @returns {boolean} Returns false to prevent closure until cleanup is
  268. * complete.
  269. */
  270. _onSubmit() {
  271. if (this._isClosing) {
  272. return false;
  273. }
  274. this._isClosing = true;
  275. this._attemptPreviewTrackCleanup()
  276. .then(this._setDevicesAndClose, this._setDevicesAndClose);
  277. return false;
  278. }
  279. /**
  280. * Creates an AudioInputPreview for previewing if audio is being received.
  281. * Null will be returned if local stats for tracking audio input levels
  282. * cannot be obtained.
  283. *
  284. * @private
  285. * @returns {ReactComponent|null}
  286. */
  287. _renderAudioInputPreview() {
  288. if (this.props.hideAudioInputPreview) {
  289. return null;
  290. }
  291. return (
  292. <AudioInputPreview
  293. track = { this.state.previewAudioTrack } />
  294. );
  295. }
  296. /**
  297. * Creates an AudioOutputPreview instance for playing a test sound with the
  298. * passed in device id. Null will be returned if hideAudioOutput is truthy.
  299. *
  300. * @private
  301. * @returns {ReactComponent|null}
  302. */
  303. _renderAudioOutputPreview() {
  304. if (this.props.hideAudioOutputSelect) {
  305. return null;
  306. }
  307. return (
  308. <AudioOutputPreview
  309. deviceId = { this.state.selectedAudioOutputId } />
  310. );
  311. }
  312. /**
  313. * Creates a DeviceSelector instance based on the passed in configuration.
  314. *
  315. * @private
  316. * @param {Object} props - The props for the DeviceSelector.
  317. * @returns {ReactElement}
  318. */
  319. _renderSelector(props) {
  320. return (
  321. <DeviceSelector { ...props } />
  322. );
  323. }
  324. /**
  325. * Creates DeviceSelector instances for video output, audio input, and audio
  326. * output.
  327. *
  328. * @private
  329. * @returns {Array<ReactElement>} DeviceSelector instances.
  330. */
  331. _renderSelectors() {
  332. const { availableDevices } = this.props;
  333. const configurations = [
  334. {
  335. devices: availableDevices.videoInput,
  336. hasPermission: this.props.hasVideoPermission(),
  337. icon: 'icon-camera',
  338. isDisabled: this.props.disableDeviceChange,
  339. key: 'videoInput',
  340. label: 'settings.selectCamera',
  341. onSelect: this._updateVideoInput,
  342. selectedDeviceId: this.state.selectedVideoInputId
  343. },
  344. {
  345. devices: availableDevices.audioInput,
  346. hasPermission: this.props.hasAudioPermission(),
  347. icon: 'icon-microphone',
  348. isDisabled: this.props.disableAudioInputChange
  349. || this.props.disableDeviceChange,
  350. key: 'audioInput',
  351. label: 'settings.selectMic',
  352. onSelect: this._updateAudioInput,
  353. selectedDeviceId: this.state.selectedAudioInputId
  354. }
  355. ];
  356. if (!this.props.hideAudioOutputSelect) {
  357. configurations.push({
  358. devices: availableDevices.audioOutput,
  359. hasPermission: this.props.hasAudioPermission()
  360. || this.props.hasVideoPermission(),
  361. icon: 'icon-volume',
  362. isDisabled: this.props.disableDeviceChange,
  363. key: 'audioOutput',
  364. label: 'settings.selectAudioOutput',
  365. onSelect: this._updateAudioOutput,
  366. selectedDeviceId: this.state.selectedAudioOutputId
  367. });
  368. }
  369. return configurations.map(this._renderSelector);
  370. }
  371. /**
  372. * Sets the selected devices and closes the dialog.
  373. *
  374. * @returns {void}
  375. */
  376. _setDevicesAndClose() {
  377. const {
  378. setVideoInputDevice,
  379. setAudioInputDevice,
  380. setAudioOutputDevice,
  381. closeModal
  382. } = this.props;
  383. const promises = [];
  384. if (this.state.selectedVideoInputId
  385. !== this.props.currentVideoInputId) {
  386. promises.push(setVideoInputDevice(this.state.selectedVideoInputId));
  387. }
  388. if (this.state.selectedAudioInputId
  389. !== this.props.currentAudioInputId) {
  390. promises.push(setAudioInputDevice(this.state.selectedAudioInputId));
  391. }
  392. if (this.state.selectedAudioOutputId
  393. !== this.props.currentAudioOutputId) {
  394. promises.push(
  395. setAudioOutputDevice(this.state.selectedAudioOutputId));
  396. }
  397. Promise.all(promises).then(closeModal, closeModal);
  398. }
  399. /**
  400. * Callback invoked when a new audio input device has been selected. Updates
  401. * the internal state of the user's selection as well as the audio track
  402. * that should display in the preview.
  403. *
  404. * @param {string} deviceId - The id of the chosen audio input device.
  405. * @private
  406. * @returns {void}
  407. */
  408. _updateAudioInput(deviceId) {
  409. this.setState({
  410. selectedAudioInputId: deviceId
  411. }, () => {
  412. this._disposeAudioPreview()
  413. .then(() => createLocalTrack('audio', deviceId))
  414. .then(jitsiLocalTrack => {
  415. this.setState({
  416. previewAudioTrack: jitsiLocalTrack
  417. });
  418. })
  419. .catch(() => {
  420. this.setState({
  421. previewAudioTrack: null
  422. });
  423. });
  424. });
  425. }
  426. /**
  427. * Callback invoked when a new audio output device has been selected.
  428. * Updates the internal state of the user's selection.
  429. *
  430. * @param {string} deviceId - The id of the chosen audio output device.
  431. * @private
  432. * @returns {void}
  433. */
  434. _updateAudioOutput(deviceId) {
  435. this.setState({
  436. selectedAudioOutputId: deviceId
  437. });
  438. }
  439. /**
  440. * Callback invoked when a new video input device has been selected. Updates
  441. * the internal state of the user's selection as well as the video track
  442. * that should display in the preview.
  443. *
  444. * @param {string} deviceId - The id of the chosen video input device.
  445. * @private
  446. * @returns {void}
  447. */
  448. _updateVideoInput(deviceId) {
  449. this.setState({
  450. selectedVideoInputId: deviceId
  451. }, () => {
  452. this._disposeVideoPreview()
  453. .then(() => createLocalTrack('video', deviceId))
  454. .then(jitsiLocalTrack => {
  455. this.setState({
  456. previewVideoTrack: jitsiLocalTrack,
  457. previewVideoTrackError: null
  458. });
  459. })
  460. .catch(() => {
  461. this.setState({
  462. previewVideoTrack: null,
  463. previewVideoTrackError:
  464. this.props.t('deviceSelection.previewUnavailable')
  465. });
  466. });
  467. });
  468. }
  469. }
  470. export default translate(DeviceSelectionDialogBase);