Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

DeviceSelectionDialogBase.js 17KB

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