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.

middleware.web.js 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // @flow
  2. import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
  3. import { showNotification, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications';
  4. import {
  5. setPrejoinPageVisibility,
  6. setSkipPrejoinOnReload
  7. } from '../../prejoin';
  8. import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share';
  9. import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
  10. import { setAudioOnly } from '../audio-only';
  11. import { getMultipleVideoSupportFeatureFlag } from '../config/functions.any';
  12. import { JitsiConferenceErrors, JitsiTrackErrors } from '../lib-jitsi-meet';
  13. import { MEDIA_TYPE, setScreenshareMuted, VIDEO_TYPE } from '../media';
  14. import { MiddlewareRegistry } from '../redux';
  15. import {
  16. addLocalTrack,
  17. createLocalTracksF,
  18. getLocalDesktopTrack,
  19. getLocalJitsiAudioTrack,
  20. replaceLocalTrack,
  21. TOGGLE_SCREENSHARING
  22. } from '../tracks';
  23. import { CONFERENCE_FAILED, CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_JOINED } from './actionTypes';
  24. import { getCurrentConference } from './functions';
  25. import './middleware.any';
  26. MiddlewareRegistry.register(store => next => action => {
  27. const { dispatch, getState } = store;
  28. const { enableForcedReload } = getState()['features/base/config'];
  29. switch (action.type) {
  30. case CONFERENCE_JOIN_IN_PROGRESS: {
  31. dispatch(setPrejoinPageVisibility(false));
  32. break;
  33. }
  34. case CONFERENCE_JOINED: {
  35. if (enableForcedReload) {
  36. dispatch(setSkipPrejoinOnReload(false));
  37. }
  38. break;
  39. }
  40. case CONFERENCE_FAILED: {
  41. enableForcedReload
  42. && action.error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
  43. && dispatch(setSkipPrejoinOnReload(true));
  44. break;
  45. }
  46. case TOGGLE_SCREENSHARING: {
  47. getMultipleVideoSupportFeatureFlag(getState()) && _toggleScreenSharing(action, store);
  48. break;
  49. }
  50. }
  51. return next(action);
  52. });
  53. /**
  54. * Displays a UI notification for screensharing failure based on the error passed.
  55. *
  56. * @private
  57. * @param {Object} error - The error.
  58. * @param {Object} store - The redux store.
  59. * @returns {void}
  60. */
  61. function _handleScreensharingError(error, { dispatch }) {
  62. if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
  63. return;
  64. }
  65. let descriptionKey, titleKey;
  66. if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
  67. descriptionKey = 'dialog.screenSharingPermissionDeniedError';
  68. titleKey = 'dialog.screenSharingFailedTitle';
  69. } else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
  70. descriptionKey = 'dialog.cameraConstraintFailedError';
  71. titleKey = 'deviceError.cameraError';
  72. } else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
  73. descriptionKey = 'dialog.screenSharingFailed';
  74. titleKey = 'dialog.screenSharingFailedTitle';
  75. } else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
  76. descriptionKey = 'notify.screenShareNoAudio';
  77. titleKey = 'notify.screenShareNoAudioTitle';
  78. }
  79. dispatch(showNotification({
  80. titleKey,
  81. descriptionKey
  82. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  83. }
  84. /**
  85. * Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop
  86. * audio track is added to the conference.
  87. *
  88. * @private
  89. * @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference.
  90. * @param {*} state - The redux state.
  91. * @returns {void}
  92. */
  93. async function _maybeApplyAudioMixerEffect(desktopAudioTrack, state) {
  94. const localAudio = getLocalJitsiAudioTrack(state);
  95. const conference = getCurrentConference(state);
  96. if (localAudio) {
  97. // If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API.
  98. const mixerEffect = new AudioMixerEffect(desktopAudioTrack);
  99. await localAudio.setEffect(mixerEffect);
  100. } else {
  101. // If no local stream is present ( i.e. no input audio devices) we use the screen share audio
  102. // stream as we would use a regular stream.
  103. await conference.replaceTrack(null, desktopAudioTrack);
  104. }
  105. }
  106. /**
  107. * Toggles screen sharing.
  108. *
  109. * @private
  110. * @param {boolean} enabled - The state to toggle screen sharing to.
  111. * @param {Store} store - The redux store.
  112. * @returns {void}
  113. */
  114. async function _toggleScreenSharing({ enabled, audioOnly = false }, store) {
  115. const { dispatch, getState } = store;
  116. const state = getState();
  117. const conference = getCurrentConference(state);
  118. const localAudio = getLocalJitsiAudioTrack(state);
  119. const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
  120. if (enabled) {
  121. let tracks;
  122. try {
  123. tracks = await createLocalTracksF({ devices: [ VIDEO_TYPE.DESKTOP ] });
  124. } catch (error) {
  125. _handleScreensharingError(error, store);
  126. return;
  127. }
  128. const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO);
  129. const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO);
  130. // Dispose the desktop track for audio-only screensharing.
  131. if (audioOnly) {
  132. desktopVideoTrack.dispose();
  133. if (!desktopAudioTrack) {
  134. _handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
  135. return;
  136. }
  137. } else if (desktopVideoTrack) {
  138. if (localScreenshare) {
  139. await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
  140. } else {
  141. await dispatch(addLocalTrack(desktopVideoTrack));
  142. }
  143. }
  144. // Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
  145. // otherwise without unmuting the microphone.
  146. if (desktopAudioTrack) {
  147. _maybeApplyAudioMixerEffect(desktopAudioTrack, state);
  148. dispatch(setScreenshareAudioTrack(desktopAudioTrack));
  149. }
  150. // Disable audio-only or best performance mode if the user starts screensharing. This doesn't apply to
  151. // audio-only screensharing.
  152. const { enabled: bestPerformanceMode } = state['features/base/audio-only'];
  153. if (bestPerformanceMode && !audioOnly) {
  154. dispatch(setAudioOnly(false));
  155. }
  156. } else {
  157. const { desktopAudioTrack } = state['features/screen-share'];
  158. // Mute the desktop track instead of removing it from the conference since we don't want the client to signal
  159. // a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
  160. // same sender will be re-used without the need for signaling a new ssrc through source-add.
  161. dispatch(setScreenshareMuted(true));
  162. if (desktopAudioTrack) {
  163. if (localAudio) {
  164. localAudio.setEffect(undefined);
  165. } else {
  166. await conference.replaceTrack(desktopAudioTrack, null);
  167. }
  168. desktopAudioTrack.dispose();
  169. dispatch(setScreenshareAudioTrack(null));
  170. }
  171. }
  172. if (audioOnly) {
  173. dispatch(setScreenAudioShareState(enabled));
  174. }
  175. }