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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // @flow
  2. import { isMobileBrowser } from '../base/environment/utils';
  3. import { isJwtFeatureEnabled } from '../base/jwt/functions';
  4. import { JitsiRecordingConstants, browser } from '../base/lib-jitsi-meet';
  5. import { getLocalParticipant, getRemoteParticipants, isLocalParticipantModerator } from '../base/participants';
  6. import { isInBreakoutRoom } from '../breakout-rooms/functions';
  7. import { isEnabled as isDropboxEnabled } from '../dropbox';
  8. import { extractFqnFromPath } from '../dynamic-branding/functions.any';
  9. import LocalRecordingManager from './components/Recording/LocalRecordingManager';
  10. import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
  11. import logger from './logger';
  12. /**
  13. * Searches in the passed in redux state for an active recording session of the
  14. * passed in mode.
  15. *
  16. * @param {Object} state - The redux state to search in.
  17. * @param {string} mode - Find an active recording session of the given mode.
  18. * @returns {Object|undefined}
  19. */
  20. export function getActiveSession(state: Object, mode: string) {
  21. const { sessionDatas } = state['features/recording'];
  22. const { status: statusConstants } = JitsiRecordingConstants;
  23. return sessionDatas.find(sessionData => sessionData.mode === mode
  24. && (sessionData.status === statusConstants.ON
  25. || sessionData.status === statusConstants.PENDING));
  26. }
  27. /**
  28. * Returns an estimated recording duration based on the size of the video file
  29. * in MB. The estimate is calculated under the assumption that 1 min of recorded
  30. * video needs 10MB of storage on average.
  31. *
  32. * @param {number} size - The size in MB of the recorded video.
  33. * @returns {number} - The estimated duration in minutes.
  34. */
  35. export function getRecordingDurationEstimation(size: ?number) {
  36. return Math.floor((size || 0) / 10);
  37. }
  38. /**
  39. * Searches in the passed in redux state for a recording session that matches
  40. * the passed in recording session ID.
  41. *
  42. * @param {Object} state - The redux state to search in.
  43. * @param {string} id - The ID of the recording session to find.
  44. * @returns {Object|undefined}
  45. */
  46. export function getSessionById(state: Object, id: string) {
  47. return state['features/recording'].sessionDatas.find(
  48. sessionData => sessionData.id === id);
  49. }
  50. /**
  51. * Fetches the recording link from the server.
  52. *
  53. * @param {string} url - The base url.
  54. * @param {string} recordingSessionId - The ID of the recording session to find.
  55. * @param {string} region - The meeting region.
  56. * @param {string} tenant - The meeting tenant.
  57. * @returns {Promise<any>}
  58. */
  59. export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
  60. const fullUrl = `${url}?recordingSessionId=${recordingSessionId}&region=${region}&tenant=${tenant}`;
  61. const res = await fetch(fullUrl, {
  62. headers: {
  63. 'Content-Type': 'application/json'
  64. }
  65. });
  66. const json = await res.json();
  67. return res.ok ? json : Promise.reject(json);
  68. }
  69. /**
  70. * Selector used for determining if recording is saved on dropbox.
  71. *
  72. * @param {Object} state - The redux state to search in.
  73. * @returns {string}
  74. */
  75. export function isSavingRecordingOnDropbox(state: Object) {
  76. return isDropboxEnabled(state)
  77. && state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
  78. }
  79. /**
  80. * Selector used for determining disable state for the meeting highlight button.
  81. *
  82. * @param {Object} state - The redux state to search in.
  83. * @returns {string}
  84. */
  85. export function isHighlightMeetingMomentDisabled(state: Object) {
  86. return state['features/recording'].disableHighlightMeetingMoment;
  87. }
  88. /**
  89. * Returns the recording session status that is to be shown in a label. E.g. If
  90. * there is a session with the status OFF and one with PENDING, then the PENDING
  91. * one will be shown, because that is likely more important for the user to see.
  92. *
  93. * @param {Object} state - The redux state to search in.
  94. * @param {string} mode - The recording mode to get status for.
  95. * @returns {string|undefined}
  96. */
  97. export function getSessionStatusToShow(state: Object, mode: string): ?string {
  98. const recordingSessions = state['features/recording'].sessionDatas;
  99. let status;
  100. if (Array.isArray(recordingSessions)) {
  101. for (const session of recordingSessions) {
  102. if (session.mode === mode
  103. && (!status
  104. || (RECORDING_STATUS_PRIORITIES.indexOf(session.status)
  105. > RECORDING_STATUS_PRIORITIES.indexOf(status)))) {
  106. status = session.status;
  107. }
  108. }
  109. }
  110. if ((!Array.isArray(recordingSessions) || recordingSessions.length === 0)
  111. && mode === JitsiRecordingConstants.mode.FILE
  112. && (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
  113. status = JitsiRecordingConstants.status.ON;
  114. }
  115. return status;
  116. }
  117. /**
  118. * Check if local recording is supported.
  119. *
  120. * @returns {boolean} - Whether local recording is supported or not.
  121. */
  122. export function supportsLocalRecording() {
  123. return browser.isChromiumBased() && !browser.isElectron() && !isMobileBrowser()
  124. && navigator.product !== 'ReactNative';
  125. }
  126. /**
  127. * Returns the recording button props.
  128. *
  129. * @param {Object} state - The redux state to search in.
  130. *
  131. * @returns {{
  132. * disabled: boolean,
  133. * tooltip: string,
  134. * visible: boolean
  135. * }}
  136. */
  137. export function getRecordButtonProps(state: Object): ?string {
  138. let visible;
  139. // a button can be disabled/enabled if enableFeaturesBasedOnToken
  140. // is on or if the livestreaming is running.
  141. let disabled = false;
  142. let tooltip = '';
  143. // If the containing component provides the visible prop, that is one
  144. // above all, but if not, the button should be autonomus and decide on
  145. // its own to be visible or not.
  146. const isModerator = isLocalParticipantModerator(state);
  147. const {
  148. recordingService,
  149. localRecording
  150. } = state['features/base/config'];
  151. const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
  152. const dropboxEnabled = isDropboxEnabled(state);
  153. visible = isModerator && (recordingService?.enabled || localRecordingEnabled || dropboxEnabled);
  154. visible = isJwtFeatureEnabled(state, 'recording', visible);
  155. // disable the button if the livestreaming is running.
  156. if (visible && getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
  157. disabled = true;
  158. tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
  159. }
  160. // disable the button if we are in a breakout room.
  161. if (isInBreakoutRoom(state)) {
  162. disabled = true;
  163. visible = false;
  164. }
  165. return {
  166. disabled,
  167. tooltip,
  168. visible
  169. };
  170. }
  171. /**
  172. * Returns the resource id.
  173. *
  174. * @param {Object | string} recorder - A participant or it's resource.
  175. * @returns {string|undefined}
  176. */
  177. export function getResourceId(recorder: string | Object) {
  178. if (recorder) {
  179. return typeof recorder === 'string'
  180. ? recorder
  181. : recorder.getId();
  182. }
  183. }
  184. /**
  185. * Sends a meeting highlight to backend.
  186. *
  187. * @param {Object} state - Redux state.
  188. * @returns {boolean} - True if sent, false otherwise.
  189. */
  190. export async function sendMeetingHighlight(state: Object) {
  191. const { webhookProxyUrl: url } = state['features/base/config'];
  192. const { conference } = state['features/base/conference'];
  193. const { jwt } = state['features/base/jwt'];
  194. const { connection } = state['features/base/connection'];
  195. const jid = connection.getJid();
  196. const localParticipant = getLocalParticipant(state);
  197. const headers = {
  198. ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
  199. 'Content-Type': 'application/json'
  200. };
  201. const reqBody = {
  202. meetingFqn: extractFqnFromPath(state),
  203. sessionId: conference.getMeetingUniqueId(),
  204. submitted: Date.now(),
  205. participantId: localParticipant.jwtId,
  206. participantName: localParticipant.name,
  207. participantJid: jid
  208. };
  209. if (url) {
  210. try {
  211. const res = await fetch(`${url}/v2/highlights`, {
  212. method: 'POST',
  213. headers,
  214. body: JSON.stringify(reqBody)
  215. });
  216. if (res.ok) {
  217. return true;
  218. }
  219. logger.error('Status error:', res.status);
  220. } catch (err) {
  221. logger.error('Could not send request', err);
  222. }
  223. }
  224. return false;
  225. }
  226. /**
  227. * Whether a remote participant is recording locally or not.
  228. *
  229. * @param {Object} state - Redux state.
  230. * @returns {boolean}
  231. */
  232. function isRemoteParticipantRecordingLocally(state) {
  233. const participants = getRemoteParticipants(state);
  234. // eslint-disable-next-line prefer-const
  235. for (let value of participants.values()) {
  236. if (value.localRecording) {
  237. return true;
  238. }
  239. }
  240. return false;
  241. }