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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  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 => 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. LocalRecordingManager.startLocalRecording({
  103. dispatch,
  104. getState
  105. }, action.onlySelf)
  106. .then(() => {
  107. const props = {
  108. descriptionKey: 'recording.on',
  109. titleKey: 'dialog.recording'
  110. };
  111. if (localRecording?.notifyAllParticipants && !onlySelf) {
  112. dispatch(playSound(RECORDING_ON_SOUND_ID));
  113. }
  114. dispatch(showNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  115. dispatch(showNotification({
  116. titleKey: 'recording.localRecordingStartWarningTitle',
  117. descriptionKey: 'recording.localRecordingStartWarning'
  118. }, NOTIFICATION_TIMEOUT_TYPE.STICKY));
  119. dispatch(updateLocalRecordingStatus(true, onlySelf));
  120. sendAnalytics(createRecordingEvent('started', `local${onlySelf ? '.self' : ''}`));
  121. if (typeof APP !== 'undefined') {
  122. APP.API.notifyRecordingStatusChanged(
  123. true, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
  124. }
  125. })
  126. .catch(err => {
  127. logger.error('Capture failed', err);
  128. let descriptionKey = 'recording.error';
  129. if (err.message === 'WrongSurfaceSelected') {
  130. descriptionKey = 'recording.surfaceError';
  131. } else if (err.message === 'NoLocalStreams') {
  132. descriptionKey = 'recording.noStreams';
  133. } else if (err.message === 'NoMicTrack') {
  134. descriptionKey = 'recording.noMicPermission';
  135. }
  136. const props = {
  137. descriptionKey,
  138. titleKey: 'recording.failedToStart'
  139. };
  140. if (typeof APP !== 'undefined') {
  141. APP.API.notifyRecordingStatusChanged(
  142. false, 'local', err.message, isRecorderTranscriptionsRunning(getState()));
  143. }
  144. dispatch(showErrorNotification(props, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
  145. });
  146. break;
  147. }
  148. case STOP_LOCAL_RECORDING: {
  149. const { localRecording } = getState()['features/base/config'];
  150. if (LocalRecordingManager.isRecordingLocally()) {
  151. LocalRecordingManager.stopLocalRecording();
  152. dispatch(updateLocalRecordingStatus(false));
  153. if (localRecording?.notifyAllParticipants && !LocalRecordingManager.selfRecording) {
  154. dispatch(playSound(RECORDING_OFF_SOUND_ID));
  155. }
  156. if (typeof APP !== 'undefined') {
  157. APP.API.notifyRecordingStatusChanged(
  158. false, 'local', undefined, isRecorderTranscriptionsRunning(getState()));
  159. }
  160. }
  161. break;
  162. }
  163. case RECORDING_SESSION_UPDATED: {
  164. const state = getState();
  165. // When in recorder mode no notifications are shown
  166. // or extra sounds are also not desired
  167. // but we want to indicate those in case of sip gateway
  168. const {
  169. iAmRecorder,
  170. iAmSipGateway,
  171. recordingLimit
  172. } = state['features/base/config'];
  173. if (iAmRecorder && !iAmSipGateway) {
  174. break;
  175. }
  176. const updatedSessionData
  177. = getSessionById(state, action.sessionData.id);
  178. const { initiator, mode = '', terminator } = updatedSessionData ?? {};
  179. const { PENDING, OFF, ON } = JitsiRecordingConstants.status;
  180. if (updatedSessionData?.status === PENDING && oldSessionData?.status !== PENDING) {
  181. dispatch(showPendingRecordingNotification(mode));
  182. dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
  183. break;
  184. }
  185. dispatch(hidePendingRecordingNotification(mode));
  186. if (updatedSessionData?.status === ON) {
  187. // We receive 2 updates of the session status ON. The first one is from jibri when it joins.
  188. // The second one is from jicofo which will deliver the initiator value. Since the start
  189. // recording notification uses the initiator value we skip the jibri update and show the
  190. // notification on the update from jicofo.
  191. // FIXME: simplify checks when the backend start sending only one status ON update containing
  192. // the initiator.
  193. if (initiator && !oldSessionData?.initiator) {
  194. if (typeof recordingLimit === 'object') {
  195. dispatch(showRecordingLimitNotification(mode));
  196. } else {
  197. dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
  198. }
  199. }
  200. if (oldSessionData?.status !== ON) {
  201. sendAnalytics(createRecordingEvent('start', mode));
  202. let soundID;
  203. if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
  204. soundID = RECORDING_ON_SOUND_ID;
  205. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  206. soundID = LIVE_STREAMING_ON_SOUND_ID;
  207. }
  208. if (soundID) {
  209. dispatch(playSound(soundID));
  210. }
  211. if (typeof APP !== 'undefined') {
  212. APP.API.notifyRecordingStatusChanged(
  213. true, mode, undefined, isRecorderTranscriptionsRunning(state));
  214. }
  215. }
  216. } else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
  217. if (terminator) {
  218. dispatch(
  219. showStoppedRecordingNotification(
  220. mode, getParticipantDisplayName(state, getResourceId(terminator))));
  221. }
  222. let duration = 0, soundOff, soundOn;
  223. if (oldSessionData?.timestamp) {
  224. duration
  225. = (Date.now() / 1000) - oldSessionData.timestamp;
  226. }
  227. sendAnalytics(createRecordingEvent('stop', mode, duration));
  228. if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
  229. soundOff = RECORDING_OFF_SOUND_ID;
  230. soundOn = RECORDING_ON_SOUND_ID;
  231. } else if (mode === JitsiRecordingConstants.mode.STREAM) {
  232. soundOff = LIVE_STREAMING_OFF_SOUND_ID;
  233. soundOn = LIVE_STREAMING_ON_SOUND_ID;
  234. }
  235. if (soundOff && soundOn) {
  236. dispatch(stopSound(soundOn));
  237. dispatch(playSound(soundOff));
  238. }
  239. if (typeof APP !== 'undefined') {
  240. APP.API.notifyRecordingStatusChanged(
  241. false, mode, undefined, isRecorderTranscriptionsRunning(state));
  242. }
  243. }
  244. break;
  245. }
  246. case TRACK_ADDED: {
  247. const { track } = action;
  248. if (LocalRecordingManager.isRecordingLocally() && track.mediaType === MEDIA_TYPE.AUDIO) {
  249. const audioTrack = track.jitsiTrack.track;
  250. LocalRecordingManager.addAudioTrackToLocalRecording(audioTrack);
  251. }
  252. break;
  253. }
  254. case PARTICIPANT_UPDATED: {
  255. const { id, role } = action.participant;
  256. const state = getState();
  257. const localParticipant = getLocalParticipant(state);
  258. if (localParticipant?.id !== id) {
  259. return next(action);
  260. }
  261. if (role === PARTICIPANT_ROLE.MODERATOR) {
  262. dispatch(showStartRecordingNotification());
  263. }
  264. return next(action);
  265. }
  266. }
  267. return result;
  268. });
  269. /**
  270. * Shows a notification about an error in the recording session. A
  271. * default notification will display if no error is specified in the passed
  272. * in recording session.
  273. *
  274. * @private
  275. * @param {Object} session - The recorder session model from the
  276. * lib.
  277. * @param {Dispatch} dispatch - The Redux Dispatch function.
  278. * @param {Function} getState - The Redux getState function.
  279. * @returns {void}
  280. */
  281. function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatch'], getState: IStore['getState']) {
  282. const mode = session.getMode();
  283. const error = session.getError();
  284. const isStreamMode = mode === JitsiMeetJS.constants.recording.mode.STREAM;
  285. switch (error) {
  286. case JitsiMeetJS.constants.recording.error.SERVICE_UNAVAILABLE:
  287. dispatch(showRecordingError({
  288. descriptionKey: 'recording.unavailable',
  289. descriptionArguments: {
  290. serviceName: isStreamMode
  291. ? '$t(liveStreaming.serviceName)'
  292. : '$t(recording.serviceName)'
  293. },
  294. titleKey: isStreamMode
  295. ? 'liveStreaming.unavailableTitle'
  296. : 'recording.unavailableTitle'
  297. }));
  298. break;
  299. case JitsiMeetJS.constants.recording.error.RESOURCE_CONSTRAINT:
  300. dispatch(showRecordingError({
  301. descriptionKey: isStreamMode
  302. ? 'liveStreaming.busy'
  303. : 'recording.busy',
  304. titleKey: isStreamMode
  305. ? 'liveStreaming.busyTitle'
  306. : 'recording.busyTitle'
  307. }));
  308. break;
  309. case JitsiMeetJS.constants.recording.error.UNEXPECTED_REQUEST:
  310. dispatch(showRecordingWarning({
  311. descriptionKey: isStreamMode
  312. ? 'liveStreaming.sessionAlreadyActive'
  313. : 'recording.sessionAlreadyActive',
  314. titleKey: isStreamMode ? 'liveStreaming.inProgress' : 'recording.inProgress'
  315. }));
  316. break;
  317. case JitsiMeetJS.constants.recording.error.POLICY_VIOLATION:
  318. dispatch(showRecordingWarning({
  319. descriptionKey: isStreamMode ? 'liveStreaming.policyError' : 'recording.policyError',
  320. titleKey: isStreamMode ? 'liveStreaming.failedToStart' : 'recording.failedToStart'
  321. }));
  322. break;
  323. default:
  324. dispatch(showRecordingError({
  325. descriptionKey: isStreamMode
  326. ? 'liveStreaming.error'
  327. : 'recording.error',
  328. titleKey: isStreamMode
  329. ? 'liveStreaming.failedToStart'
  330. : 'recording.failedToStart'
  331. }));
  332. break;
  333. }
  334. if (typeof APP !== 'undefined') {
  335. APP.API.notifyRecordingStatusChanged(false, mode, error, isRecorderTranscriptionsRunning(getState()));
  336. }
  337. }