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.

mediaDeviceHelper.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /* global APP, JitsiMeetJS */
  2. import {
  3. notifyCameraError,
  4. notifyMicError
  5. } from '../../react/features/base/devices/actions.web';
  6. import {
  7. flattenAvailableDevices,
  8. getAudioOutputDeviceId
  9. } from '../../react/features/base/devices/functions.web';
  10. import { updateSettings } from '../../react/features/base/settings/actions';
  11. import {
  12. getUserSelectedCameraDeviceId,
  13. getUserSelectedMicDeviceId,
  14. getUserSelectedOutputDeviceId
  15. } from '../../react/features/base/settings/functions';
  16. /**
  17. * Determines if currently selected audio output device should be changed after
  18. * list of available devices has been changed.
  19. * @param {MediaDeviceInfo[]} newDevices
  20. * @returns {string|undefined} - ID of new audio output device to use, undefined
  21. * if audio output device should not be changed.
  22. */
  23. function getNewAudioOutputDevice(newDevices) {
  24. if (!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
  25. return;
  26. }
  27. const selectedAudioOutputDeviceId = getAudioOutputDeviceId();
  28. const availableAudioOutputDevices = newDevices.filter(
  29. d => d.kind === 'audiooutput');
  30. // Switch to 'default' audio output device if we don't have the selected one
  31. // available anymore.
  32. if (selectedAudioOutputDeviceId !== 'default'
  33. && !availableAudioOutputDevices.find(d =>
  34. d.deviceId === selectedAudioOutputDeviceId)) {
  35. return 'default';
  36. }
  37. const preferredAudioOutputDeviceId = getUserSelectedOutputDeviceId(APP.store.getState());
  38. // if the preferred one is not the selected and is available in the new devices
  39. // we want to use it as it was just added
  40. if (preferredAudioOutputDeviceId
  41. && preferredAudioOutputDeviceId !== selectedAudioOutputDeviceId
  42. && availableAudioOutputDevices.find(d => d.deviceId === preferredAudioOutputDeviceId)) {
  43. return preferredAudioOutputDeviceId;
  44. }
  45. }
  46. /**
  47. * Determines if currently selected audio input device should be changed after
  48. * list of available devices has been changed.
  49. * @param {MediaDeviceInfo[]} newDevices
  50. * @param {JitsiLocalTrack} localAudio
  51. * @param {boolean} newLabel
  52. * @returns {string|undefined} - ID of new microphone device to use, undefined
  53. * if audio input device should not be changed.
  54. */
  55. function getNewAudioInputDevice(newDevices, localAudio, newLabel) {
  56. const availableAudioInputDevices = newDevices.filter(
  57. d => d.kind === 'audioinput');
  58. const selectedAudioInputDeviceId = getUserSelectedMicDeviceId(APP.store.getState());
  59. const selectedAudioInputDevice = availableAudioInputDevices.find(
  60. d => d.deviceId === selectedAudioInputDeviceId);
  61. const localAudioDeviceId = localAudio?.getDeviceId();
  62. const localAudioDevice = availableAudioInputDevices.find(
  63. d => d.deviceId === localAudioDeviceId);
  64. // Here we handle case when no device was initially plugged, but
  65. // then it's connected OR new device was connected when previous
  66. // track has ended.
  67. if (!localAudio || localAudio.disposed || localAudio.isEnded()) {
  68. // If we have new audio device and permission to use it was granted
  69. // (label is not an empty string), then we will try to use the first
  70. // available device.
  71. if (selectedAudioInputDevice && selectedAudioInputDeviceId) {
  72. return selectedAudioInputDeviceId;
  73. } else if (availableAudioInputDevices.length
  74. && availableAudioInputDevices[0].label !== '') {
  75. return availableAudioInputDevices[0].deviceId;
  76. }
  77. } else if (selectedAudioInputDevice
  78. && selectedAudioInputDeviceId !== localAudioDeviceId) {
  79. if (newLabel) {
  80. // If a Firefox user with manual permission prompt chose a different
  81. // device from what we have stored as the preferred device we accept
  82. // and store that as the new preferred device.
  83. APP.store.dispatch(updateSettings({
  84. userSelectedMicDeviceId: localAudioDeviceId,
  85. userSelectedMicDeviceLabel: localAudioDevice.label
  86. }));
  87. } else {
  88. // And here we handle case when we already have some device working,
  89. // but we plug-in a "preferred" (previously selected in settings, stored
  90. // in local storage) device.
  91. return selectedAudioInputDeviceId;
  92. }
  93. }
  94. }
  95. /**
  96. * Determines if currently selected video input device should be changed after
  97. * list of available devices has been changed.
  98. * @param {MediaDeviceInfo[]} newDevices
  99. * @param {JitsiLocalTrack} localVideo
  100. * @param {boolean} newLabel
  101. * @returns {string|undefined} - ID of new camera device to use, undefined
  102. * if video input device should not be changed.
  103. */
  104. function getNewVideoInputDevice(newDevices, localVideo, newLabel) {
  105. const availableVideoInputDevices = newDevices.filter(
  106. d => d.kind === 'videoinput');
  107. const selectedVideoInputDeviceId = getUserSelectedCameraDeviceId(APP.store.getState());
  108. const selectedVideoInputDevice = availableVideoInputDevices.find(
  109. d => d.deviceId === selectedVideoInputDeviceId);
  110. const localVideoDeviceId = localVideo?.getDeviceId();
  111. const localVideoDevice = availableVideoInputDevices.find(
  112. d => d.deviceId === localVideoDeviceId);
  113. // Here we handle case when no video input device was initially plugged,
  114. // but then device is connected OR new device was connected when
  115. // previous track has ended.
  116. if (!localVideo || localVideo.disposed || localVideo.isEnded()) {
  117. // If we have new video device and permission to use it was granted
  118. // (label is not an empty string), then we will try to use the first
  119. // available device.
  120. if (selectedVideoInputDevice && selectedVideoInputDeviceId) {
  121. return selectedVideoInputDeviceId;
  122. } else if (availableVideoInputDevices.length
  123. && availableVideoInputDevices[0].label !== '') {
  124. return availableVideoInputDevices[0].deviceId;
  125. }
  126. } else if (selectedVideoInputDevice
  127. && selectedVideoInputDeviceId !== localVideoDeviceId) {
  128. if (newLabel) {
  129. // If a Firefox user with manual permission prompt chose a different
  130. // device from what we have stored as the preferred device we accept
  131. // and store that as the new preferred device.
  132. APP.store.dispatch(updateSettings({
  133. userSelectedCameraDeviceId: localVideoDeviceId,
  134. userSelectedCameraDeviceLabel: localVideoDevice.label
  135. }));
  136. } else {
  137. // And here we handle case when we already have some device working,
  138. // but we plug-in a "preferred" (previously selected in settings, stored
  139. // in local storage) device.
  140. return selectedVideoInputDeviceId;
  141. }
  142. }
  143. }
  144. export default {
  145. /**
  146. * Determines if currently selected media devices should be changed after
  147. * list of available devices has been changed.
  148. * @param {MediaDeviceInfo[]} newDevices
  149. * @param {JitsiLocalTrack} localVideo
  150. * @param {JitsiLocalTrack} localAudio
  151. * @returns {{
  152. * audioinput: (string|undefined),
  153. * videoinput: (string|undefined),
  154. * audiooutput: (string|undefined)
  155. * }}
  156. */
  157. getNewMediaDevicesAfterDeviceListChanged( // eslint-disable-line max-params
  158. newDevices,
  159. localVideo,
  160. localAudio,
  161. newLabels) {
  162. return {
  163. audioinput: getNewAudioInputDevice(newDevices, localAudio, newLabels),
  164. videoinput: getNewVideoInputDevice(newDevices, localVideo, newLabels),
  165. audiooutput: getNewAudioOutputDevice(newDevices)
  166. };
  167. },
  168. /**
  169. * Checks if the only difference between an object of known devices compared
  170. * to an array of new devices are only the labels for the devices.
  171. * @param {Object} oldDevices
  172. * @param {MediaDeviceInfo[]} newDevices
  173. * @returns {boolean}
  174. */
  175. newDeviceListAddedLabelsOnly(oldDevices, newDevices) {
  176. const oldDevicesFlattend = flattenAvailableDevices(oldDevices);
  177. if (oldDevicesFlattend.length !== newDevices.length) {
  178. return false;
  179. }
  180. oldDevicesFlattend.forEach(oldDevice => {
  181. if (oldDevice.label !== '') {
  182. return false;
  183. }
  184. const newDevice = newDevices.find(nd => nd.deviceId === oldDevice.deviceId);
  185. if (!newDevice || newDevice.label === '') {
  186. return false;
  187. }
  188. });
  189. return true;
  190. },
  191. /**
  192. * Tries to create new local tracks for new devices obtained after device
  193. * list changed. Shows error dialog in case of failures.
  194. * @param {function} createLocalTracks
  195. * @param {string} (cameraDeviceId)
  196. * @param {string} (micDeviceId)
  197. * @returns {Promise.<JitsiLocalTrack[]>}
  198. */
  199. createLocalTracksAfterDeviceListChanged(
  200. createLocalTracks,
  201. cameraDeviceId,
  202. micDeviceId) {
  203. let audioTrackError;
  204. let videoTrackError;
  205. const audioRequested = Boolean(micDeviceId);
  206. const videoRequested = Boolean(cameraDeviceId);
  207. if (audioRequested && videoRequested) {
  208. // First we try to create both audio and video tracks together.
  209. return (
  210. createLocalTracks({
  211. devices: [ 'audio', 'video' ],
  212. cameraDeviceId,
  213. micDeviceId
  214. })
  215. // If we fail to do this, try to create them separately.
  216. .catch(() => Promise.all([
  217. createAudioTrack(false).then(([ stream ]) => stream),
  218. createVideoTrack(false).then(([ stream ]) => stream)
  219. ]))
  220. .then(tracks => {
  221. if (audioTrackError) {
  222. APP.store.dispatch(notifyMicError(audioTrackError));
  223. }
  224. if (videoTrackError) {
  225. APP.store.dispatch(notifyCameraError(videoTrackError));
  226. }
  227. return tracks.filter(t => typeof t !== 'undefined');
  228. }));
  229. } else if (videoRequested && !audioRequested) {
  230. return createVideoTrack();
  231. } else if (audioRequested && !videoRequested) {
  232. return createAudioTrack();
  233. }
  234. return Promise.resolve([]);
  235. /**
  236. *
  237. */
  238. function createAudioTrack(showError = true) {
  239. return (
  240. createLocalTracks({
  241. devices: [ 'audio' ],
  242. cameraDeviceId: null,
  243. micDeviceId
  244. })
  245. .catch(err => {
  246. audioTrackError = err;
  247. showError && APP.store.dispatch(notifyMicError(err));
  248. return [];
  249. }));
  250. }
  251. /**
  252. *
  253. */
  254. function createVideoTrack(showError = true) {
  255. return (
  256. createLocalTracks({
  257. devices: [ 'video' ],
  258. cameraDeviceId,
  259. micDeviceId: null
  260. })
  261. .catch(err => {
  262. videoTrackError = err;
  263. showError && APP.store.dispatch(notifyCameraError(err));
  264. return [];
  265. }));
  266. }
  267. }
  268. };