2022-11-11 08:20:33 +00:00
|
|
|
import { IReduxState } from '../app/types';
|
2022-07-12 11:25:56 +00:00
|
|
|
import { isMobileBrowser } from '../base/environment/utils';
|
2022-08-16 12:53:53 +00:00
|
|
|
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
2022-07-12 11:25:56 +00:00
|
|
|
import { JitsiRecordingConstants, browser } from '../base/lib-jitsi-meet';
|
2022-11-11 08:20:33 +00:00
|
|
|
import {
|
|
|
|
getLocalParticipant,
|
|
|
|
getRemoteParticipants,
|
|
|
|
isLocalParticipantModerator
|
|
|
|
} from '../base/participants/functions';
|
2022-03-25 11:44:05 +00:00
|
|
|
import { isInBreakoutRoom } from '../breakout-rooms/functions';
|
2022-11-11 08:20:33 +00:00
|
|
|
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
|
2022-05-23 15:02:14 +00:00
|
|
|
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
|
2018-05-16 14:00:16 +00:00
|
|
|
|
2022-06-03 11:45:27 +00:00
|
|
|
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
|
2021-06-23 08:57:11 +00:00
|
|
|
import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
|
2022-03-14 08:31:08 +00:00
|
|
|
import logger from './logger';
|
2018-09-11 10:16:01 +00:00
|
|
|
|
2018-05-16 14:00:16 +00:00
|
|
|
/**
|
|
|
|
* Searches in the passed in redux state for an active recording session of the
|
|
|
|
* passed in mode.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
* @param {string} mode - Find an active recording session of the given mode.
|
|
|
|
* @returns {Object|undefined}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getActiveSession(state: IReduxState, mode: string) {
|
2018-05-16 14:00:16 +00:00
|
|
|
const { sessionDatas } = state['features/recording'];
|
|
|
|
const { status: statusConstants } = JitsiRecordingConstants;
|
|
|
|
|
|
|
|
return sessionDatas.find(sessionData => sessionData.mode === mode
|
|
|
|
&& (sessionData.status === statusConstants.ON
|
|
|
|
|| sessionData.status === statusConstants.PENDING));
|
|
|
|
}
|
2018-06-05 18:20:43 +00:00
|
|
|
|
2018-09-25 00:08:55 +00:00
|
|
|
/**
|
|
|
|
* Returns an estimated recording duration based on the size of the video file
|
|
|
|
* in MB. The estimate is calculated under the assumption that 1 min of recorded
|
2021-03-16 15:59:33 +00:00
|
|
|
* video needs 10MB of storage on average.
|
2018-09-25 00:08:55 +00:00
|
|
|
*
|
|
|
|
* @param {number} size - The size in MB of the recorded video.
|
|
|
|
* @returns {number} - The estimated duration in minutes.
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getRecordingDurationEstimation(size?: number | null) {
|
2018-09-25 00:08:55 +00:00
|
|
|
return Math.floor((size || 0) / 10);
|
|
|
|
}
|
|
|
|
|
2018-06-05 18:20:43 +00:00
|
|
|
/**
|
|
|
|
* Searches in the passed in redux state for a recording session that matches
|
|
|
|
* the passed in recording session ID.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
* @param {string} id - The ID of the recording session to find.
|
|
|
|
* @returns {Object|undefined}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getSessionById(state: IReduxState, id: string) {
|
2018-06-05 18:20:43 +00:00
|
|
|
return state['features/recording'].sessionDatas.find(
|
|
|
|
sessionData => sessionData.id === id);
|
|
|
|
}
|
2018-09-11 10:16:01 +00:00
|
|
|
|
2021-06-21 08:36:18 +00:00
|
|
|
/**
|
|
|
|
* Fetches the recording link from the server.
|
|
|
|
*
|
|
|
|
* @param {string} url - The base url.
|
|
|
|
* @param {string} recordingSessionId - The ID of the recording session to find.
|
|
|
|
* @param {string} region - The meeting region.
|
|
|
|
* @param {string} tenant - The meeting tenant.
|
|
|
|
* @returns {Promise<any>}
|
|
|
|
*/
|
|
|
|
export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
|
|
|
|
const fullUrl = `${url}?recordingSessionId=${recordingSessionId}®ion=${region}&tenant=${tenant}`;
|
|
|
|
const res = await fetch(fullUrl, {
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const json = await res.json();
|
|
|
|
|
2021-12-06 14:11:16 +00:00
|
|
|
return res.ok ? json : Promise.reject(json);
|
2021-06-21 08:36:18 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 08:57:11 +00:00
|
|
|
/**
|
|
|
|
* Selector used for determining if recording is saved on dropbox.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function isSavingRecordingOnDropbox(state: IReduxState) {
|
2021-06-23 08:57:11 +00:00
|
|
|
return isDropboxEnabled(state)
|
|
|
|
&& state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
|
|
|
|
}
|
|
|
|
|
2022-03-14 08:31:08 +00:00
|
|
|
/**
|
|
|
|
* Selector used for determining disable state for the meeting highlight button.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function isHighlightMeetingMomentDisabled(state: IReduxState) {
|
2022-03-14 08:31:08 +00:00
|
|
|
return state['features/recording'].disableHighlightMeetingMoment;
|
|
|
|
}
|
|
|
|
|
2018-09-11 10:16:01 +00:00
|
|
|
/**
|
2018-11-08 12:25:02 +00:00
|
|
|
* Returns the recording session status that is to be shown in a label. E.g. If
|
2018-09-11 10:16:01 +00:00
|
|
|
* there is a session with the status OFF and one with PENDING, then the PENDING
|
|
|
|
* one will be shown, because that is likely more important for the user to see.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
* @param {string} mode - The recording mode to get status for.
|
|
|
|
* @returns {string|undefined}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getSessionStatusToShow(state: IReduxState, mode: string): string | undefined {
|
2018-09-11 10:16:01 +00:00
|
|
|
const recordingSessions = state['features/recording'].sessionDatas;
|
|
|
|
let status;
|
|
|
|
|
|
|
|
if (Array.isArray(recordingSessions)) {
|
|
|
|
for (const session of recordingSessions) {
|
|
|
|
if (session.mode === mode
|
|
|
|
&& (!status
|
|
|
|
|| (RECORDING_STATUS_PRIORITIES.indexOf(session.status)
|
|
|
|
> RECORDING_STATUS_PRIORITIES.indexOf(status)))) {
|
|
|
|
status = session.status;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-03 11:45:27 +00:00
|
|
|
if ((!Array.isArray(recordingSessions) || recordingSessions.length === 0)
|
|
|
|
&& mode === JitsiRecordingConstants.mode.FILE
|
|
|
|
&& (LocalRecordingManager.isRecordingLocally() || isRemoteParticipantRecordingLocally(state))) {
|
|
|
|
status = JitsiRecordingConstants.status.ON;
|
|
|
|
}
|
2018-09-11 10:16:01 +00:00
|
|
|
|
|
|
|
return status;
|
|
|
|
}
|
2021-06-21 08:36:18 +00:00
|
|
|
|
2022-07-12 11:25:56 +00:00
|
|
|
/**
|
|
|
|
* Check if local recording is supported.
|
|
|
|
*
|
2022-07-14 07:10:08 +00:00
|
|
|
* @returns {boolean} - Whether local recording is supported or not.
|
2022-07-12 11:25:56 +00:00
|
|
|
*/
|
|
|
|
export function supportsLocalRecording() {
|
|
|
|
return browser.isChromiumBased() && !browser.isElectron() && !isMobileBrowser()
|
|
|
|
&& navigator.product !== 'ReactNative';
|
|
|
|
}
|
|
|
|
|
2022-03-25 11:44:05 +00:00
|
|
|
/**
|
|
|
|
* Returns the recording button props.
|
|
|
|
*
|
|
|
|
* @param {Object} state - The redux state to search in.
|
|
|
|
*
|
|
|
|
* @returns {{
|
|
|
|
* disabled: boolean,
|
|
|
|
* tooltip: string,
|
|
|
|
* visible: boolean
|
|
|
|
* }}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getRecordButtonProps(state: IReduxState) {
|
2022-03-25 11:44:05 +00:00
|
|
|
let visible;
|
|
|
|
|
|
|
|
// a button can be disabled/enabled if enableFeaturesBasedOnToken
|
|
|
|
// is on or if the livestreaming is running.
|
2022-08-16 12:53:53 +00:00
|
|
|
let disabled = false;
|
2022-03-25 11:44:05 +00:00
|
|
|
let tooltip = '';
|
|
|
|
|
|
|
|
// If the containing component provides the visible prop, that is one
|
|
|
|
// above all, but if not, the button should be autonomus and decide on
|
|
|
|
// its own to be visible or not.
|
|
|
|
const isModerator = isLocalParticipantModerator(state);
|
|
|
|
const {
|
2022-06-29 12:28:20 +00:00
|
|
|
recordingService,
|
2022-06-29 07:05:55 +00:00
|
|
|
localRecording
|
2022-03-25 11:44:05 +00:00
|
|
|
} = state['features/base/config'];
|
2022-07-12 11:25:56 +00:00
|
|
|
const localRecordingEnabled = !localRecording?.disable && supportsLocalRecording();
|
2022-06-29 07:05:55 +00:00
|
|
|
|
2022-06-29 12:28:20 +00:00
|
|
|
const dropboxEnabled = isDropboxEnabled(state);
|
|
|
|
|
|
|
|
visible = isModerator && (recordingService?.enabled || localRecordingEnabled || dropboxEnabled);
|
2022-08-16 12:53:53 +00:00
|
|
|
visible = isJwtFeatureEnabled(state, 'recording', visible);
|
2022-03-25 11:44:05 +00:00
|
|
|
|
|
|
|
// disable the button if the livestreaming is running.
|
2022-08-16 12:53:53 +00:00
|
|
|
if (visible && getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
|
2022-03-25 11:44:05 +00:00
|
|
|
disabled = true;
|
|
|
|
tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
|
|
|
|
}
|
|
|
|
|
|
|
|
// disable the button if we are in a breakout room.
|
|
|
|
if (isInBreakoutRoom(state)) {
|
|
|
|
disabled = true;
|
|
|
|
visible = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
disabled,
|
|
|
|
tooltip,
|
|
|
|
visible
|
|
|
|
};
|
|
|
|
}
|
2021-06-21 08:36:18 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the resource id.
|
|
|
|
*
|
|
|
|
* @param {Object | string} recorder - A participant or it's resource.
|
|
|
|
* @returns {string|undefined}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export function getResourceId(recorder: string | { getId: Function; }) {
|
2021-06-21 08:36:18 +00:00
|
|
|
if (recorder) {
|
|
|
|
return typeof recorder === 'string'
|
|
|
|
? recorder
|
|
|
|
: recorder.getId();
|
|
|
|
}
|
|
|
|
}
|
2022-03-14 08:31:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a meeting highlight to backend.
|
|
|
|
*
|
|
|
|
* @param {Object} state - Redux state.
|
|
|
|
* @returns {boolean} - True if sent, false otherwise.
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
export async function sendMeetingHighlight(state: IReduxState) {
|
2022-03-14 08:31:08 +00:00
|
|
|
const { webhookProxyUrl: url } = state['features/base/config'];
|
|
|
|
const { conference } = state['features/base/conference'];
|
|
|
|
const { jwt } = state['features/base/jwt'];
|
|
|
|
const { connection } = state['features/base/connection'];
|
2022-11-11 08:20:33 +00:00
|
|
|
const jid = connection?.getJid();
|
2022-03-14 08:31:08 +00:00
|
|
|
const localParticipant = getLocalParticipant(state);
|
|
|
|
|
|
|
|
const headers = {
|
|
|
|
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
};
|
|
|
|
|
|
|
|
const reqBody = {
|
2022-03-18 14:16:56 +00:00
|
|
|
meetingFqn: extractFqnFromPath(state),
|
2022-11-11 08:20:33 +00:00
|
|
|
sessionId: conference?.getMeetingUniqueId(),
|
2022-03-14 08:31:08 +00:00
|
|
|
submitted: Date.now(),
|
2022-11-11 08:20:33 +00:00
|
|
|
participantId: localParticipant?.jwtId,
|
|
|
|
participantName: localParticipant?.name,
|
2022-03-14 08:31:08 +00:00
|
|
|
participantJid: jid
|
|
|
|
};
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
try {
|
|
|
|
const res = await fetch(`${url}/v2/highlights`, {
|
|
|
|
method: 'POST',
|
|
|
|
headers,
|
|
|
|
body: JSON.stringify(reqBody)
|
|
|
|
});
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
logger.error('Status error:', res.status);
|
|
|
|
} catch (err) {
|
|
|
|
logger.error('Could not send request', err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
2022-06-03 11:45:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether a remote participant is recording locally or not.
|
|
|
|
*
|
|
|
|
* @param {Object} state - Redux state.
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
2022-11-11 08:20:33 +00:00
|
|
|
function isRemoteParticipantRecordingLocally(state: IReduxState) {
|
2022-06-03 11:45:27 +00:00
|
|
|
const participants = getRemoteParticipants(state);
|
|
|
|
|
|
|
|
// eslint-disable-next-line prefer-const
|
|
|
|
for (let value of participants.values()) {
|
|
|
|
if (value.localRecording) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|