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

functions.ts 14KB

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