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.

AudioSettingsContent.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // @flow
  2. import React, { Component } from 'react';
  3. import { translate } from '../../../../base/i18n';
  4. import { IconMicrophoneHollow, IconVolumeEmpty } from '../../../../base/icons';
  5. import JitsiMeetJS from '../../../../base/lib-jitsi-meet';
  6. import { equals } from '../../../../base/redux';
  7. import { createLocalAudioTracks } from '../../../functions';
  8. import AudioSettingsHeader from './AudioSettingsHeader';
  9. import MicrophoneEntry from './MicrophoneEntry';
  10. import SpeakerEntry from './SpeakerEntry';
  11. const browser = JitsiMeetJS.util.browser;
  12. /**
  13. * Translates the default device label into a more user friendly one.
  14. *
  15. * @param {string} deviceId - The device Id.
  16. * @param {string} label - The device label.
  17. * @param {Function} t - The translation function.
  18. * @returns {string}
  19. */
  20. function transformDefaultDeviceLabel(deviceId, label, t) {
  21. return deviceId === 'default'
  22. ? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
  23. : label;
  24. }
  25. export type Props = {
  26. /**
  27. * The deviceId of the microphone in use.
  28. */
  29. currentMicDeviceId: string,
  30. /**
  31. * The deviceId of the output device in use.
  32. */
  33. currentOutputDeviceId: string,
  34. /**
  35. * Used to set a new microphone as the current one.
  36. */
  37. setAudioInputDevice: Function,
  38. /**
  39. * Used to set a new output device as the current one.
  40. */
  41. setAudioOutputDevice: Function,
  42. /**
  43. * A list of objects containing the labels and deviceIds
  44. * of all the output devices.
  45. */
  46. outputDevices: Object[],
  47. /**
  48. * A list with objects containing the labels and deviceIds
  49. * of all the input devices.
  50. */
  51. microphoneDevices: Object[],
  52. /**
  53. * Invoked to obtain translated strings.
  54. */
  55. t: Function
  56. };
  57. type State = {
  58. /**
  59. * An list of objects, each containing the microphone label, audio track, device id
  60. * and track error if the case.
  61. */
  62. audioTracks: Object[]
  63. }
  64. /**
  65. * Implements a React {@link Component} which displays a list of all
  66. * the audio input & output devices to choose from.
  67. *
  68. * @extends Component
  69. */
  70. class AudioSettingsContent extends Component<Props, State> {
  71. _componentWasUnmounted: boolean;
  72. _audioContentRef: Object;
  73. microphoneHeaderId = 'microphone_settings_header';
  74. speakerHeaderId = 'speaker_settings_header';
  75. /**
  76. * Initializes a new {@code AudioSettingsContent} instance.
  77. *
  78. * @param {Object} props - The read-only properties with which the new
  79. * instance is to be initialized.
  80. */
  81. constructor(props) {
  82. super(props);
  83. this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
  84. this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
  85. this._onEscClick = this._onEscClick.bind(this);
  86. this._audioContentRef = React.createRef();
  87. this.state = {
  88. audioTracks: props.microphoneDevices.map(({ deviceId, label }) => {
  89. return {
  90. deviceId,
  91. hasError: false,
  92. jitsiTrack: null,
  93. label
  94. };
  95. })
  96. };
  97. }
  98. _onEscClick: (KeyboardEvent) => void;
  99. /**
  100. * Click handler for the speaker entries.
  101. *
  102. * @param {KeyboardEvent} event - Esc key click to close the popup.
  103. * @returns {void}
  104. */
  105. _onEscClick(event) {
  106. if (event.key === 'Escape') {
  107. event.preventDefault();
  108. event.stopPropagation();
  109. this._audioContentRef.current.style.display = 'none';
  110. }
  111. }
  112. _onMicrophoneEntryClick: (string) => void;
  113. /**
  114. * Click handler for the microphone entries.
  115. *
  116. * @param {string} deviceId - The deviceId for the clicked microphone.
  117. * @returns {void}
  118. */
  119. _onMicrophoneEntryClick(deviceId) {
  120. this.props.setAudioInputDevice(deviceId);
  121. }
  122. _onSpeakerEntryClick: (string) => void;
  123. /**
  124. * Click handler for the speaker entries.
  125. *
  126. * @param {string} deviceId - The deviceId for the clicked speaker.
  127. * @returns {void}
  128. */
  129. _onSpeakerEntryClick(deviceId) {
  130. this.props.setAudioOutputDevice(deviceId);
  131. }
  132. /**
  133. * Renders a single microphone entry.
  134. *
  135. * @param {Object} data - An object with the deviceId, jitsiTrack & label of the microphone.
  136. * @param {number} index - The index of the element, used for creating a key.
  137. * @param {length} length - The length of the microphone list.
  138. * @param {Function} t - The translation function.
  139. * @returns {React$Node}
  140. */
  141. _renderMicrophoneEntry(data, index, length, t) {
  142. const { deviceId, jitsiTrack, hasError } = data;
  143. const label = transformDefaultDeviceLabel(deviceId, data.label, t);
  144. const isSelected = deviceId === this.props.currentMicDeviceId;
  145. return (
  146. <MicrophoneEntry
  147. deviceId = { deviceId }
  148. hasError = { hasError }
  149. index = { index }
  150. isSelected = { isSelected }
  151. jitsiTrack = { jitsiTrack }
  152. key = { `me-${index}` }
  153. length = { length }
  154. listHeaderId = { this.microphoneHeaderId }
  155. onClick = { this._onMicrophoneEntryClick }>
  156. {label}
  157. </MicrophoneEntry>
  158. );
  159. }
  160. /**
  161. * Renders a single speaker entry.
  162. *
  163. * @param {Object} data - An object with the deviceId and label of the speaker.
  164. * @param {number} index - The index of the element, used for creating a key.
  165. * @param {length} length - The length of the speaker list.
  166. * @param {Function} t - The translation function.
  167. * @returns {React$Node}
  168. */
  169. _renderSpeakerEntry(data, index, length, t) {
  170. const { deviceId } = data;
  171. const label = transformDefaultDeviceLabel(deviceId, data.label, t);
  172. const key = `se-${index}`;
  173. const isSelected = deviceId === this.props.currentOutputDeviceId;
  174. return (
  175. <SpeakerEntry
  176. deviceId = { deviceId }
  177. index = { index }
  178. isSelected = { isSelected }
  179. key = { key }
  180. length = { length }
  181. listHeaderId = { this.speakerHeaderId }
  182. onClick = { this._onSpeakerEntryClick }>
  183. {label}
  184. </SpeakerEntry>
  185. );
  186. }
  187. /**
  188. * Creates and updates the audio tracks.
  189. *
  190. * @returns {void}
  191. */
  192. async _setTracks() {
  193. if (browser.isWebKitBased()) {
  194. // It appears that at the time of this writing, creating audio tracks blocks the browser's main thread for
  195. // long time on safari. Wasn't able to confirm which part of track creation does the blocking exactly, but
  196. // not creating the tracks seems to help and makes the UI much more responsive.
  197. return;
  198. }
  199. this._disposeTracks(this.state.audioTracks);
  200. const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000);
  201. if (this._componentWasUnmounted) {
  202. this._disposeTracks(audioTracks);
  203. } else {
  204. this.setState({
  205. audioTracks
  206. });
  207. }
  208. }
  209. /**
  210. * Disposes the audio tracks.
  211. *
  212. * @param {Object} audioTracks - The object holding the audio tracks.
  213. * @returns {void}
  214. */
  215. _disposeTracks(audioTracks) {
  216. audioTracks.forEach(({ jitsiTrack }) => {
  217. jitsiTrack && jitsiTrack.dispose();
  218. });
  219. }
  220. /**
  221. * Implements React's {@link Component#componentDidMount}.
  222. *
  223. * @inheritdoc
  224. */
  225. componentDidMount() {
  226. this._setTracks();
  227. }
  228. /**
  229. * Implements React's {@link Component#componentWillUnmount}.
  230. *
  231. * @inheritdoc
  232. */
  233. componentWillUnmount() {
  234. this._componentWasUnmounted = true;
  235. this._disposeTracks(this.state.audioTracks);
  236. }
  237. /**
  238. * Implements React's {@link Component#componentDidUpdate}.
  239. *
  240. * @inheritdoc
  241. */
  242. componentDidUpdate(prevProps) {
  243. if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) {
  244. this._setTracks();
  245. }
  246. }
  247. /**
  248. * Implements React's {@link Component#render}.
  249. *
  250. * @inheritdoc
  251. */
  252. render() {
  253. const { outputDevices, t } = this.props;
  254. return (
  255. <div>
  256. <div
  257. aria-labelledby = 'audio-settings-button'
  258. className = 'audio-preview-content'
  259. id = 'audio-settings-dialog'
  260. onKeyDown = { this._onEscClick }
  261. ref = { this._audioContentRef }
  262. role = 'menu'
  263. tabIndex = { -1 }>
  264. <div role = 'menuitem'>
  265. <AudioSettingsHeader
  266. IconComponent = { IconMicrophoneHollow }
  267. id = { this.microphoneHeaderId }
  268. text = { t('settings.microphones') } />
  269. <ul
  270. aria-labelledby = 'microphone_settings_header'
  271. className = 'audio-preview-content-ul'
  272. role = 'radiogroup'
  273. tabIndex = '-1'>
  274. {this.state.audioTracks.map((data, i) =>
  275. this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t),
  276. )}
  277. </ul>
  278. </div>
  279. { outputDevices.length > 0 && (
  280. <div role = 'menuitem'>
  281. <hr className = 'audio-preview-hr' />
  282. <AudioSettingsHeader
  283. IconComponent = { IconVolumeEmpty }
  284. id = { this.speakerHeaderId }
  285. text = { t('settings.speakers') } />
  286. <ul
  287. aria-labelledby = 'speaker_settings_header'
  288. className = 'audio-preview-content-ul'
  289. role = 'radiogroup'
  290. tabIndex = '-1'>
  291. { outputDevices.map((data, i) =>
  292. this._renderSpeakerEntry(data, i, outputDevices.length, t),
  293. )}
  294. </ul>
  295. </div>)
  296. }
  297. </div>
  298. </div>
  299. );
  300. }
  301. }
  302. export default translate(AudioSettingsContent);