您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

DeviceSelectionDialog.js 17KB

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