Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

actions.web.ts 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. // @ts-expect-error
  2. import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from '../../../../modules/UI/UIErrors';
  3. import { IReduxState, IStore } from '../../app/types';
  4. import { showModeratedNotification } from '../../av-moderation/actions';
  5. import { shouldShowModeratedNotification } from '../../av-moderation/functions';
  6. import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
  7. import { showNotification } from '../../notifications/actions';
  8. import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
  9. import { stopReceiver } from '../../remote-control/actions';
  10. import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share/actions';
  11. import { isAudioOnlySharing, isScreenVideoShared } from '../../screen-share/functions';
  12. import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions';
  13. import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions';
  14. import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
  15. import { getCurrentConference } from '../conference/functions';
  16. import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
  17. import { setScreenshareMuted } from '../media/actions';
  18. import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
  19. import {
  20. addLocalTrack,
  21. replaceLocalTrack
  22. } from './actions.any';
  23. import {
  24. createLocalTracksF,
  25. getLocalDesktopTrack,
  26. getLocalJitsiAudioTrack
  27. } from './functions';
  28. import { IShareOptions, IToggleScreenSharingOptions } from './types';
  29. export * from './actions.any';
  30. /**
  31. * Signals that the local participant is ending screensharing or beginning the screensharing flow.
  32. *
  33. * @param {boolean} enabled - The state to toggle screen sharing to.
  34. * @param {boolean} audioOnly - Only share system audio.
  35. * @param {Object} shareOptions - The options to be passed for capturing screenshare.
  36. * @returns {Function}
  37. */
  38. export function toggleScreensharing(
  39. enabled?: boolean,
  40. audioOnly = false,
  41. shareOptions: IShareOptions = {}) {
  42. return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
  43. // check for A/V Moderation when trying to start screen sharing
  44. if ((enabled || enabled === undefined) && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, getState())) {
  45. dispatch(showModeratedNotification(MEDIA_TYPE.SCREENSHARE));
  46. return Promise.reject();
  47. }
  48. return _toggleScreenSharing({
  49. enabled,
  50. audioOnly,
  51. shareOptions
  52. }, {
  53. dispatch,
  54. getState
  55. });
  56. };
  57. }
  58. /**
  59. * Displays a UI notification for screensharing failure based on the error passed.
  60. *
  61. * @private
  62. * @param {Object} error - The error.
  63. * @param {Object} store - The redux store.
  64. * @returns {void}
  65. */
  66. function _handleScreensharingError(
  67. error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK,
  68. { dispatch }: IStore): void {
  69. if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
  70. return;
  71. }
  72. let descriptionKey, titleKey;
  73. if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
  74. descriptionKey = 'dialog.screenSharingPermissionDeniedError';
  75. titleKey = 'dialog.screenSharingFailedTitle';
  76. } else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
  77. descriptionKey = 'dialog.cameraConstraintFailedError';
  78. titleKey = 'deviceError.cameraError';
  79. } else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
  80. descriptionKey = 'dialog.screenSharingFailed';
  81. titleKey = 'dialog.screenSharingFailedTitle';
  82. } else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
  83. descriptionKey = 'notify.screenShareNoAudio';
  84. titleKey = 'notify.screenShareNoAudioTitle';
  85. }
  86. dispatch(showNotification({
  87. titleKey,
  88. descriptionKey
  89. }, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  90. }
  91. /**
  92. * Applies the AudioMixer effect on the local audio track if applicable. If there is no local audio track, the desktop
  93. * audio track is added to the conference.
  94. *
  95. * @private
  96. * @param {JitsiLocalTrack} desktopAudioTrack - The audio track to be added to the conference.
  97. * @param {*} state - The redux state.
  98. * @returns {void}
  99. */
  100. async function _maybeApplyAudioMixerEffect(desktopAudioTrack: any, state: IReduxState): Promise<void> {
  101. const localAudio = getLocalJitsiAudioTrack(state);
  102. const conference = getCurrentConference(state);
  103. if (localAudio) {
  104. // If there is a localAudio stream, mix in the desktop audio stream captured by the screen sharing API.
  105. const mixerEffect = new AudioMixerEffect(desktopAudioTrack);
  106. await localAudio.setEffect(mixerEffect);
  107. } else {
  108. // If no local stream is present ( i.e. no input audio devices) we use the screen share audio
  109. // stream as we would use a regular stream.
  110. await conference?.replaceTrack(null, desktopAudioTrack);
  111. }
  112. }
  113. /**
  114. * Toggles screen sharing.
  115. *
  116. * @private
  117. * @param {boolean} enabled - The state to toggle screen sharing to.
  118. * @param {Store} store - The redux store.
  119. * @returns {void}
  120. */
  121. async function _toggleScreenSharing(
  122. {
  123. enabled,
  124. audioOnly = false,
  125. shareOptions = {}
  126. }: IToggleScreenSharingOptions,
  127. store: IStore
  128. ): Promise<void> {
  129. const { dispatch, getState } = store;
  130. const state = getState();
  131. const audioOnlySharing = isAudioOnlySharing(state);
  132. const screenSharing = isScreenVideoShared(state);
  133. const conference = getCurrentConference(state);
  134. const localAudio = getLocalJitsiAudioTrack(state);
  135. const localScreenshare = getLocalDesktopTrack(state['features/base/tracks']);
  136. // Toggle screenshare or audio-only share if the new state is not passed. Happens in the following two cases.
  137. // 1. ShareAudioDialog passes undefined when the user hits continue in the share audio demo modal.
  138. // 2. Toggle screenshare called from the external API.
  139. const enable = audioOnly
  140. ? enabled ?? !audioOnlySharing
  141. : enabled ?? !screenSharing;
  142. const screensharingDetails: { sourceType?: string; } = {};
  143. if (enable) {
  144. let tracks;
  145. // Spot proxy stream.
  146. if (shareOptions.desktopStream) {
  147. tracks = [ shareOptions.desktopStream ];
  148. } else {
  149. const { _desktopSharingSourceDevice } = state['features/base/config'];
  150. if (!shareOptions.desktopSharingSources && _desktopSharingSourceDevice) {
  151. shareOptions.desktopSharingSourceDevice = _desktopSharingSourceDevice;
  152. }
  153. const options = {
  154. devices: [ VIDEO_TYPE.DESKTOP ],
  155. ...shareOptions
  156. };
  157. try {
  158. tracks = await createLocalTracksF(options) as any[];
  159. } catch (error) {
  160. _handleScreensharingError(error as any, store);
  161. throw error;
  162. }
  163. }
  164. const desktopAudioTrack = tracks.find(track => track.getType() === MEDIA_TYPE.AUDIO);
  165. const desktopVideoTrack = tracks.find(track => track.getType() === MEDIA_TYPE.VIDEO);
  166. if (audioOnly) {
  167. // Dispose the desktop track for audio-only screensharing.
  168. desktopVideoTrack.dispose();
  169. if (!desktopAudioTrack) {
  170. _handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
  171. throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
  172. }
  173. } else if (desktopVideoTrack) {
  174. if (localScreenshare) {
  175. await dispatch(replaceLocalTrack(localScreenshare.jitsiTrack, desktopVideoTrack, conference));
  176. } else {
  177. await dispatch(addLocalTrack(desktopVideoTrack));
  178. }
  179. if (isScreenshotCaptureEnabled(state, false, true)) {
  180. dispatch(toggleScreenshotCaptureSummary(true));
  181. }
  182. screensharingDetails.sourceType = desktopVideoTrack.sourceType;
  183. }
  184. // Apply the AudioMixer effect if there is a local audio track, add the desktop track to the conference
  185. // otherwise without unmuting the microphone.
  186. if (desktopAudioTrack) {
  187. // Noise suppression doesn't work with desktop audio because we can't chain track effects yet, disable it
  188. // first. We need to to wait for the effect to clear first or it might interfere with the audio mixer.
  189. await dispatch(setNoiseSuppressionEnabled(false));
  190. _maybeApplyAudioMixerEffect(desktopAudioTrack, state);
  191. dispatch(setScreenshareAudioTrack(desktopAudioTrack));
  192. // Handle the case where screen share was stopped from the browsers 'screen share in progress' window.
  193. if (audioOnly) {
  194. desktopAudioTrack?.on(
  195. JitsiTrackEvents.LOCAL_TRACK_STOPPED,
  196. () => dispatch(toggleScreensharing(undefined, true)));
  197. }
  198. }
  199. // Show notification about more bandwidth usage in audio-only mode if the user starts screensharing. This
  200. // doesn't apply to audio-only screensharing.
  201. const { enabled: bestPerformanceMode } = state['features/base/audio-only'];
  202. if (bestPerformanceMode && !audioOnly) {
  203. dispatch(showNotification({
  204. titleKey: 'notify.screenSharingAudioOnlyTitle',
  205. descriptionKey: 'notify.screenSharingAudioOnlyDescription'
  206. }, NOTIFICATION_TIMEOUT_TYPE.LONG));
  207. }
  208. } else {
  209. const { desktopAudioTrack } = state['features/screen-share'];
  210. dispatch(stopReceiver());
  211. dispatch(toggleScreenshotCaptureSummary(false));
  212. // Mute the desktop track instead of removing it from the conference since we don't want the client to signal
  213. // a source-remove to the remote peer for the screenshare track. Later when screenshare is enabled again, the
  214. // same sender will be re-used without the need for signaling a new ssrc through source-add.
  215. dispatch(setScreenshareMuted(true));
  216. if (desktopAudioTrack) {
  217. if (localAudio) {
  218. localAudio.setEffect(undefined);
  219. } else {
  220. await conference?.replaceTrack(desktopAudioTrack, null);
  221. }
  222. desktopAudioTrack.dispose();
  223. dispatch(setScreenshareAudioTrack(null));
  224. }
  225. }
  226. if (audioOnly) {
  227. dispatch(setScreenAudioShareState(enable));
  228. } else {
  229. // Notify the external API.
  230. APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
  231. }
  232. }