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

actions.web.ts 12KB

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