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

actions.web.ts 11KB

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