您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

functions.ts 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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. const isModerator = isLocalParticipantModerator(state);
  188. return isJwtFeatureEnabled(state, 'recording', isModerator, false);
  189. }
  190. return false;
  191. }
  192. /**
  193. * Returns whether the transcription should start automatically when recording starts.
  194. *
  195. * @param {Object} state - The redux state to search in.
  196. * @returns {boolean}
  197. */
  198. export function shouldAutoTranscribeOnRecord(state: IReduxState) {
  199. const { transcription } = state['features/base/config'];
  200. return (transcription?.autoTranscribeOnRecord ?? true) && canAddTranscriber(state);
  201. }
  202. /**
  203. * Returns whether the recording should be shared.
  204. *
  205. * @param {Object} state - The redux state to search in.
  206. * @returns {boolean}
  207. */
  208. export function isRecordingSharingEnabled(state: IReduxState) {
  209. const { recordingService } = state['features/base/config'];
  210. return recordingService?.sharingEnabled ?? false;
  211. }
  212. /**
  213. * Returns the recording button props.
  214. *
  215. * @param {Object} state - The redux state to search in.
  216. *
  217. * @returns {{
  218. * disabled: boolean,
  219. * tooltip: string,
  220. * visible: boolean
  221. * }}
  222. */
  223. export function getRecordButtonProps(state: IReduxState) {
  224. let visible;
  225. // a button can be disabled/enabled if enableFeaturesBasedOnToken
  226. // is on or if the livestreaming is running.
  227. let disabled = false;
  228. let tooltip = '';
  229. // If the containing component provides the visible prop, that is one
  230. // above all, but if not, the button should be autonomus and decide on
  231. // its own to be visible or not.
  232. const isModerator = isLocalParticipantModerator(state);
  233. const {
  234. recordingService,
  235. localRecording
  236. } = state['features/base/config'];
  237. const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
  238. const dropboxEnabled = isDropboxEnabled(state);
  239. const recordingEnabled = recordingService?.enabled || dropboxEnabled;
  240. if (localRecordingEnabled) {
  241. visible = true;
  242. } else if (isJwtFeatureEnabled(state, 'recording', isModerator, false)) {
  243. visible = recordingEnabled;
  244. }
  245. // disable the button if the livestreaming is running.
  246. if (visible && isLiveStreamingRunning(state)) {
  247. disabled = true;
  248. tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
  249. }
  250. // disable the button if we are in a breakout room.
  251. if (isInBreakoutRoomF(state)) {
  252. disabled = true;
  253. visible = false;
  254. }
  255. return {
  256. disabled,
  257. tooltip,
  258. visible
  259. };
  260. }
  261. /**
  262. * Returns the resource id.
  263. *
  264. * @param {Object | string} recorder - A participant or it's resource.
  265. * @returns {string|undefined}
  266. */
  267. export function getResourceId(recorder: string | { getId: Function; }) {
  268. if (recorder) {
  269. return typeof recorder === 'string'
  270. ? recorder
  271. : recorder.getId();
  272. }
  273. }
  274. /**
  275. * Sends a meeting highlight to backend.
  276. *
  277. * @param {Object} state - Redux state.
  278. * @returns {boolean} - True if sent, false otherwise.
  279. */
  280. export async function sendMeetingHighlight(state: IReduxState) {
  281. const { webhookProxyUrl: url } = state['features/base/config'];
  282. const { conference } = state['features/base/conference'];
  283. const { jwt } = state['features/base/jwt'];
  284. const { connection } = state['features/base/connection'];
  285. const jid = connection?.getJid();
  286. const localParticipant = getLocalParticipant(state);
  287. const headers = {
  288. ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
  289. 'Content-Type': 'application/json'
  290. };
  291. const reqBody = {
  292. meetingFqn: extractFqnFromPath(state),
  293. sessionId: conference?.getMeetingUniqueId(),
  294. submitted: Date.now(),
  295. participantId: localParticipant?.jwtId,
  296. participantName: localParticipant?.name,
  297. participantJid: jid
  298. };
  299. if (url) {
  300. try {
  301. const res = await fetch(`${url}/v2/highlights`, {
  302. method: 'POST',
  303. headers,
  304. body: JSON.stringify(reqBody)
  305. });
  306. if (res.ok) {
  307. return true;
  308. }
  309. logger.error('Status error:', res.status);
  310. } catch (err) {
  311. logger.error('Could not send request', err);
  312. }
  313. }
  314. return false;
  315. }
  316. /**
  317. * Whether a remote participant is recording locally or not.
  318. *
  319. * @param {Object} state - Redux state.
  320. * @returns {boolean}
  321. */
  322. export function isRemoteParticipantRecordingLocally(state: IReduxState) {
  323. const participants = getRemoteParticipants(state);
  324. // eslint-disable-next-line prefer-const
  325. for (let value of participants.values()) {
  326. if (value.localRecording) {
  327. return true;
  328. }
  329. }
  330. return false;
  331. }
  332. /**
  333. * Unregisters the audio files based on locale.
  334. *
  335. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  336. * @returns {void}
  337. */
  338. export function unregisterRecordingAudioFiles(dispatch: IStore['dispatch']) {
  339. dispatch(unregisterSound(LIVE_STREAMING_OFF_SOUND_FILE));
  340. dispatch(unregisterSound(LIVE_STREAMING_ON_SOUND_FILE));
  341. dispatch(unregisterSound(RECORDING_OFF_SOUND_FILE));
  342. dispatch(unregisterSound(RECORDING_ON_SOUND_FILE));
  343. }
  344. /**
  345. * Registers the audio files based on locale.
  346. *
  347. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  348. * @param {boolean|undefined} shouldUnregister - Whether the sounds should be unregistered.
  349. * @returns {void}
  350. */
  351. export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], shouldUnregister?: boolean) {
  352. const language = i18next.language;
  353. if (shouldUnregister) {
  354. unregisterRecordingAudioFiles(dispatch);
  355. }
  356. dispatch(registerSound(
  357. LIVE_STREAMING_OFF_SOUND_ID,
  358. getSoundFileSrc(LIVE_STREAMING_OFF_SOUND_FILE, language)));
  359. dispatch(registerSound(
  360. LIVE_STREAMING_ON_SOUND_ID,
  361. getSoundFileSrc(LIVE_STREAMING_ON_SOUND_FILE, language)));
  362. dispatch(registerSound(
  363. RECORDING_OFF_SOUND_ID,
  364. getSoundFileSrc(RECORDING_OFF_SOUND_FILE, language)));
  365. dispatch(registerSound(
  366. RECORDING_ON_SOUND_ID,
  367. getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
  368. }
  369. /**
  370. * Returns true if the live-streaming button should be visible.
  371. *
  372. * @param {boolean} liveStreamingEnabled - True if the live-streaming is enabled.
  373. * @param {boolean} liveStreamingAllowed - True if the live-streaming feature is enabled in JWT
  374. * or is a moderator if JWT is missing or features are missing in JWT.
  375. * @param {boolean} isInBreakoutRoom - True if in breakout room.
  376. * @returns {boolean}
  377. */
  378. export function isLiveStreamingButtonVisible({
  379. liveStreamingAllowed,
  380. liveStreamingEnabled,
  381. isInBreakoutRoom
  382. }: {
  383. isInBreakoutRoom: boolean;
  384. liveStreamingAllowed: boolean;
  385. liveStreamingEnabled: boolean;
  386. }) {
  387. return !isInBreakoutRoom && liveStreamingEnabled && liveStreamingAllowed;
  388. }