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

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