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.js 11KB

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