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.

DeviceSelectionDialog.js 19KB

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