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

functions.ts 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 } from '../breakout-rooms/functions';
  14. import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
  15. import { extractFqnFromPath } from '../dynamic-branding/functions.any';
  16. import LocalRecordingManager from './components/Recording/LocalRecordingManager';
  17. import {
  18. LIVE_STREAMING_OFF_SOUND_ID,
  19. LIVE_STREAMING_ON_SOUND_ID,
  20. RECORDING_OFF_SOUND_ID,
  21. RECORDING_ON_SOUND_ID,
  22. RECORDING_STATUS_PRIORITIES,
  23. RECORDING_TYPES
  24. } from './constants';
  25. import logger from './logger';
  26. import {
  27. LIVE_STREAMING_OFF_SOUND_FILE,
  28. LIVE_STREAMING_ON_SOUND_FILE,
  29. RECORDING_OFF_SOUND_FILE,
  30. RECORDING_ON_SOUND_FILE
  31. } from './sounds';
  32. /**
  33. * Searches in the passed in redux state for an active recording session of the
  34. * passed in mode.
  35. *
  36. * @param {Object} state - The redux state to search in.
  37. * @param {string} mode - Find an active recording session of the given mode.
  38. * @returns {Object|undefined}
  39. */
  40. export function getActiveSession(state: IReduxState, mode: string) {
  41. const { sessionDatas } = state['features/recording'];
  42. const { status: statusConstants } = JitsiRecordingConstants;
  43. return sessionDatas.find(sessionData => sessionData.mode === mode
  44. && (sessionData.status === statusConstants.ON
  45. || sessionData.status === statusConstants.PENDING));
  46. }
  47. /**
  48. * Returns an estimated recording duration based on the size of the video file
  49. * in MB. The estimate is calculated under the assumption that 1 min of recorded
  50. * video needs 10MB of storage on average.
  51. *
  52. * @param {number} size - The size in MB of the recorded video.
  53. * @returns {number} - The estimated duration in minutes.
  54. */
  55. export function getRecordingDurationEstimation(size?: number | null) {
  56. return Math.floor((size || 0) / 10);
  57. }
  58. /**
  59. * Searches in the passed in redux state for a recording session that matches
  60. * the passed in recording session ID.
  61. *
  62. * @param {Object} state - The redux state to search in.
  63. * @param {string} id - The ID of the recording session to find.
  64. * @returns {Object|undefined}
  65. */
  66. export function getSessionById(state: IReduxState, id: string) {
  67. return state['features/recording'].sessionDatas.find(
  68. sessionData => sessionData.id === id);
  69. }
  70. /**
  71. * Fetches the recording link from the server.
  72. *
  73. * @param {string} url - The base url.
  74. * @param {string} recordingSessionId - The ID of the recording session to find.
  75. * @param {string} region - The meeting region.
  76. * @param {string} tenant - The meeting tenant.
  77. * @returns {Promise<any>}
  78. */
  79. export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
  80. const fullUrl = `${url}?recordingSessionId=${recordingSessionId}&region=${region}&tenant=${tenant}`;
  81. const res = await fetch(fullUrl, {
  82. headers: {
  83. 'Content-Type': 'application/json'
  84. }
  85. });
  86. const json = await res.json();
  87. return res.ok ? json : Promise.reject(json);
  88. }
  89. /**
  90. * Selector used for determining if recording is saved on dropbox.
  91. *
  92. * @param {Object} state - The redux state to search in.
  93. * @returns {string}
  94. */
  95. export function isSavingRecordingOnDropbox(state: IReduxState) {
  96. return isDropboxEnabled(state)
  97. && state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
  98. }
  99. /**
  100. * Selector used for determining disable state for the meeting highlight button.
  101. *
  102. * @param {Object} state - The redux state to search in.
  103. * @returns {string}
  104. */
  105. export function isHighlightMeetingMomentDisabled(state: IReduxState) {
  106. return state['features/recording'].disableHighlightMeetingMoment;
  107. }
  108. /**
  109. * Returns the recording session status that is to be shown in a label. E.g. If
  110. * there is a session with the status OFF and one with PENDING, then the PENDING
  111. * one will be shown, because that is likely more important for the user to see.
  112. *
  113. * @param {Object} state - The redux state to search in.
  114. * @param {string} mode - The recording mode to get status for.
  115. * @returns {string|undefined}
  116. */
  117. export function getSessionStatusToShow(state: IReduxState, mode: string): string | undefined {
  118. const recordingSessions = state['features/recording'].sessionDatas;
  119. let status;
  120. if (Array.isArray(recordingSessions)) {
  121. for (const session of recordingSessions) {
  122. if (session.mode === mode
  123. && (!status
  124. || (RECORDING_STATUS_PRIORITIES.indexOf(session.status)
  125. > RECORDING_STATUS_PRIORITIES.indexOf(status)))) {
  126. status = session.status;
  127. }
  128. }
  129. }
  130. if ((!Array.isArray(recordingSessions) || recordingSessions.length === 0)
  131. && 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 the recording button props.
  148. *
  149. * @param {Object} state - The redux state to search in.
  150. *
  151. * @returns {{
  152. * disabled: boolean,
  153. * tooltip: string,
  154. * visible: boolean
  155. * }}
  156. */
  157. export function getRecordButtonProps(state: IReduxState) {
  158. let visible;
  159. // a button can be disabled/enabled if enableFeaturesBasedOnToken
  160. // is on or if the livestreaming is running.
  161. let disabled = false;
  162. let tooltip = '';
  163. // If the containing component provides the visible prop, that is one
  164. // above all, but if not, the button should be autonomus and decide on
  165. // its own to be visible or not.
  166. const isModerator = isLocalParticipantModerator(state);
  167. const {
  168. recordingService,
  169. localRecording
  170. } = state['features/base/config'];
  171. const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
  172. const dropboxEnabled = isDropboxEnabled(state);
  173. const recordingEnabled = recordingService?.enabled || localRecordingEnabled || dropboxEnabled;
  174. if (isModerator) {
  175. visible = recordingEnabled ? isJwtFeatureEnabled(state, 'recording', true) : false;
  176. } else {
  177. visible = navigator.product !== 'ReactNative' && localRecordingEnabled;
  178. }
  179. // disable the button if the livestreaming is running.
  180. if (visible && getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
  181. disabled = true;
  182. tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
  183. }
  184. // disable the button if we are in a breakout room.
  185. if (isInBreakoutRoom(state)) {
  186. disabled = true;
  187. visible = false;
  188. }
  189. return {
  190. disabled,
  191. tooltip,
  192. visible
  193. };
  194. }
  195. /**
  196. * Returns the resource id.
  197. *
  198. * @param {Object | string} recorder - A participant or it's resource.
  199. * @returns {string|undefined}
  200. */
  201. export function getResourceId(recorder: string | { getId: Function; }) {
  202. if (recorder) {
  203. return typeof recorder === 'string'
  204. ? recorder
  205. : recorder.getId();
  206. }
  207. }
  208. /**
  209. * Sends a meeting highlight to backend.
  210. *
  211. * @param {Object} state - Redux state.
  212. * @returns {boolean} - True if sent, false otherwise.
  213. */
  214. export async function sendMeetingHighlight(state: IReduxState) {
  215. const { webhookProxyUrl: url } = state['features/base/config'];
  216. const { conference } = state['features/base/conference'];
  217. const { jwt } = state['features/base/jwt'];
  218. const { connection } = state['features/base/connection'];
  219. const jid = connection?.getJid();
  220. const localParticipant = getLocalParticipant(state);
  221. const headers = {
  222. ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
  223. 'Content-Type': 'application/json'
  224. };
  225. const reqBody = {
  226. meetingFqn: extractFqnFromPath(state),
  227. sessionId: conference?.getMeetingUniqueId(),
  228. submitted: Date.now(),
  229. participantId: localParticipant?.jwtId,
  230. participantName: localParticipant?.name,
  231. participantJid: jid
  232. };
  233. if (url) {
  234. try {
  235. const res = await fetch(`${url}/v2/highlights`, {
  236. method: 'POST',
  237. headers,
  238. body: JSON.stringify(reqBody)
  239. });
  240. if (res.ok) {
  241. return true;
  242. }
  243. logger.error('Status error:', res.status);
  244. } catch (err) {
  245. logger.error('Could not send request', err);
  246. }
  247. }
  248. return false;
  249. }
  250. /**
  251. * Whether a remote participant is recording locally or not.
  252. *
  253. * @param {Object} state - Redux state.
  254. * @returns {boolean}
  255. */
  256. function isRemoteParticipantRecordingLocally(state: IReduxState) {
  257. const participants = getRemoteParticipants(state);
  258. // eslint-disable-next-line prefer-const
  259. for (let value of participants.values()) {
  260. if (value.localRecording) {
  261. return true;
  262. }
  263. }
  264. return false;
  265. }
  266. /**
  267. * Unregisters the audio files based on locale.
  268. *
  269. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  270. * @returns {void}
  271. */
  272. export function unregisterRecordingAudioFiles(dispatch: IStore['dispatch']) {
  273. dispatch(unregisterSound(LIVE_STREAMING_OFF_SOUND_FILE));
  274. dispatch(unregisterSound(LIVE_STREAMING_ON_SOUND_FILE));
  275. dispatch(unregisterSound(RECORDING_OFF_SOUND_FILE));
  276. dispatch(unregisterSound(RECORDING_ON_SOUND_FILE));
  277. }
  278. /**
  279. * Registers the audio files based on locale.
  280. *
  281. * @param {Dispatch<any>} dispatch - The redux dispatch function.
  282. * @param {boolean|undefined} shouldUnregister - Whether the sounds should be unregistered.
  283. * @returns {void}
  284. */
  285. export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], shouldUnregister?: boolean) {
  286. const language = i18next.language;
  287. if (shouldUnregister) {
  288. unregisterRecordingAudioFiles(dispatch);
  289. }
  290. dispatch(registerSound(
  291. LIVE_STREAMING_OFF_SOUND_ID,
  292. getSoundFileSrc(LIVE_STREAMING_OFF_SOUND_FILE, language)));
  293. dispatch(registerSound(
  294. LIVE_STREAMING_ON_SOUND_ID,
  295. getSoundFileSrc(LIVE_STREAMING_ON_SOUND_FILE, language)));
  296. dispatch(registerSound(
  297. RECORDING_OFF_SOUND_ID,
  298. getSoundFileSrc(RECORDING_OFF_SOUND_FILE, language)));
  299. dispatch(registerSound(
  300. RECORDING_ON_SOUND_ID,
  301. getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
  302. }