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.

functions.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import i18next from 'i18next';
  2. import { IReduxState, IStore } from '../app/types';
  3. import { isMobileBrowser } from '../base/environment/utils';
  4. import { isJwtFeatureEnabled } from '../base/jwt/functions';
  5. import { JitsiRecordingConstants, browser } from '../base/lib-jitsi-meet';
  6. import { getSoundFileSrc } from '../base/media/functions';
  7. import {
  8. getLocalParticipant,
  9. getRemoteParticipants,
  10. isLocalParticipantModerator
  11. } from '../base/participants/functions';
  12. import { registerSound, unregisterSound } from '../base/sounds/actions';
  13. import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functions';
  14. import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
  15. import { extractFqnFromPath } from '../dynamic-branding/functions.any';
  16. import { canAddTranscriber, isRecorderTranscriptionsRunning } from '../transcribing/functions';
  17. import LocalRecordingManager from './components/Recording/LocalRecordingManager';
  18. import {
  19. LIVE_STREAMING_OFF_SOUND_ID,
  20. LIVE_STREAMING_ON_SOUND_ID,
  21. RECORDING_OFF_SOUND_ID,
  22. RECORDING_ON_SOUND_ID,
  23. RECORDING_STATUS_PRIORITIES,
  24. RECORDING_TYPES
  25. } from './constants';
  26. import logger from './logger';
  27. import {
  28. LIVE_STREAMING_OFF_SOUND_FILE,
  29. LIVE_STREAMING_ON_SOUND_FILE,
  30. RECORDING_OFF_SOUND_FILE,
  31. RECORDING_ON_SOUND_FILE
  32. } from './sounds';
  33. /**
  34. * Searches in the passed in redux state for an active recording session of the
  35. * passed in mode.
  36. *
  37. * @param {Object} state - The redux state to search in.
  38. * @param {string} mode - Find an active recording session of the given mode.
  39. * @returns {Object|undefined}
  40. */
  41. export function getActiveSession(state: IReduxState, mode: string) {
  42. const { sessionDatas } = state['features/recording'];
  43. const { status: statusConstants } = JitsiRecordingConstants;
  44. return sessionDatas.find(sessionData => sessionData.mode === mode
  45. && (sessionData.status === statusConstants.ON
  46. || sessionData.status === statusConstants.PENDING));
  47. }
  48. /**
  49. * Returns an estimated recording duration based on the size of the video file
  50. * in MB. The estimate is calculated under the assumption that 1 min of recorded
  51. * video needs 10MB of storage on average.
  52. *
  53. * @param {number} size - The size in MB of the recorded video.
  54. * @returns {number} - The estimated duration in minutes.
  55. */
  56. export function getRecordingDurationEstimation(size?: number | null) {
  57. return Math.floor((size || 0) / 10);
  58. }
  59. /**
  60. * Searches in the passed in redux state for a recording session that matches
  61. * the passed in recording session ID.
  62. *
  63. * @param {Object} state - The redux state to search in.
  64. * @param {string} id - The ID of the recording session to find.
  65. * @returns {Object|undefined}
  66. */
  67. export function getSessionById(state: IReduxState, id: string) {
  68. return state['features/recording'].sessionDatas.find(
  69. sessionData => sessionData.id === id);
  70. }
  71. /**
  72. * Fetches the recording link from the server.
  73. *
  74. * @param {string} url - The base url.
  75. * @param {string} recordingSessionId - The ID of the recording session to find.
  76. * @param {string} region - The meeting region.
  77. * @param {string} tenant - The meeting tenant.
  78. * @returns {Promise<any>}
  79. */
  80. export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
  81. const fullUrl = `${url}?recordingSessionId=${recordingSessionId}&region=${region}&tenant=${tenant}`;
  82. const res = await fetch(fullUrl, {
  83. headers: {
  84. 'Content-Type': 'application/json'
  85. }
  86. });
  87. const json = await res.json();
  88. return res.ok ? json : Promise.reject(json);
  89. }
  90. /**
  91. * Selector used for determining if recording is saved on dropbox.
  92. *
  93. * @param {Object} state - The redux state to search in.
  94. * @returns {string}
  95. */
  96. export function isSavingRecordingOnDropbox(state: IReduxState) {
  97. return isDropboxEnabled(state)
  98. && state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
  99. }
  100. /**
  101. * Selector used for determining disable state for the meeting highlight button.
  102. *
  103. * @param {Object} state - The redux state to search in.
  104. * @returns {string}
  105. */
  106. export function isHighlightMeetingMomentDisabled(state: IReduxState) {
  107. return state['features/recording'].disableHighlightMeetingMoment;
  108. }
  109. /**
  110. * Returns the recording session status that is to be shown in a label. E.g. If
  111. * there is a session with the status OFF and one with PENDING, then the PENDING
  112. * one will be shown, because that is likely more important for the user to see.
  113. *
  114. * @param {Object} state - The redux state to search in.
  115. * @param {string} mode - The recording mode to get status for.
  116. * @returns {string|undefined}
  117. */
  118. export function getSessionStatusToShow(state: IReduxState, mode: string): string | undefined {
  119. const recordingSessions = state['features/recording'].sessionDatas;
  120. let status;
  121. if (Array.isArray(recordingSessions)) {
  122. for (const session of recordingSessions) {
  123. if (session.mode === mode
  124. && (!status
  125. || (RECORDING_STATUS_PRIORITIES.indexOf(session.status)
  126. > RECORDING_STATUS_PRIORITIES.indexOf(status)))) {
  127. status = session.status;
  128. }
  129. }
  130. }
  131. if (!status && mode === JitsiRecordingConstants.mode.FILE
  132. && (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
  133. status = JitsiRecordingConstants.status.ON;
  134. }
  135. return status;
  136. }
  137. /**
  138. * Check if local recording is supported.
  139. *
  140. * @returns {boolean} - Whether local recording is supported or not.
  141. */
  142. export function supportsLocalRecording() {
  143. return browser.isChromiumBased() && !browser.isElectron() && !isMobileBrowser()
  144. && navigator.product !== 'ReactNative';
  145. }
  146. /**
  147. * Returns true if there is a cloud recording running.
  148. *
  149. * @param {IReduxState} state - The redux state to search in.
  150. * @returns {boolean}
  151. */
  152. export function isCloudRecordingRunning(state: IReduxState) {
  153. return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE));
  154. }
  155. /**
  156. * Returns true if there is a live streaming running.
  157. *
  158. * @param {IReduxState} state - The redux state to search in.
  159. * @returns {boolean}
  160. */
  161. export function isLiveStreamingRunning(state: IReduxState) {
  162. return Boolean(getActiveSession(state, JitsiRecordingConstants.mode.STREAM));
  163. }
  164. /**
  165. * Returns true if there is a recording session running.
  166. *
  167. * @param {Object} state - The redux state to search in.
  168. * @returns {boolean}
  169. */
  170. export function isRecordingRunning(state: IReduxState) {
  171. return (
  172. isCloudRecordingRunning(state)
  173. || LocalRecordingManager.isRecordingLocally()
  174. );
  175. }
  176. /**
  177. * Returns true if the participant can stop recording.
  178. *
  179. * @param {Object} state - The redux state to search in.
  180. * @returns {boolean}
  181. */
  182. export function canStopRecording(state: IReduxState) {
  183. if (LocalRecordingManager.isRecordingLocally()) {
  184. return true;
  185. }
  186. if (isCloudRecordingRunning(state) || isRecorderTranscriptionsRunning(state)) {
  187. return isLocalParticipantModerator(state) && isJwtFeatureEnabled(state, 'recording', true);
  188. }
  189. return false;
  190. }
  191. /**
  192. * Returns whether the transcription should start automatically when recording starts.
  193. *
  194. * @param {Object} state - The redux state to search in.
  195. * @returns {boolean}
  196. */
  197. export function shouldAutoTranscribeOnRecord(state: IReduxState) {
  198. const { transcription } = state['features/base/config'];
  199. return (transcription?.autoTranscribeOnRecord ?? true) && canAddTranscriber(state);
  200. }
  201. /**
  202. * Returns whether the recording should be shared.
  203. *
  204. * @param {Object} state - The redux state to search in.
  205. * @returns {boolean}
  206. */
  207. export function isRecordingSharingEnabled(state: IReduxState) {
  208. const { recordingService } = state['features/base/config'];
  209. return recordingService?.sharingEnabled ?? false;
  210. }
  211. /**
  212. * Returns the recording button props.
  213. *
  214. * @param {Object} state - The redux state to search in.
  215. *
  216. * @returns {{
  217. * disabled: boolean,
  218. * tooltip: string,
  219. * visible: boolean
  220. * }}
  221. */
  222. export function getRecordButtonProps(state: IReduxState) {
  223. let visible;
  224. // a button can be disabled/enabled if enableFeaturesBasedOnToken
  225. // is on or if the livestreaming is running.
  226. let disabled = false;
  227. let tooltip = '';
  228. // If the containing component provides the visible prop, that is one
  229. // above all, but if not, the button should be autonomus and decide on
  230. // its own to be visible or not.
  231. const isModerator = isLocalParticipantModerator(state);
  232. const {
  233. recordingService,
  234. localRecording
  235. } = state['features/base/config'];
  236. const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
  237. const dropboxEnabled = isDropboxEnabled(state);
  238. const recordingEnabled = recordingService?.enabled || dropboxEnabled;
  239. if (localRecordingEnabled) {
  240. visible = true;
  241. } else if (isModerator) {
  242. visible = recordingEnabled ? isJwtFeatureEnabled(state, 'recording', true) : false;
  243. }
  244. // disable the button if the livestreaming is running.
  245. if (visible && isLiveStreamingRunning(state)) {
  246. disabled = true;
  247. tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
  248. }
  249. // disable the button if we are in a breakout room.
  250. if (isInBreakoutRoomF(state)) {
  251. disabled = true;
  252. visible = false;
  253. }
  254. return {
  255. disabled,
  256. tooltip,
  257. visible
  258. };
  259. }
  260. /**
  261. * Returns the resource id.
  262. *
  263. * @param {Object | string} recorder - A participant or it's resource.
  264. * @returns {string|undefined}
  265. */
  266. export function getResourceId(recorder: string | { getId: Function; }) {
  267. if (recorder) {
  268. return typeof recorder === 'string'
  269. ? recorder
  270. : recorder.getId();
  271. }
  272. }
  273. /**
  274. * Sends a meeting highlight to backend.
  275. *
  276. * @param {Object} state - Redux state.
  277. * @returns {boolean} - True if sent, false otherwise.
  278. */
  279. export async function sendMeetingHighlight(state: IReduxState) {
  280. const { webhookProxyUrl: url } = state['features/base/config'];
  281. const { conference } = state['features/base/conference'];
  282. const { jwt } = state['features/base/jwt'];
  283. const { connection } = state['features/base/connection'];
  284. const jid = connection?.getJid();
  285. const localParticipant = getLocalParticipant(state);
  286. const headers = {
  287. ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
  288. 'Content-Type': 'application/json'
  289. };
  290. const reqBody = {
  291. meetingFqn: extractFqnFromPath(state),
  292. sessionId: conference?.getMeetingUniqueId(),
  293. submitted: Date.now(),
  294. participantId: localParticipant?.jwtId,
  295. participantName: localParticipant?.name,
  296. participantJid: jid
  297. };
  298. if (url) {
  299. try {
  300. const res = await fetch(`${url}/v2/highlights`, {
  301. method: 'POST',
  302. headers,
  303. body: JSON.stringify(reqBody)
  304. });
  305. if (res.ok) {
  306. return true;
  307. }
  308. logger.error('Status error:', res.status);
  309. } catch (err) {
  310. logger.error('Could not send request', err);
  311. }
  312. }
  313. return false;
  314. }
  315. /**
  316. * Whether a remote participant is recording locally or not.
  317. *
  318. * @param {Object} state - Redux state.
  319. * @returns {boolean}
  320. */
  321. export function isRemoteParticipantRecordingLocally(state: IReduxState) {
  322. const participants = getRemoteParticipants(state);
  323. // eslint-disable-next-line prefer-const
  324. for (let value of participants.values()) {
  325. if (value.localRecording) {
  326. return true;
  327. }
  328. }
  329. return false;
  330. }
  331. /**
  332. * Unregisters the audio files based on locale.
  333. *
  334. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  335. * @returns {void}
  336. */
  337. export function unregisterRecordingAudioFiles(dispatch: IStore['dispatch']) {
  338. dispatch(unregisterSound(LIVE_STREAMING_OFF_SOUND_FILE));
  339. dispatch(unregisterSound(LIVE_STREAMING_ON_SOUND_FILE));
  340. dispatch(unregisterSound(RECORDING_OFF_SOUND_FILE));
  341. dispatch(unregisterSound(RECORDING_ON_SOUND_FILE));
  342. }
  343. /**
  344. * Registers the audio files based on locale.
  345. *
  346. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  347. * @param {boolean|undefined} shouldUnregister - Whether the sounds should be unregistered.
  348. * @returns {void}
  349. */
  350. export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], shouldUnregister?: boolean) {
  351. const language = i18next.language;
  352. if (shouldUnregister) {
  353. unregisterRecordingAudioFiles(dispatch);
  354. }
  355. dispatch(registerSound(
  356. LIVE_STREAMING_OFF_SOUND_ID,
  357. getSoundFileSrc(LIVE_STREAMING_OFF_SOUND_FILE, language)));
  358. dispatch(registerSound(
  359. LIVE_STREAMING_ON_SOUND_ID,
  360. getSoundFileSrc(LIVE_STREAMING_ON_SOUND_FILE, language)));
  361. dispatch(registerSound(
  362. RECORDING_OFF_SOUND_ID,
  363. getSoundFileSrc(RECORDING_OFF_SOUND_FILE, language)));
  364. dispatch(registerSound(
  365. RECORDING_ON_SOUND_ID,
  366. getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
  367. }
  368. /**
  369. * Returns true if the live streaming button should be visible.
  370. *
  371. * @param {boolean} localParticipantIsModerator - True if the local participant is moderator.
  372. * @param {boolean} liveStreamingEnabled - True if the live streaming is enabled.
  373. * @param {boolean} liveStreamingEnabledInJwt - True if the lives treaming feature is enabled in JWT.
  374. * @returns {boolean}
  375. */
  376. export function isLiveStreamingButtonVisible({
  377. localParticipantIsModerator,
  378. liveStreamingEnabled,
  379. liveStreamingEnabledInJwt,
  380. isInBreakoutRoom
  381. }: {
  382. isInBreakoutRoom: boolean;
  383. liveStreamingEnabled: boolean;
  384. liveStreamingEnabledInJwt: boolean;
  385. localParticipantIsModerator: boolean;
  386. }) {
  387. let visible = false;
  388. if (localParticipantIsModerator && !isInBreakoutRoom) {
  389. visible = liveStreamingEnabled ? liveStreamingEnabledInJwt : false;
  390. }
  391. return visible;
  392. }