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.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import { createRecordingEvent } from '../analytics/AnalyticsEvents';
  2. import { sendAnalytics } from '../analytics/functions';
  3. import { IStore } from '../app/types';
  4. import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
  5. import { CONFERENCE_JOIN_IN_PROGRESS } from '../base/conference/actionTypes';
  6. import { getCurrentConference } from '../base/conference/functions';
  7. import JitsiMeetJS, {
  8. JitsiConferenceEvents,
  9. JitsiRecordingConstants
  10. } from '../base/lib-jitsi-meet';
  11. import { MEDIA_TYPE } from '../base/media/constants';
  12. import { PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
  13. import { updateLocalRecordingStatus } from '../base/participants/actions';
  14. import { PARTICIPANT_ROLE } from '../base/participants/constants';
  15. import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
  16. import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
  17. import StateListenerRegistry from '../base/redux/StateListenerRegistry';
  18. import {
  19. playSound,
  20. stopSound
  21. } from '../base/sounds/actions';
  22. import { TRACK_ADDED } from '../base/tracks/actionTypes';
  23. import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
  24. import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
  25. import { isRecorderTranscriptionsRunning } from '../transcribing/functions';
  26. import { RECORDING_SESSION_UPDATED, START_LOCAL_RECORDING, STOP_LOCAL_RECORDING } from './actionTypes';
  27. import {
  28. clearRecordingSessions,
  29. hidePendingRecordingNotification,
  30. showPendingRecordingNotification,
  31. showRecordingError,
  32. showRecordingLimitNotification,
  33. showRecordingWarning,
  34. showStartRecordingNotification,
  35. showStartedRecordingNotification,
  36. showStoppedRecordingNotification,
  37. updateRecordingSessionData
  38. } from './actions';
  39. import LocalRecordingManager from './components/Recording/LocalRecordingManager';
  40. import {
  41. LIVE_STREAMING_OFF_SOUND_ID,
  42. LIVE_STREAMING_ON_SOUND_ID,
  43. RECORDING_OFF_SOUND_ID,
  44. RECORDING_ON_SOUND_ID,
  45. START_RECORDING_NOTIFICATION_ID
  46. } from './constants';
  47. import {
  48. getResourceId,
  49. getSessionById,
  50. registerRecordingAudioFiles,
  51. unregisterRecordingAudioFiles
  52. } from './functions';
  53. import logger from './logger';
  54. /**
  55. * StateListenerRegistry provides a reliable way to detect the leaving of a
  56. * conference, where we need to clean up the recording sessions.
  57. */
  58. StateListenerRegistry.register(
  59. /* selector */ state => getCurrentConference(state),
  60. /* listener */ (conference, { dispatch }) => {
  61. if (!conference) {
  62. dispatch(clearRecordingSessions());
  63. }
  64. }
  65. );
  66. /**
  67. * The redux middleware to handle the recorder updates in a React way.
  68. *
  69. * @param {Store} store - The redux store.
  70. * @returns {Function}
  71. */
  72. MiddlewareRegistry.register(({ dispatch, getState }) => next => async action => {
  73. let oldSessionData;
  74. if (action.type === RECORDING_SESSION_UPDATED) {
  75. oldSessionData
  76. = getSessionById(getState(), action.sessionData.id);
  77. }
  78. const result = next(action);
  79. switch (action.type) {
  80. case APP_WILL_MOUNT:
  81. registerRecordingAudioFiles(dispatch);
  82. break;
  83. case APP_WILL_UNMOUNT:
  84. unregisterRecordingAudioFiles(dispatch);
  85. break;
  86. case CONFERENCE_JOIN_IN_PROGRESS: {
  87. const { conference } = action;
  88. conference.on(
  89. JitsiConferenceEvents.RECORDER_STATE_CHANGED,
  90. (recorderSession: any) => {
  91. if (recorderSession) {
  92. recorderSession.getID() && dispatch(updateRecordingSessionData(recorderSession));
  93. recorderSession.getError() && _showRecordingErrorNotification(recorderSession, dispatch, getState);
  94. }
  95. return;
  96. });
  97. break;
  98. }
  99. case START_LOCAL_RECORDING: {
  100. const { localRecording } = getState()['features/base/config'];
  101. const { onlySelf } = action;
  102. try {
  103. await LocalRecordingManager.startLocalRecording({ dispatch,
  104. getState }, action.onlySelf);
  105. const props = {
  106. descriptionKey: 'recording.on',
  107. titleKey: 'dialog.recording'
  108. };
  109. if (localRecording?.notifyAllParticipants && !onlySelf) {
  110. dispatch(playSound(RECORDING_ON_SOUND_ID));
  111. }
  112. dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  113. dispatch(showNotification({
  114. titleKey: 'recording.localRecordingStartWarningTitle',
  115. descriptionKey: 'recording.localRecordingStartWarning'
  116. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  117. dispatch(updateLocalRecordingStatus(true, onlySelf));
  118. sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
  119. if (typeof APP !== 'undefined') {
  120. APP.API.notifyRecordingStatusChanged(
  121. true, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
  122. }
  123. } catch (err: any) {
  124. logger.error('Capture failed', err);
  125. let descriptionKey = 'recording.error';
  126. if (err.message === 'WrongSurfaceSelected') {
  127. descriptionKey = 'recording.surfaceError';
  128. } else if (err.message === 'NoLocalStreams') {
  129. descriptionKey = 'recording.noStreams';
  130. } else if (err.message === 'NoMicTrack') {
  131. descriptionKey = 'recording.noMicPermission';
  132. }
  133. const props = {
  134. descriptionKey,
  135. titleKey: 'recording.failedToStart'
  136. };
  137. if (typeof APP !== 'undefined') {
  138. APP.API.notifyRecordingStatusChanged(
  139. false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
  140. }
  141. dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  142. }
  143. break;
  144. }
  145. case STOP_LOCAL_RECORDING: {
  146. const { localRecording } = getState()['features/base/config'];
  147. if (LocalRecordingManager.isRecordingLocally()) {
  148. LocalRecordingManager.stopLocalRecording();
  149. dispatch(updateLocalRecordingStatus(false));
  150. if (localRecording?.notifyAllParticipants && !LocalRecordingManager.selfRecording) {
  151. dispatch(playSound(RECORDING_OFF_SOUND_ID));
  152. }
  153. if (typeof APP !== 'undefined') {
  154. APP.API.notifyRecordingStatusChanged(
  155. false, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
  156. }
  157. }
  158. break;
  159. }
  160. case RECORDING_SESSION_UPDATED: {
  161. const state = getState();
  162. // When in recorder mode no notifications are shown
  163. // or extra sounds are also not desired
  164. // but we want to indicate those in case of sip gateway
  165. const {
  166. iAmRecorder,
  167. iAmSipGateway,
  168. recordingLimit
  169. } = state['features/base/config'];
  170. if (iAmRecorder && !iAmSipGateway) {
  171. break;
  172. }
  173. const updatedSessionData
  174. = getSessionById(state, action.sessionData.id);
  175. const { initiator, mode = '', terminator } = updatedSessionData ?? {};
  176. const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
  177. if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
  178. dispatch(showPendingRecordingNotification(mode));
  179. dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
  180. } else {
  181. dispatch(hidePendingRecordingNotification(mode));
  182. if (updatedSessionData?.status === ON) {
  183. // We receive 2 updates of the session status ON. The first one is from jibri when it joins.
  184. // The second one is from jicofo which will deliever the initiator value. Since the start
  185. // recording notification uses the initiator value we skip the jibri update and show the
  186. // notification on the update from jicofo.
  187. // FIXE: simplify checks when the backend start sending only one status ON update containing the
  188. // initiator.
  189. if (initiator && !oldSessionData?.initiator) {
  190. if (typeof recordingLimit === 'object') {
  191. dispatch(showRecordingLimitNotification(mode));
  192. } else {
  193. dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
  194. }
  195. }
  196. if (oldSessionData?.status !== ON) {
  197. sendAnalytics(createRecordingEvent('start', mode));
  198. let soundID;
  199. if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
  200. soundID = RECORDING_ON_SOUND_ID;
  201. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  202. soundID = LIVE_STREAMING_ON_SOUND_ID;
  203. }
  204. if (soundID) {
  205. dispatch(playSound(soundID));
  206. }
  207. if (typeof APP !== 'undefined') {
  208. APP.API.notifyRecordingStatusChanged(
  209. true, mode, undefined, isRecorderTranscriptionsRunning(state));
  210. }
  211. }
  212. } else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
  213. if (terminator) {
  214. dispatch(
  215. showStoppedRecordingNotification(
  216. mode, getParticipantDisplayName(state, getResourceId(terminator))));
  217. }
  218. let duration = 0, soundOff, soundOn;
  219. if (oldSessionData?.timestamp) {
  220. duration
  221. = (Date.now() / 1000) - oldSessionData.timestamp;
  222. }
  223. sendAnalytics(createRecordingEvent('stop', mode, duration));
  224. if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
  225. soundOff = RECORDING_OFF_SOUND_ID;
  226. soundOn = RECORDING_ON_SOUND_ID;
  227. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  228. soundOff = LIVE_STREAMING_OFF_SOUND_ID;
  229. soundOn = LIVE_STREAMING_ON_SOUND_ID;
  230. }
  231. if (soundOff && soundOn) {
  232. dispatch(stopSound(soundOn));
  233. dispatch(playSound(soundOff));
  234. }
  235. if (typeof APP !== 'undefined') {
  236. APP.API.notifyRecordingStatusChanged(
  237. false, mode, undefined, isRecorderTranscriptionsRunning(state));
  238. }
  239. }
  240. }
  241. break;
  242. }
  243. case TRACK_ADDED: {
  244. const { track } = action;
  245. if (LocalRecordingManager.isRecordingLocally() && track.mediaType === MEDIA_TYPE.AUDIO) {
  246. const audioTrack = track.jitsiTrack.track;
  247. LocalRecordingManager.addAudioTrackToLocalRecording(audioTrack);
  248. }
  249. break;
  250. }
  251. case PARTICIPANT_UPDATED: {
  252. const { id, role } = action.participant;
  253. const state = getState();
  254. const localParticipant = getLocalParticipant(state);
  255. if (localParticipant?.id !== id) {
  256. return next(action);
  257. }
  258. if (role === PARTICIPANT_ROLE.MODERATOR) {
  259. dispatch(showStartRecordingNotification());
  260. }
  261. return next(action);
  262. }
  263. }
  264. return result;
  265. });
  266. /**
  267. * Shows a notification about an error in the recording session. A
  268. * default notification will display if no error is specified in the passed
  269. * in recording session.
  270. *
  271. * @private
  272. * @param {Object} session - The recorder session model from the
  273. * lib.
  274. * @param {Dispatch} dispatch - The Redux Dispatch function.
  275. * @param {Function} getState - The Redux getState function.
  276. * @returns {void}
  277. */
  278. function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
  279. const mode = session.getMode();
  280. const error = session.getError();
  281. const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
  282. switch (error) {
  283. case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
  284. dispatch(showRecordingError({
  285. descriptionKey: 'recording.unavailable',
  286. descriptionArguments: {
  287. serviceName: isStreamMode
  288. ? '$t(liveStreaming.serviceName)'
  289. : '$t(recording.serviceName)'
  290. },
  291. titleKey: isStreamMode
  292. ? 'liveStreaming.unavailableTitle'
  293. : 'recording.unavailableTitle'
  294. }));
  295. break;
  296. case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
  297. dispatch(showRecordingError({
  298. descriptionKey: isStreamMode
  299. ? 'liveStreaming.busy'
  300. : 'recording.busy',
  301. titleKey: isStreamMode
  302. ? 'liveStreaming.busyTitle'
  303. : 'recording.busyTitle'
  304. }));
  305. break;
  306. case JitsiMeetJS.constants.recording.error.UNEXPECTED_REQUEST:
  307. dispatch(showRecordingWarning({
  308. descriptionKey: isStreamMode
  309. ? 'liveStreaming.sessionAlreadyActive'
  310. : 'recording.sessionAlreadyActive',
  311. titleKey: isStreamMode ? 'liveStreaming.inProgress' : 'recording.inProgress'
  312. }));
  313. break;
  314. default:
  315. dispatch(showRecordingError({
  316. descriptionKey: isStreamMode
  317. ? 'liveStreaming.error'
  318. : 'recording.error',
  319. titleKey: isStreamMode
  320. ? 'liveStreaming.failedToStart'
  321. : 'recording.failedToStart'
  322. }));
  323. break;
  324. }
  325. if (typeof APP !== 'undefined') {
  326. APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
  327. }
  328. }