diff --git a/conference.js b/conference.js index 0799488e33..4cde0a11bf 100644 --- a/conference.js +++ b/conference.js @@ -62,12 +62,14 @@ import { import { checkAndNotifyForNewDevice, getAvailableDevices, - getDefaultDeviceId, notifyCameraError, notifyMicError, - setAudioOutputDeviceId, updateDeviceList -} from './react/features/base/devices'; +} from './react/features/base/devices/actions.web'; +import { + getDefaultDeviceId, + setAudioOutputDeviceId +} from './react/features/base/devices/functions.web'; import { JitsiConferenceErrors, JitsiConferenceEvents, diff --git a/globals.native.d.ts b/globals.native.d.ts index 4da6347911..9f4ba34dc2 100644 --- a/globals.native.d.ts +++ b/globals.native.d.ts @@ -12,8 +12,21 @@ interface IWindow { JITSI_MEET_LITE_SDK: boolean; JitsiMeetJS: any; config: IConfig; + document: any; + innerHeight: number; + innerWidth: number; interfaceConfig: any; location: ILocation; + self: any; + top: any; + + onerror: (event: string, source: any, lineno: any, colno: any, e: Error) => void; + onunhandledrejection: (event: any) => void; + + setTimeout: typeof setTimeout; + clearTimeout: typeof clearTimeout; + setImmediate: typeof setImmediate; + clearImmediate: typeof clearImmediate; } interface INavigator { @@ -22,6 +35,7 @@ interface INavigator { declare global { const APP: any; + const document: any; const interfaceConfig: any; const navigator: INavigator; const window: IWindow; diff --git a/modules/devices/mediaDeviceHelper.js b/modules/devices/mediaDeviceHelper.js index 4c5f3bf385..ef6e18aae3 100644 --- a/modules/devices/mediaDeviceHelper.js +++ b/modules/devices/mediaDeviceHelper.js @@ -1,10 +1,12 @@ /* global APP, JitsiMeetJS */ import { - getAudioOutputDeviceId, notifyCameraError, notifyMicError -} from '../../react/features/base/devices'; +} from '../../react/features/base/devices/actions.web'; +import { + getAudioOutputDeviceId +} from '../../react/features/base/devices/functions.web'; import { getUserSelectedCameraDeviceId, getUserSelectedMicDeviceId, diff --git a/package.json b/package.json index b36815bdf8..ba2afde8d9 100644 --- a/package.json +++ b/package.json @@ -199,7 +199,7 @@ "tsc:web": "tsc --noEmit --project tsconfig.web.json", "tsc:native": "tsc --noEmit --project tsconfig.native.json", "tsc:ci": "npm run tsc:web && npm run tsc:native", - "lint:ci": "eslint --ext .js,.ts,.tsx --max-warnings 0 . && npm run tsc:web", + "lint:ci": "eslint --ext .js,.ts,.tsx --max-warnings 0 . && npm run tsc:ci", "lang-sort": "./resources/lang-sort.sh", "lint-fix": "eslint --ext .js,.ts,.tsx --max-warnings 0 --fix .", "postinstall": "patch-package --error-on-fail && jetify", diff --git a/react/features/analytics/functions.ts b/react/features/analytics/functions.ts index 3a14bc9150..da4ad247f2 100644 --- a/react/features/analytics/functions.ts +++ b/react/features/analytics/functions.ts @@ -3,8 +3,8 @@ import { API_ID } from '../../../modules/API/constants'; import { getName as getAppName } from '../app/functions'; import { IStore } from '../app/types'; import { getAnalyticsRoomName } from '../base/conference/functions'; +import checkChromeExtensionsInstalled from '../base/environment/checkChromeExtensionsInstalled'; import { - checkChromeExtensionsInstalled, isMobileBrowser } from '../base/environment/utils'; import JitsiMeetJS, { diff --git a/react/features/analytics/handlers/AmplitudeHandler.ts b/react/features/analytics/handlers/AmplitudeHandler.ts index 0fa3634135..e0086dc567 100644 --- a/react/features/analytics/handlers/AmplitudeHandler.ts +++ b/react/features/analytics/handlers/AmplitudeHandler.ts @@ -1,3 +1,5 @@ +/* eslint-disable lines-around-comment */ + import logger from '../logger'; import AbstractHandler, { IEvent } from './AbstractHandler'; @@ -63,7 +65,7 @@ export default class AmplitudeHandler extends AbstractHandler { * @param {Object} userProps - The user portperties. * @returns {void} */ - setUserProperties(userProps: Object) { + setUserProperties(userProps: any) { if (this._enabled) { amplitude.getInstance().setUserProperties(userProps); } @@ -82,6 +84,7 @@ export default class AmplitudeHandler extends AbstractHandler { return; } + // @ts-ignore amplitude.getInstance().logEvent(this._extractName(event) ?? '', event); } @@ -100,7 +103,9 @@ export default class AmplitudeHandler extends AbstractHandler { return { sessionId: amplitude.getInstance().getSessionId(), + // @ts-ignore deviceId: amplitude.getInstance().options.deviceId, + // @ts-ignore userId: amplitude.getInstance().options.userId }; } diff --git a/react/features/app/types.ts b/react/features/app/types.ts index 925c84bd70..42954c6fd3 100644 --- a/react/features/app/types.ts +++ b/react/features/app/types.ts @@ -9,7 +9,7 @@ import { IAudioOnlyState } from '../base/audio-only/reducer'; import { IConferenceState } from '../base/conference/reducer'; import { IConfigState } from '../base/config/reducer'; import { IConnectionState } from '../base/connection/reducer'; -import { IDevicesState } from '../base/devices/reducer'; +import { IDevicesState } from '../base/devices/types'; import { IDialogState } from '../base/dialog/reducer'; import { IFlagsState } from '../base/flags/reducer'; import { IJwtState } from '../base/jwt/reducer'; diff --git a/react/features/base/devices/actions.ts b/react/features/base/devices/actions.web.ts similarity index 99% rename from react/features/base/devices/actions.ts rename to react/features/base/devices/actions.web.ts index 16330b1e31..c0047e48e0 100644 --- a/react/features/base/devices/actions.ts +++ b/react/features/base/devices/actions.web.ts @@ -1,7 +1,7 @@ import { IStore } from '../../app/types'; import JitsiMeetJS from '../lib-jitsi-meet'; import { updateSettings } from '../settings/actions'; -import { getUserSelectedOutputDeviceId } from '../settings/functions.any'; +import { getUserSelectedOutputDeviceId } from '../settings/functions.web'; import { ADD_PENDING_DEVICE_REQUEST, diff --git a/react/features/base/devices/functions.any.ts b/react/features/base/devices/functions.any.ts new file mode 100644 index 0000000000..da97a38da6 --- /dev/null +++ b/react/features/base/devices/functions.any.ts @@ -0,0 +1,19 @@ +import { IReduxState } from '../../app/types'; + +/** + * Returns true if there are devices of a specific type or on native platform. + * + * @param {Object} state - The state of the application. + * @param {string} type - The type of device: VideoOutput | audioOutput | audioInput. + * + * @returns {boolean} + */ +export function hasAvailableDevices(state: IReduxState, type: string) { + if (state['features/base/devices'] === undefined) { + return true; + } + + const availableDevices = state['features/base/devices'].availableDevices; + + return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0; +} diff --git a/react/features/base/devices/functions.native.ts b/react/features/base/devices/functions.native.ts new file mode 100644 index 0000000000..fb2a6bc39a --- /dev/null +++ b/react/features/base/devices/functions.native.ts @@ -0,0 +1 @@ +export * from './functions.any'; diff --git a/react/features/base/devices/functions.ts b/react/features/base/devices/functions.web.ts similarity index 93% rename from react/features/base/devices/functions.ts rename to react/features/base/devices/functions.web.ts index 45a5a4c101..b3d355e95c 100644 --- a/react/features/base/devices/functions.ts +++ b/react/features/base/devices/functions.web.ts @@ -5,10 +5,9 @@ import { ISettingsState } from '../settings/reducer'; import { parseURLParams } from '../util/parseURLParams'; import logger from './logger'; -import { IDevicesState } from './reducer'; +import { IDevicesState } from './types'; - -declare const APP: any; +export * from './functions.any'; const webrtcKindToJitsiKindTranslator = { audioinput: 'audioInput', @@ -240,24 +239,6 @@ export function getVideoDeviceIds(state: IReduxState) { return state['features/base/devices'].availableDevices.videoInput?.map(({ deviceId }) => deviceId); } -/** - * Returns true if there are devices of a specific type or on native platform. - * - * @param {Object} state - The state of the application. - * @param {string} type - The type of device: VideoOutput | audioOutput | audioInput. - * - * @returns {boolean} - */ -export function hasAvailableDevices(state: IReduxState, type: string) { - if (state['features/base/devices'] === undefined) { - return true; - } - - const availableDevices = state['features/base/devices'].availableDevices; - - return Number(availableDevices[type as keyof typeof availableDevices]?.length) > 0; -} - /** * Set device id of the audio output device which is currently in use. * Empty string stands for default device. diff --git a/react/features/base/devices/index.js b/react/features/base/devices/index.js deleted file mode 100644 index 08fe9014b1..0000000000 --- a/react/features/base/devices/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions'; -export * from './actionTypes'; -export * from './functions'; diff --git a/react/features/base/devices/middleware.web.ts b/react/features/base/devices/middleware.web.ts index c4304e9202..3e9c6149f0 100644 --- a/react/features/base/devices/middleware.web.ts +++ b/react/features/base/devices/middleware.web.ts @@ -35,7 +35,7 @@ import { setAudioOutputDeviceId } from './functions'; import logger from './logger'; -import { IDevicesState } from './reducer'; +import { IDevicesState } from './types'; const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = { microphone: { diff --git a/react/features/base/devices/reducer.ts b/react/features/base/devices/reducer.web.ts similarity index 87% rename from react/features/base/devices/reducer.ts rename to react/features/base/devices/reducer.web.ts index 7a83effa90..abdaeb9933 100644 --- a/react/features/base/devices/reducer.ts +++ b/react/features/base/devices/reducer.web.ts @@ -8,8 +8,9 @@ import { SET_VIDEO_INPUT_DEVICE, UPDATE_DEVICE_LIST } from './actionTypes'; -import { groupDevicesByKind } from './functions'; +import { groupDevicesByKind } from './functions.web'; import logger from './logger'; +import { IDevicesState } from './types'; const DEFAULT_STATE: IDevicesState = { @@ -25,19 +26,6 @@ const DEFAULT_STATE: IDevicesState = { } }; -export interface IDevicesState { - availableDevices: { - audioInput?: MediaDeviceInfo[]; - audioOutput?: MediaDeviceInfo[]; - videoInput?: MediaDeviceInfo[]; - }; - pendingRequests: Object[]; - permissions: { - audio: boolean; - video: boolean; - }; -} - /** * Listen for actions which changes the state of known and used devices. * diff --git a/react/features/base/devices/types.ts b/react/features/base/devices/types.ts new file mode 100644 index 0000000000..0eba52af02 --- /dev/null +++ b/react/features/base/devices/types.ts @@ -0,0 +1,17 @@ +/* eslint-disable lines-around-comment */ + +export interface IDevicesState { + availableDevices: { + // @ts-ignore + audioInput?: MediaDeviceInfo[]; + // @ts-ignore + audioOutput?: MediaDeviceInfo[]; + // @ts-ignore + videoInput?: MediaDeviceInfo[]; + }; + pendingRequests: any[]; + permissions: { + audio: boolean; + video: boolean; + }; +} diff --git a/react/features/base/environment/checkChromeExtensionsInstalled.native.ts b/react/features/base/environment/checkChromeExtensionsInstalled.native.ts new file mode 100644 index 0000000000..159dac5ec0 --- /dev/null +++ b/react/features/base/environment/checkChromeExtensionsInstalled.native.ts @@ -0,0 +1,10 @@ +/** + * Checks whether the chrome extensions defined in the config file are installed or not. + * + * @param {Object} _config - Objects containing info about the configured extensions. + * + * @returns {Promise[]} + */ +export default function checkChromeExtensionsInstalled(_config: any = {}) { + return Promise.resolve([]); +} diff --git a/react/features/base/environment/checkChromeExtensionsInstalled.web.ts b/react/features/base/environment/checkChromeExtensionsInstalled.web.ts new file mode 100644 index 0000000000..4324b1c711 --- /dev/null +++ b/react/features/base/environment/checkChromeExtensionsInstalled.web.ts @@ -0,0 +1,26 @@ +/** + * Checks whether the chrome extensions defined in the config file are installed or not. + * + * @param {Object} config - Objects containing info about the configured extensions. + * + * @returns {Promise[]} + */ +export default function checkChromeExtensionsInstalled(config: any = {}) { + const isExtensionInstalled = (info: any) => new Promise(resolve => { + const img = new Image(); + + img.src = `chrome-extension://${info.id}/${info.path}`; + img.setAttribute('aria-hidden', 'true'); + img.onload = function() { + resolve(true); + }; + img.onerror = function() { + resolve(false); + }; + }); + const extensionInstalledFunction = (info: any) => isExtensionInstalled(info); + + return Promise.all( + (config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info)) + ); +} diff --git a/react/features/base/environment/utils.ts b/react/features/base/environment/utils.ts index cd55115f87..6d26062cbe 100644 --- a/react/features/base/environment/utils.ts +++ b/react/features/base/environment/utils.ts @@ -18,30 +18,3 @@ export function isMobileBrowser() { export function isIosMobileBrowser() { return Platform.OS === 'ios'; } - -/** - * Checks whether the chrome extensions defined in the config file are installed or not. - * - * @param {Object} config - Objects containing info about the configured extensions. - * - * @returns {Promise[]} - */ -export function checkChromeExtensionsInstalled(config: any = {}) { - const isExtensionInstalled = (info: any) => new Promise(resolve => { - const img = new Image(); - - img.src = `chrome-extension://${info.id}/${info.path}`; - img.setAttribute('aria-hidden', 'true'); - img.onload = function() { - resolve(true); - }; - img.onerror = function() { - resolve(false); - }; - }); - const extensionInstalledFunction = (info: any) => isExtensionInstalled(info); - - return Promise.all( - (config.chromeExtensionsInfo || []).map((info: any) => extensionInstalledFunction(info)) - ); -} diff --git a/react/features/base/jwt/functions.ts b/react/features/base/jwt/functions.ts index 1ed3211de7..57566d1208 100644 --- a/react/features/base/jwt/functions.ts +++ b/react/features/base/jwt/functions.ts @@ -16,7 +16,7 @@ import { MEET_FEATURES } from './constants'; * @returns {string} The JSON Web Token (JWT), if any, defined by the specified * {@code url}; otherwise, {@code undefined}. */ -export function parseJWTFromURLParams(url: URL | Location = window.location) { +export function parseJWTFromURLParams(url: URL | typeof window.location = window.location) { // @ts-ignore return parseURLParams(url, true, 'search').jwt; } diff --git a/react/features/base/settings/functions.any.ts b/react/features/base/settings/functions.any.ts index 603af749b2..b7ba4dd2f9 100644 --- a/react/features/base/settings/functions.any.ts +++ b/react/features/base/settings/functions.any.ts @@ -103,160 +103,6 @@ export function getServerURL(stateful: IStateful) { return state['features/base/settings'].serverURL || DEFAULT_SERVER_URL; } -/** - * Searches known devices for a matching deviceId and fall back to matching on - * label. Returns the stored preferred cameraDeviceId if a match is not found. - * - * @param {Object|Function} stateful - The redux state object or - * {@code getState} function. - * @returns {string} - */ -export function getUserSelectedCameraDeviceId(stateful: IStateful) { - const state = toState(stateful); - const { - userSelectedCameraDeviceId, - userSelectedCameraDeviceLabel - } = state['features/base/settings']; - const { videoInput } = state['features/base/devices'].availableDevices; - - return _getUserSelectedDeviceId({ - availableDevices: videoInput, - - // Operating systems may append " #{number}" somewhere in the label so - // find and strip that bit. - matchRegex: /\s#\d*(?!.*\s#\d*)/, - userSelectedDeviceId: userSelectedCameraDeviceId, - userSelectedDeviceLabel: userSelectedCameraDeviceLabel, - replacement: '' - }); -} - -/** - * Searches known devices for a matching deviceId and fall back to matching on - * label. Returns the stored preferred micDeviceId if a match is not found. - * - * @param {Object|Function} stateful - The redux state object or - * {@code getState} function. - * @returns {string} - */ -export function getUserSelectedMicDeviceId(stateful: IStateful) { - const state = toState(stateful); - const { - userSelectedMicDeviceId, - userSelectedMicDeviceLabel - } = state['features/base/settings']; - const { audioInput } = state['features/base/devices'].availableDevices; - - return _getUserSelectedDeviceId({ - availableDevices: audioInput, - - // Operating systems may append " ({number}-" somewhere in the label so - // find and strip that bit. - matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/, - userSelectedDeviceId: userSelectedMicDeviceId, - userSelectedDeviceLabel: userSelectedMicDeviceLabel, - replacement: ' (' - }); -} - -/** - * Searches known devices for a matching deviceId and fall back to matching on - * label. Returns the stored preferred audioOutputDeviceId if a match is not found. - * - * @param {Object|Function} stateful - The redux state object or - * {@code getState} function. - * @returns {string} - */ -export function getUserSelectedOutputDeviceId(stateful: IStateful) { - const state = toState(stateful); - const { - userSelectedAudioOutputDeviceId, - userSelectedAudioOutputDeviceLabel - } = state['features/base/settings']; - const { audioOutput } = state['features/base/devices'].availableDevices; - - return _getUserSelectedDeviceId({ - availableDevices: audioOutput, - matchRegex: undefined, - userSelectedDeviceId: userSelectedAudioOutputDeviceId, - userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel, - replacement: undefined - }); -} - -/** - * A helper function to abstract the logic for choosing which device ID to - * use. Falls back to fuzzy matching on label if a device ID match is not found. - * - * @param {Object} options - The arguments used to match find the preferred - * device ID from available devices. - * @param {Array} options.availableDevices - The array of currently - * available devices to match against. - * @param {Object} options.matchRegex - The regex to use to find strings - * appended to the label by the operating system. The matches will be replaced - * with options.replacement, with the intent of matching the same device that - * might have a modified label. - * @param {string} options.userSelectedDeviceId - The device ID the participant - * prefers to use. - * @param {string} options.userSelectedDeviceLabel - The label associated with the - * device ID the participant prefers to use. - * @param {string} options.replacement - The string to use with - * options.matchRegex to remove identifies added to the label by the operating - * system. - * @private - * @returns {string} The preferred device ID to use for media. - */ -function _getUserSelectedDeviceId(options: { - availableDevices: MediaDeviceInfo[] | undefined; - matchRegex?: RegExp; - replacement?: string; - userSelectedDeviceId?: string; - userSelectedDeviceLabel?: string; -}) { - const { - availableDevices, - matchRegex = '', - userSelectedDeviceId, - userSelectedDeviceLabel, - replacement = '' - } = options; - - // If there is no label at all, there is no need to fall back to checking - // the label for a fuzzy match. - if (!userSelectedDeviceLabel || !userSelectedDeviceId) { - return userSelectedDeviceId; - } - - const foundMatchingBasedonDeviceId = availableDevices?.find( - candidate => candidate.deviceId === userSelectedDeviceId); - - // Prioritize matching the deviceId - if (foundMatchingBasedonDeviceId) { - return userSelectedDeviceId; - } - - const strippedDeviceLabel - = matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement) - : userSelectedDeviceLabel; - const foundMatchBasedOnLabel = availableDevices?.find(candidate => { - const { label } = candidate; - - if (!label) { - return false; - } else if (strippedDeviceLabel === label) { - return true; - } - - const strippedCandidateLabel - = label.replace(matchRegex, replacement); - - return strippedDeviceLabel === strippedCandidateLabel; - }); - - return foundMatchBasedOnLabel - ? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId; -} - /** * Should we hide the helper dialog when a user tries to do audio only screen sharing. * diff --git a/react/features/base/settings/functions.web.ts b/react/features/base/settings/functions.web.ts index a03199f99b..7da5785841 100644 --- a/react/features/base/settings/functions.web.ts +++ b/react/features/base/settings/functions.web.ts @@ -1,5 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { IReduxState } from '../../app/types'; +import { IStateful } from '../app/types'; +import { toState } from '../redux/functions'; export * from './functions.any'; @@ -58,3 +59,157 @@ function getDeviceIdByType(state: IReduxState, isType: string) { export function getDisplayName(state: IReduxState): string { return state['features/base/settings'].displayName || ''; } + +/** + * Searches known devices for a matching deviceId and fall back to matching on + * label. Returns the stored preferred cameraDeviceId if a match is not found. + * + * @param {Object|Function} stateful - The redux state object or + * {@code getState} function. + * @returns {string} + */ +export function getUserSelectedCameraDeviceId(stateful: IStateful) { + const state = toState(stateful); + const { + userSelectedCameraDeviceId, + userSelectedCameraDeviceLabel + } = state['features/base/settings']; + const { videoInput } = state['features/base/devices'].availableDevices; + + return _getUserSelectedDeviceId({ + availableDevices: videoInput, + + // Operating systems may append " #{number}" somewhere in the label so + // find and strip that bit. + matchRegex: /\s#\d*(?!.*\s#\d*)/, + userSelectedDeviceId: userSelectedCameraDeviceId, + userSelectedDeviceLabel: userSelectedCameraDeviceLabel, + replacement: '' + }); +} + +/** + * Searches known devices for a matching deviceId and fall back to matching on + * label. Returns the stored preferred micDeviceId if a match is not found. + * + * @param {Object|Function} stateful - The redux state object or + * {@code getState} function. + * @returns {string} + */ +export function getUserSelectedMicDeviceId(stateful: IStateful) { + const state = toState(stateful); + const { + userSelectedMicDeviceId, + userSelectedMicDeviceLabel + } = state['features/base/settings']; + const { audioInput } = state['features/base/devices'].availableDevices; + + return _getUserSelectedDeviceId({ + availableDevices: audioInput, + + // Operating systems may append " ({number}-" somewhere in the label so + // find and strip that bit. + matchRegex: /\s\(\d*-\s(?!.*\s\(\d*-\s)/, + userSelectedDeviceId: userSelectedMicDeviceId, + userSelectedDeviceLabel: userSelectedMicDeviceLabel, + replacement: ' (' + }); +} + +/** + * Searches known devices for a matching deviceId and fall back to matching on + * label. Returns the stored preferred audioOutputDeviceId if a match is not found. + * + * @param {Object|Function} stateful - The redux state object or + * {@code getState} function. + * @returns {string} + */ +export function getUserSelectedOutputDeviceId(stateful: IStateful) { + const state = toState(stateful); + const { + userSelectedAudioOutputDeviceId, + userSelectedAudioOutputDeviceLabel + } = state['features/base/settings']; + const { audioOutput } = state['features/base/devices'].availableDevices; + + return _getUserSelectedDeviceId({ + availableDevices: audioOutput, + matchRegex: undefined, + userSelectedDeviceId: userSelectedAudioOutputDeviceId, + userSelectedDeviceLabel: userSelectedAudioOutputDeviceLabel, + replacement: undefined + }); +} + +/** + * A helper function to abstract the logic for choosing which device ID to + * use. Falls back to fuzzy matching on label if a device ID match is not found. + * + * @param {Object} options - The arguments used to match find the preferred + * device ID from available devices. + * @param {Array} options.availableDevices - The array of currently + * available devices to match against. + * @param {Object} options.matchRegex - The regex to use to find strings + * appended to the label by the operating system. The matches will be replaced + * with options.replacement, with the intent of matching the same device that + * might have a modified label. + * @param {string} options.userSelectedDeviceId - The device ID the participant + * prefers to use. + * @param {string} options.userSelectedDeviceLabel - The label associated with the + * device ID the participant prefers to use. + * @param {string} options.replacement - The string to use with + * options.matchRegex to remove identifies added to the label by the operating + * system. + * @private + * @returns {string} The preferred device ID to use for media. + */ +function _getUserSelectedDeviceId(options: { + availableDevices: MediaDeviceInfo[] | undefined; + matchRegex?: RegExp; + replacement?: string; + userSelectedDeviceId?: string; + userSelectedDeviceLabel?: string; +}) { + const { + availableDevices, + matchRegex = '', + userSelectedDeviceId, + userSelectedDeviceLabel, + replacement = '' + } = options; + + // If there is no label at all, there is no need to fall back to checking + // the label for a fuzzy match. + if (!userSelectedDeviceLabel || !userSelectedDeviceId) { + return userSelectedDeviceId; + } + + const foundMatchingBasedonDeviceId = availableDevices?.find( + candidate => candidate.deviceId === userSelectedDeviceId); + + // Prioritize matching the deviceId + if (foundMatchingBasedonDeviceId) { + return userSelectedDeviceId; + } + + const strippedDeviceLabel + = matchRegex ? userSelectedDeviceLabel.replace(matchRegex, replacement) + : userSelectedDeviceLabel; + const foundMatchBasedOnLabel = availableDevices?.find(candidate => { + const { label } = candidate; + + if (!label) { + return false; + } else if (strippedDeviceLabel === label) { + return true; + } + + const strippedCandidateLabel + = label.replace(matchRegex, replacement); + + return strippedDeviceLabel === strippedCandidateLabel; + }); + + return foundMatchBasedOnLabel + ? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId; +} diff --git a/react/features/base/tracks/functions.ts b/react/features/base/tracks/functions.any.ts similarity index 62% rename from react/features/base/tracks/functions.ts rename to react/features/base/tracks/functions.any.ts index 4a08bb12bf..bbe5f12a60 100644 --- a/react/features/base/tracks/functions.ts +++ b/react/features/base/tracks/functions.any.ts @@ -1,29 +1,18 @@ -import { IReduxState, IStore } from '../../app/types'; -import { IStateful } from '../app/types'; +import { IReduxState } from '../../app/types'; import { getMultipleVideoSendingSupportFeatureFlag, getMultipleVideoSupportFeatureFlag } from '../config/functions.any'; -import { isMobileBrowser } from '../environment/utils'; -import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet'; -import { setAudioMuted } from '../media/actions'; +import { JitsiTrackErrors, browser } from '../lib-jitsi-meet'; import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants'; import { getVirtualScreenshareParticipantOwnerId, isScreenShareParticipant } from '../participants/functions'; import { IParticipant } from '../participants/types'; -import { toState } from '../redux/functions'; -import { - getUserSelectedCameraDeviceId, - getUserSelectedMicDeviceId -} from '../settings/functions.any'; -// @ts-ignore -import loadEffects from './loadEffects'; import logger from './logger'; -import { ITrack } from './reducer'; -import { ITrackOptions } from './types'; +import { ITrack } from './types'; /** * Returns root tracks state. @@ -79,223 +68,6 @@ export function isParticipantVideoMuted(participant: IParticipant, state: IRedux return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state); } -/** - * Creates a local video track for presenter. The constraints are computed based - * on the height of the desktop that is being shared. - * - * @param {Object} options - The options with which the local presenter track - * is to be created. - * @param {string|null} [options.cameraDeviceId] - Camera device id or - * {@code undefined} to use app's settings. - * @param {number} desktopHeight - The height of the desktop that is being - * shared. - * @returns {Promise} - */ -export async function createLocalPresenterTrack(options: ITrackOptions, desktopHeight: number) { - const { cameraDeviceId } = options; - - // compute the constraints of the camera track based on the resolution - // of the desktop screen that is being shared. - const cameraHeights = [ 180, 270, 360, 540, 720 ]; - const proportion = 5; - const result = cameraHeights.find( - height => (desktopHeight / proportion) < height); - const constraints = { - video: { - aspectRatio: 4 / 3, - height: { - ideal: result - } - } - }; - const [ videoTrack ] = await JitsiMeetJS.createLocalTracks( - { - cameraDeviceId, - constraints, - devices: [ 'video' ] - }); - - videoTrack.type = MEDIA_TYPE.PRESENTER; - - return videoTrack; -} - -/** - * Create local tracks of specific types. - * - * @param {Object} options - The options with which the local tracks are to be - * created. - * @param {string|null} [options.cameraDeviceId] - Camera device id or - * {@code undefined} to use app's settings. - * @param {string[]} options.devices - Required track types such as 'audio' - * and/or 'video'. - * @param {string|null} [options.micDeviceId] - Microphone device id or - * {@code undefined} to use app's settings. - * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks. - * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet - * should check for a {@code getUserMedia} permission prompt and fire a - * corresponding event. - * @param {IStore} store - The redux store in the context of which the function - * is to execute and from which state such as {@code config} is to be retrieved. - * @returns {Promise} - */ -export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) { - let { cameraDeviceId, micDeviceId } = options; - const { - desktopSharingSourceDevice, - desktopSharingSources, - firePermissionPromptIsShownEvent, - timeout - } = options; - - if (typeof APP !== 'undefined') { - // TODO The app's settings should go in the redux store and then the - // reliance on the global variable APP will go away. - if (!store) { - store = APP.store; // eslint-disable-line no-param-reassign - } - - const state = store.getState(); - - if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) { - cameraDeviceId = getUserSelectedCameraDeviceId(state); - } - if (typeof micDeviceId === 'undefined' || micDeviceId === null) { - micDeviceId = getUserSelectedMicDeviceId(state); - } - } - - // @ts-ignore - const state = store.getState(); - const { - desktopSharingFrameRate, - firefox_fake_device, // eslint-disable-line camelcase - resolution - } = state['features/base/config']; - const constraints = options.constraints ?? state['features/base/config'].constraints; - - return ( - loadEffects(store).then((effectsArray: Object[]) => { - // Filter any undefined values returned by Promise.resolve(). - const effects = effectsArray.filter(effect => Boolean(effect)); - - return JitsiMeetJS.createLocalTracks( - { - cameraDeviceId, - constraints, - desktopSharingFrameRate, - desktopSharingSourceDevice, - desktopSharingSources, - - // Copy array to avoid mutations inside library. - devices: options.devices?.slice(0), - effects, - firefox_fake_device, // eslint-disable-line camelcase - firePermissionPromptIsShownEvent, - micDeviceId, - resolution, - timeout - }) - .catch((err: Error) => { - logger.error('Failed to create local tracks', options.devices, err); - - return Promise.reject(err); - }); - })); -} - -/** - * Returns an object containing a promise which resolves with the created tracks & - * the errors resulting from that process. - * - * @returns {Promise} - * - * @todo Refactor to not use APP. - */ -export function createPrejoinTracks() { - const errors: any = {}; - const initialDevices = [ 'audio' ]; - const requestedAudio = true; - let requestedVideo = false; - const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings']; - - // Always get a handle on the audio input device so that we have statistics even if the user joins the - // conference muted. Previous implementation would only acquire the handle when the user first unmuted, - // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available - // only after that point. - if (startWithAudioMuted) { - APP.store.dispatch(setAudioMuted(true)); - } - - if (!startWithVideoMuted && !startAudioOnly) { - initialDevices.push('video'); - requestedVideo = true; - } - - let tryCreateLocalTracks; - - if (!requestedAudio && !requestedVideo) { - // Resolve with no tracks - tryCreateLocalTracks = Promise.resolve([]); - } else { - tryCreateLocalTracks = createLocalTracksF({ - devices: initialDevices, - firePermissionPromptIsShownEvent: true - }, APP.store) - .catch((err: Error) => { - if (requestedAudio && requestedVideo) { - - // Try audio only... - errors.audioAndVideoError = err; - - return ( - createLocalTracksF({ - devices: [ 'audio' ], - firePermissionPromptIsShownEvent: true - })); - } else if (requestedAudio && !requestedVideo) { - errors.audioOnlyError = err; - - return []; - } else if (requestedVideo && !requestedAudio) { - errors.videoOnlyError = err; - - return []; - } - logger.error('Should never happen'); - }) - .catch((err: Error) => { - // Log this just in case... - if (!requestedAudio) { - logger.error('The impossible just happened', err); - } - errors.audioOnlyError = err; - - // Try video only... - return requestedVideo - ? createLocalTracksF({ - devices: [ 'video' ], - firePermissionPromptIsShownEvent: true - }) - : []; - }) - .catch((err: Error) => { - // Log this just in case... - if (!requestedVideo) { - logger.error('The impossible just happened', err); - } - errors.videoOnlyError = err; - - return []; - }); - } - - return { - tryCreateLocalTracks, - errors - }; -} - /** * Returns local audio track. * @@ -667,16 +439,3 @@ export function setTrackMuted(track: any, muted: boolean, state: IReduxState) { } }); } - -/** - * Determines whether toggle camera should be enabled or not. - * - * @param {Function|Object} stateful - The redux store or {@code getState} function. - * @returns {boolean} - Whether toggle camera should be enabled. - */ -export function isToggleCameraEnabled(stateful: IStateful) { - const state = toState(stateful); - const { videoInput } = state['features/base/devices'].availableDevices; - - return isMobileBrowser() && Number(videoInput?.length) > 1; -} diff --git a/react/features/base/tracks/functions.native.ts b/react/features/base/tracks/functions.native.ts new file mode 100644 index 0000000000..d59e4fc750 --- /dev/null +++ b/react/features/base/tracks/functions.native.ts @@ -0,0 +1,45 @@ +import { IStore } from '../../app/types'; +import JitsiMeetJS from '../lib-jitsi-meet'; + +import { ITrackOptions } from './types'; + +export * from './functions.any'; + +/** + * Create local tracks of specific types. + * + * @param {Object} options - The options with which the local tracks are to be + * created. + * @param {string|null} [options.cameraDeviceId] - Camera device id or + * {@code undefined} to use app's settings. + * @param {string[]} options.devices - Required track types such as 'audio' + * and/or 'video'. + * @param {string|null} [options.micDeviceId] - Microphone device id or + * {@code undefined} to use app's settings. + * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks. + * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet + * should check for a {@code getUserMedia} permission prompt and fire a + * corresponding event. + * @param {IStore} store - The redux store in the context of which the function + * is to execute and from which state such as {@code config} is to be retrieved. + * @returns {Promise} + */ +export function createLocalTracksF(options: ITrackOptions = {}, store: IStore) { + const { cameraDeviceId, micDeviceId } = options; + const state = store.getState(); + const { + resolution + } = state['features/base/config']; + const constraints = options.constraints ?? state['features/base/config'].constraints; + + return JitsiMeetJS.createLocalTracks( + { + cameraDeviceId, + constraints, + + // Copy array to avoid mutations inside library. + devices: options.devices?.slice(0), + micDeviceId, + resolution + }); +} diff --git a/react/features/base/tracks/functions.web.ts b/react/features/base/tracks/functions.web.ts new file mode 100644 index 0000000000..e43477fc1b --- /dev/null +++ b/react/features/base/tracks/functions.web.ts @@ -0,0 +1,242 @@ +import { IStore } from '../../app/types'; +import { IStateful } from '../app/types'; +import { isMobileBrowser } from '../environment/utils'; +import JitsiMeetJS from '../lib-jitsi-meet'; +import { setAudioMuted } from '../media/actions'; +import { MEDIA_TYPE } from '../media/constants'; +import { toState } from '../redux/functions'; +import { + getUserSelectedCameraDeviceId, + getUserSelectedMicDeviceId +} from '../settings/functions.web'; + +// @ts-ignore +import loadEffects from './loadEffects'; +import logger from './logger'; +import { ITrackOptions } from './types'; + +export * from './functions.any'; + +/** + * Create local tracks of specific types. + * + * @param {Object} options - The options with which the local tracks are to be + * created. + * @param {string|null} [options.cameraDeviceId] - Camera device id or + * {@code undefined} to use app's settings. + * @param {string[]} options.devices - Required track types such as 'audio' + * and/or 'video'. + * @param {string|null} [options.micDeviceId] - Microphone device id or + * {@code undefined} to use app's settings. + * @param {number|undefined} [oprions.timeout] - A timeout for JitsiMeetJS.createLocalTracks used to create the tracks. + * @param {boolean} [options.firePermissionPromptIsShownEvent] - Whether lib-jitsi-meet + * should check for a {@code getUserMedia} permission prompt and fire a + * corresponding event. + * @param {IStore} store - The redux store in the context of which the function + * is to execute and from which state such as {@code config} is to be retrieved. + * @returns {Promise} + */ +export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore) { + let { cameraDeviceId, micDeviceId } = options; + const { + desktopSharingSourceDevice, + desktopSharingSources, + firePermissionPromptIsShownEvent, + timeout + } = options; + + // TODO The app's settings should go in the redux store and then the + // reliance on the global variable APP will go away. + store = store || APP.store; // eslint-disable-line no-param-reassign + + const state = store.getState(); + + if (typeof cameraDeviceId === 'undefined' || cameraDeviceId === null) { + cameraDeviceId = getUserSelectedCameraDeviceId(state); + } + if (typeof micDeviceId === 'undefined' || micDeviceId === null) { + micDeviceId = getUserSelectedMicDeviceId(state); + } + + const { + desktopSharingFrameRate, + firefox_fake_device, // eslint-disable-line camelcase + resolution + } = state['features/base/config']; + const constraints = options.constraints ?? state['features/base/config'].constraints; + + return ( + loadEffects(store).then((effectsArray: Object[]) => { + // Filter any undefined values returned by Promise.resolve(). + const effects = effectsArray.filter(effect => Boolean(effect)); + + return JitsiMeetJS.createLocalTracks( + { + cameraDeviceId, + constraints, + desktopSharingFrameRate, + desktopSharingSourceDevice, + desktopSharingSources, + + // Copy array to avoid mutations inside library. + devices: options.devices?.slice(0), + effects, + firefox_fake_device, // eslint-disable-line camelcase + firePermissionPromptIsShownEvent, + micDeviceId, + resolution, + timeout + }) + .catch((err: Error) => { + logger.error('Failed to create local tracks', options.devices, err); + + return Promise.reject(err); + }); + })); +} + +/** + * Creates a local video track for presenter. The constraints are computed based + * on the height of the desktop that is being shared. + * + * @param {Object} options - The options with which the local presenter track + * is to be created. + * @param {string|null} [options.cameraDeviceId] - Camera device id or + * {@code undefined} to use app's settings. + * @param {number} desktopHeight - The height of the desktop that is being + * shared. + * @returns {Promise} + */ +export async function createLocalPresenterTrack(options: ITrackOptions, desktopHeight: number) { + const { cameraDeviceId } = options; + + // compute the constraints of the camera track based on the resolution + // of the desktop screen that is being shared. + const cameraHeights = [ 180, 270, 360, 540, 720 ]; + const proportion = 5; + const result = cameraHeights.find( + height => (desktopHeight / proportion) < height); + const constraints = { + video: { + aspectRatio: 4 / 3, + height: { + ideal: result + } + } + }; + const [ videoTrack ] = await JitsiMeetJS.createLocalTracks( + { + cameraDeviceId, + constraints, + devices: [ 'video' ] + }); + + videoTrack.type = MEDIA_TYPE.PRESENTER; + + return videoTrack; +} + +/** + * Returns an object containing a promise which resolves with the created tracks & + * the errors resulting from that process. + * + * @returns {Promise} + * + * @todo Refactor to not use APP. + */ +export function createPrejoinTracks() { + const errors: any = {}; + const initialDevices = [ 'audio' ]; + const requestedAudio = true; + let requestedVideo = false; + const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings']; + + // Always get a handle on the audio input device so that we have statistics even if the user joins the + // conference muted. Previous implementation would only acquire the handle when the user first unmuted, + // which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available + // only after that point. + if (startWithAudioMuted) { + APP.store.dispatch(setAudioMuted(true)); + } + + if (!startWithVideoMuted && !startAudioOnly) { + initialDevices.push('video'); + requestedVideo = true; + } + + let tryCreateLocalTracks; + + if (!requestedAudio && !requestedVideo) { + // Resolve with no tracks + tryCreateLocalTracks = Promise.resolve([]); + } else { + tryCreateLocalTracks = createLocalTracksF({ + devices: initialDevices, + firePermissionPromptIsShownEvent: true + }, APP.store) + .catch((err: Error) => { + if (requestedAudio && requestedVideo) { + + // Try audio only... + errors.audioAndVideoError = err; + + return ( + createLocalTracksF({ + devices: [ 'audio' ], + firePermissionPromptIsShownEvent: true + })); + } else if (requestedAudio && !requestedVideo) { + errors.audioOnlyError = err; + + return []; + } else if (requestedVideo && !requestedAudio) { + errors.videoOnlyError = err; + + return []; + } + logger.error('Should never happen'); + }) + .catch((err: Error) => { + // Log this just in case... + if (!requestedAudio) { + logger.error('The impossible just happened', err); + } + errors.audioOnlyError = err; + + // Try video only... + return requestedVideo + ? createLocalTracksF({ + devices: [ 'video' ], + firePermissionPromptIsShownEvent: true + }) + : []; + }) + .catch((err: Error) => { + // Log this just in case... + if (!requestedVideo) { + logger.error('The impossible just happened', err); + } + errors.videoOnlyError = err; + + return []; + }); + } + + return { + tryCreateLocalTracks, + errors + }; +} + +/** + * Determines whether toggle camera should be enabled or not. + * + * @param {Function|Object} stateful - The redux store or {@code getState} function. + * @returns {boolean} - Whether toggle camera should be enabled. + */ +export function isToggleCameraEnabled(stateful: IStateful) { + const state = toState(stateful); + const { videoInput } = state['features/base/devices'].availableDevices; + + return isMobileBrowser() && Number(videoInput?.length) > 1; +} diff --git a/react/features/base/tracks/middleware.ts b/react/features/base/tracks/middleware.any.ts similarity index 56% rename from react/features/base/tracks/middleware.ts rename to react/features/base/tracks/middleware.any.ts index 07d288482f..26dbe3931e 100644 --- a/react/features/base/tracks/middleware.ts +++ b/react/features/base/tracks/middleware.any.ts @@ -2,11 +2,9 @@ import { batch } from 'react-redux'; import { IStore } from '../../app/types'; import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes'; -import { hideNotification } from '../../notifications/actions'; import { isPrejoinPageVisible } from '../../prejoin/functions'; import { getCurrentConference } from '../conference/functions'; import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any'; -import { getAvailableDevices } from '../devices/actions'; import { SET_AUDIO_MUTED, SET_CAMERA_FACING_MODE, @@ -14,43 +12,31 @@ import { SET_VIDEO_MUTED, TOGGLE_CAMERA_FACING_MODE } from '../media/actionTypes'; -import { setScreenshareMuted, toggleCameraFacingMode } from '../media/actions'; +import { toggleCameraFacingMode } from '../media/actions'; import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, SCREENSHARE_MUTISM_AUTHORITY, - VIDEO_MUTISM_AUTHORITY, - VIDEO_TYPE + VIDEO_MUTISM_AUTHORITY } from '../media/constants'; import MiddlewareRegistry from '../redux/MiddlewareRegistry'; import StateListenerRegistry from '../redux/StateListenerRegistry'; import { - TRACK_ADDED, - TRACK_MUTE_UNMUTE_FAILED, - TRACK_NO_DATA_FROM_SOURCE, - TRACK_REMOVED, - TRACK_STOPPED, TRACK_UPDATED } from './actionTypes'; import { createLocalTracksA, destroyLocalTracks, - showNoDataFromSourceVideoError, - toggleScreensharing, trackMuteUnmuteFailed, - trackNoDataFromSourceNotificationInfoChanged, trackRemoved } from './actions'; import { getLocalTrack, - getTrackByJitsiTrack, isUserInteractionRequiredForUnmute, setTrackMuted } from './functions'; -import { ITrack } from './reducer'; - import './subscriber'; /** @@ -63,29 +49,6 @@ import './subscriber'; */ MiddlewareRegistry.register(store => next => action => { switch (action.type) { - case TRACK_ADDED: { - const { local } = action.track; - - // The devices list needs to be refreshed when no initial video permissions - // were granted and a local video track is added by umuting the video. - if (local) { - store.dispatch(getAvailableDevices()); - } - - break; - } - case TRACK_NO_DATA_FROM_SOURCE: { - const result = next(action); - - _handleNoDataFromSourceErrors(store, action); - - return result; - } - - case TRACK_REMOVED: { - _removeNoDataFromSourceNotification(store, action.track); - break; - } case SET_AUDIO_MUTED: if (!action.muted && isUserInteractionRequiredForUnmute(store.getState())) { @@ -153,82 +116,6 @@ MiddlewareRegistry.register(store => next => action => { } break; } - - case TRACK_MUTE_UNMUTE_FAILED: { - const { jitsiTrack } = action.track; - const muted = action.wasMuted; - const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO; - - if (typeof APP !== 'undefined') { - if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP - && getMultipleVideoSendingSupportFeatureFlag(store.getState())) { - store.dispatch(setScreenshareMuted(!muted)); - } else if (isVideoTrack) { - APP.conference.setVideoMuteStatus(); - } else { - APP.conference.setAudioMuteStatus(!muted); - } - } - break; - } - - case TRACK_STOPPED: { - const { jitsiTrack } = action.track; - - if (typeof APP !== 'undefined' - && getMultipleVideoSendingSupportFeatureFlag(store.getState()) - && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) { - store.dispatch(toggleScreensharing(false)); - } - break; - } - - case TRACK_UPDATED: { - // TODO Remove the following calls to APP.UI once components interested - // in track mute changes are moved into React and/or redux. - if (typeof APP !== 'undefined') { - const result = next(action); - const state = store.getState(); - - if (isPrejoinPageVisible(state)) { - return result; - } - - const { jitsiTrack } = action.track; - const muted = jitsiTrack.isMuted(); - const participantID = jitsiTrack.getParticipantId(); - const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO; - - if (isVideoTrack) { - // Do not change the video mute state for local presenter tracks. - if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) { - APP.conference.mutePresenter(muted); - } else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) { - APP.conference.setVideoMuteStatus(); - } else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) { - !getMultipleVideoSendingSupportFeatureFlag(state) - && store.dispatch(toggleScreensharing(false, false, true)); - } else { - APP.UI.setVideoMuted(participantID); - } - } else if (jitsiTrack.isLocal()) { - APP.conference.setAudioMuteStatus(muted); - } else { - APP.UI.setAudioMuted(participantID, muted); - } - - return result; - } - - // Mobile. - const { jitsiTrack, local } = action.track; - - if (local && jitsiTrack.isMuted() - && jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) { - store.dispatch(toggleScreensharing(false)); - } - break; - } } return next(action); @@ -259,53 +146,6 @@ StateListenerRegistry.register( } }); -/** - * Handles no data from source errors. - * - * @param {Store} store - The redux store in which the specified action is - * dispatched. - * @param {Action} action - The redux action dispatched in the specified store. - * @private - * @returns {void} - */ -function _handleNoDataFromSourceErrors(store: IStore, action: any) { - const { getState, dispatch } = store; - - const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack); - - if (!track || !track.local) { - return; - } - - const { jitsiTrack } = track; - - if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) { - _removeNoDataFromSourceNotification(store, action.track); - } - - if (track.mediaType === MEDIA_TYPE.VIDEO) { - const { noDataFromSourceNotificationInfo = {} } = track; - - if (track.isReceivingData) { - if (noDataFromSourceNotificationInfo.timeout) { - clearTimeout(noDataFromSourceNotificationInfo.timeout); - dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined)); - } - - // try to remove the notification if there is one. - _removeNoDataFromSourceNotification(store, action.track); - } else { - if (noDataFromSourceNotificationInfo.timeout) { - return; - } - - const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000); - - dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout })); - } - } -} - /** * Gets the local track associated with a specific {@code MEDIA_TYPE} in a * specific redux store. @@ -334,23 +174,6 @@ function _getLocalTrack( includePending)); } -/** - * Removes the no data from source notification associated with the JitsiTrack if displayed. - * - * @param {Store} store - The redux store. - * @param {Track} track - The redux action dispatched in the specified store. - * @returns {void} - */ -function _removeNoDataFromSourceNotification({ getState, dispatch }: IStore, track: ITrack) { - const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack); - const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {}; - - if (noDataFromSourceNotificationInfo?.uid) { - dispatch(hideNotification(noDataFromSourceNotificationInfo.uid)); - dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined)); - } -} - /** * Mutes or unmutes a local track with a specific media type. * diff --git a/react/features/base/tracks/middleware.native.ts b/react/features/base/tracks/middleware.native.ts new file mode 100644 index 0000000000..b5d64a00be --- /dev/null +++ b/react/features/base/tracks/middleware.native.ts @@ -0,0 +1,38 @@ +import { + MEDIA_TYPE, + VIDEO_TYPE +} from '../media/constants'; +import MiddlewareRegistry from '../redux/MiddlewareRegistry'; + +import { + TRACK_UPDATED +} from './actionTypes'; +import { + toggleScreensharing +} from './actions.native'; + +import './middleware.any'; + +/** + * Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and, + * respectively, creates/destroys local media tracks. Also listens to + * media-related actions and performs corresponding operations with tracks. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case TRACK_UPDATED: { + const { jitsiTrack, local } = action.track; + + if (local && jitsiTrack.isMuted() + && jitsiTrack.type === MEDIA_TYPE.VIDEO && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) { + store.dispatch(toggleScreensharing(false)); + } + break; + } + } + + return next(action); +}); diff --git a/react/features/base/tracks/middleware.web.ts b/react/features/base/tracks/middleware.web.ts new file mode 100644 index 0000000000..5c670b13bb --- /dev/null +++ b/react/features/base/tracks/middleware.web.ts @@ -0,0 +1,198 @@ +import { IStore } from '../../app/types'; +import { hideNotification } from '../../notifications/actions'; +import { isPrejoinPageVisible } from '../../prejoin/functions'; +import { getMultipleVideoSendingSupportFeatureFlag } from '../config/functions.any'; +import { getAvailableDevices } from '../devices/actions.web'; +import { setScreenshareMuted } from '../media/actions'; +import { + MEDIA_TYPE, + VIDEO_TYPE +} from '../media/constants'; +import MiddlewareRegistry from '../redux/MiddlewareRegistry'; + +import { + TRACK_ADDED, + TRACK_MUTE_UNMUTE_FAILED, + TRACK_NO_DATA_FROM_SOURCE, + TRACK_REMOVED, + TRACK_STOPPED, + TRACK_UPDATED +} from './actionTypes'; +import { + showNoDataFromSourceVideoError, + toggleScreensharing, + trackNoDataFromSourceNotificationInfoChanged +} from './actions.web'; +import { + getTrackByJitsiTrack +} from './functions.web'; +import { ITrack } from './types'; + +import './middleware.any'; + +/** + * Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and, + * respectively, creates/destroys local media tracks. Also listens to + * media-related actions and performs corresponding operations with tracks. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case TRACK_ADDED: { + const { local } = action.track; + + // The devices list needs to be refreshed when no initial video permissions + // were granted and a local video track is added by umuting the video. + if (local) { + store.dispatch(getAvailableDevices()); + } + + break; + } + case TRACK_NO_DATA_FROM_SOURCE: { + const result = next(action); + + _handleNoDataFromSourceErrors(store, action); + + return result; + } + + case TRACK_REMOVED: { + _removeNoDataFromSourceNotification(store, action.track); + break; + } + + case TRACK_MUTE_UNMUTE_FAILED: { + const { jitsiTrack } = action.track; + const muted = action.wasMuted; + const isVideoTrack = jitsiTrack.getType() !== MEDIA_TYPE.AUDIO; + + if (isVideoTrack && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP + && getMultipleVideoSendingSupportFeatureFlag(store.getState())) { + store.dispatch(setScreenshareMuted(!muted)); + } else if (isVideoTrack) { + APP.conference.setVideoMuteStatus(); + } else { + APP.conference.setAudioMuteStatus(!muted); + } + + break; + } + + case TRACK_STOPPED: { + const { jitsiTrack } = action.track; + + if (getMultipleVideoSendingSupportFeatureFlag(store.getState()) + && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) { + store.dispatch(toggleScreensharing(false)); + } + break; + } + + case TRACK_UPDATED: { + // TODO Remove the following calls to APP.UI once components interested + // in track mute changes are moved into React and/or redux. + + const result = next(action); + const state = store.getState(); + + if (isPrejoinPageVisible(state)) { + return result; + } + + const { jitsiTrack } = action.track; + const muted = jitsiTrack.isMuted(); + const participantID = jitsiTrack.getParticipantId(); + const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO; + + if (isVideoTrack) { + // Do not change the video mute state for local presenter tracks. + if (jitsiTrack.type === MEDIA_TYPE.PRESENTER) { + APP.conference.mutePresenter(muted); + } else if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) { + APP.conference.setVideoMuteStatus(); + } else if (jitsiTrack.isLocal() && muted && jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP) { + !getMultipleVideoSendingSupportFeatureFlag(state) + && store.dispatch(toggleScreensharing(false, false, true)); + } else { + APP.UI.setVideoMuted(participantID); + } + } else if (jitsiTrack.isLocal()) { + APP.conference.setAudioMuteStatus(muted); + } else { + APP.UI.setAudioMuted(participantID, muted); + } + + return result; + } + + } + + return next(action); +}); + +/** + * Handles no data from source errors. + * + * @param {Store} store - The redux store in which the specified action is + * dispatched. + * @param {Action} action - The redux action dispatched in the specified store. + * @private + * @returns {void} + */ +function _handleNoDataFromSourceErrors(store: IStore, action: any) { + const { getState, dispatch } = store; + + const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack); + + if (!track || !track.local) { + return; + } + + const { jitsiTrack } = track; + + if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) { + _removeNoDataFromSourceNotification(store, action.track); + } + + if (track.mediaType === MEDIA_TYPE.VIDEO) { + const { noDataFromSourceNotificationInfo = {} } = track; + + if (track.isReceivingData) { + if (noDataFromSourceNotificationInfo.timeout) { + clearTimeout(noDataFromSourceNotificationInfo.timeout); + dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined)); + } + + // try to remove the notification if there is one. + _removeNoDataFromSourceNotification(store, action.track); + } else { + if (noDataFromSourceNotificationInfo.timeout) { + return; + } + + const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000); + + dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout })); + } + } +} + +/** + * Removes the no data from source notification associated with the JitsiTrack if displayed. + * + * @param {Store} store - The redux store. + * @param {Track} track - The redux action dispatched in the specified store. + * @returns {void} + */ +function _removeNoDataFromSourceNotification({ getState, dispatch }: IStore, track: ITrack) { + const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack); + const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {}; + + if (noDataFromSourceNotificationInfo?.uid) { + dispatch(hideNotification(noDataFromSourceNotificationInfo.uid)); + dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined)); + } +} diff --git a/react/features/base/tracks/reducer.ts b/react/features/base/tracks/reducer.ts index 6f5c9533c4..26f0fac50a 100644 --- a/react/features/base/tracks/reducer.ts +++ b/react/features/base/tracks/reducer.ts @@ -1,4 +1,3 @@ -import { MediaType } from '../media/constants'; import { PARTICIPANT_ID_CHANGED } from '../participants/actionTypes'; import ReducerRegistry from '../redux/ReducerRegistry'; import { set } from '../redux/functions'; @@ -14,48 +13,7 @@ import { TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT, TRACK_WILL_CREATE } from './actionTypes'; - -export interface ITrack { - isReceivingData: boolean; - jitsiTrack: any; - lastMediaEvent?: string; - local: boolean; - mediaType: MediaType; - mirror: boolean; - muted: boolean; - noDataFromSourceNotificationInfo?: { - timeout?: number; - uid?: string; - }; - participantId: string; - streamingStatus?: string; - videoStarted: boolean; - videoType?: string | null; -} - -/** - * Track type. - * - * @typedef {object} Track - * @property {JitsiLocalTrack|JitsiRemoteTrack} jitsiTrack - The associated - * {@code JitsiTrack} instance. Optional for local tracks if those are still - * being created (ie {@code getUserMedia} is still in progress). - * @property {Promise} [gumProcess] - If a local track is still being created, - * it will have no {@code JitsiTrack}, but a {@code gumProcess} set to a - * {@code Promise} with and extra {@code cancel()}. - * @property {boolean} local=false - If the track is local. - * @property {MEDIA_TYPE} mediaType=false - The media type of the track. - * @property {boolean} mirror=false - The indicator which determines whether the - * display/rendering of the track should be mirrored. It only makes sense in the - * context of video (at least at the time of this writing). - * @property {boolean} muted=false - If the track is muted. - * @property {(string|undefined)} participantId - The ID of the participant whom - * the track belongs to. - * @property {boolean} videoStarted=false - If the video track has already - * started to play. - * @property {(VIDEO_TYPE|undefined)} videoType - The type of video track if - * any. - */ +import { ITrack } from './types'; /** * Reducer function for a single track. diff --git a/react/features/base/tracks/types.ts b/react/features/base/tracks/types.ts index db72cf0a3c..283ccca3fc 100644 --- a/react/features/base/tracks/types.ts +++ b/react/features/base/tracks/types.ts @@ -1,3 +1,5 @@ +import { MediaType } from '../media/constants'; + export interface ITrackOptions { cameraDeviceId?: string | null; constraints?: { @@ -18,6 +20,47 @@ export interface ITrackOptions { timeout?: number; } +/** + * Track type. + * + * @typedef {object} Track + * @property {JitsiLocalTrack|JitsiRemoteTrack} jitsiTrack - The associated + * {@code JitsiTrack} instance. Optional for local tracks if those are still + * being created (ie {@code getUserMedia} is still in progress). + * @property {Promise} [gumProcess] - If a local track is still being created, + * it will have no {@code JitsiTrack}, but a {@code gumProcess} set to a + * {@code Promise} with and extra {@code cancel()}. + * @property {boolean} local=false - If the track is local. + * @property {MEDIA_TYPE} mediaType=false - The media type of the track. + * @property {boolean} mirror=false - The indicator which determines whether the + * display/rendering of the track should be mirrored. It only makes sense in the + * context of video (at least at the time of this writing). + * @property {boolean} muted=false - If the track is muted. + * @property {(string|undefined)} participantId - The ID of the participant whom + * the track belongs to. + * @property {boolean} videoStarted=false - If the video track has already + * started to play. + * @property {(VIDEO_TYPE|undefined)} videoType - The type of video track if + * any. + */ +export interface ITrack { + isReceivingData: boolean; + jitsiTrack: any; + lastMediaEvent?: string; + local: boolean; + mediaType: MediaType; + mirror: boolean; + muted: boolean; + noDataFromSourceNotificationInfo?: { + timeout?: number; + uid?: string; + }; + participantId: string; + streamingStatus?: string; + videoStarted: boolean; + videoType?: string | null; +} + export interface IToggleScreenSharingOptions { audioOnly: boolean; enabled?: boolean; diff --git a/react/features/base/ui/components/_.native.ts b/react/features/base/ui/components/_.native.ts deleted file mode 100644 index 738c4d2b8a..0000000000 --- a/react/features/base/ui/components/_.native.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './native'; diff --git a/react/features/base/ui/components/_.web.ts b/react/features/base/ui/components/_.web.ts deleted file mode 100644 index b80c83af34..0000000000 --- a/react/features/base/ui/components/_.web.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './web'; diff --git a/react/features/base/ui/components/native/IconButton.tsx b/react/features/base/ui/components/native/IconButton.tsx index 724ba9be18..3a0e03a6e6 100644 --- a/react/features/base/ui/components/native/IconButton.tsx +++ b/react/features/base/ui/components/native/IconButton.tsx @@ -1,7 +1,9 @@ +/* eslint-disable lines-around-comment */ import React from 'react'; import { TouchableRipple } from 'react-native-paper'; import Icon from '../../../icons/components/Icon'; +// @ts-ignore import styles from '../../../react/components/native/styles'; import { IIconButtonProps } from '../../../react/types'; import { BUTTON_TYPES } from '../../constants'; diff --git a/react/features/base/ui/components/web/index.ts b/react/features/base/ui/components/web/index.ts deleted file mode 100644 index eae9c8e3b3..0000000000 --- a/react/features/base/ui/components/web/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Button } from './Button'; diff --git a/react/features/base/util/helpers.ts b/react/features/base/util/helpers.ts index 5daf57e41e..84ac81e163 100644 --- a/react/features/base/util/helpers.ts +++ b/react/features/base/util/helpers.ts @@ -63,7 +63,7 @@ export function escapeRegexp(s: string) { * @param {Object} w - Window object to use instead of the built in one. * @returns {string} */ -export function getBaseUrl(w: Window = window) { +export function getBaseUrl(w: typeof window = window) { const doc = w.document; const base = doc.querySelector('base'); diff --git a/react/features/chat/components/web/DisplayNameForm.tsx b/react/features/chat/components/web/DisplayNameForm.tsx index 54867d8e1d..d4118f4fe3 100644 --- a/react/features/chat/components/web/DisplayNameForm.tsx +++ b/react/features/chat/components/web/DisplayNameForm.tsx @@ -5,7 +5,7 @@ import { IStore } from '../../../app/types'; import { translate } from '../../../base/i18n/functions'; import { connect } from '../../../base/redux/functions'; import { updateSettings } from '../../../base/settings/actions'; -import { Button } from '../../../base/ui/components/web'; +import Button from '../../../base/ui/components/web/Button'; import Input from '../../../base/ui/components/web/Input'; // @ts-ignore diff --git a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js index 2ba4ad8d20..a6e6b0af4e 100644 --- a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js +++ b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js @@ -8,8 +8,8 @@ import { sendAnalytics } from '../../analytics'; import { getCurrentConference } from '../../base/conference/functions'; +import checkChromeExtensionsInstalled from '../../base/environment/checkChromeExtensionsInstalled'; import { - checkChromeExtensionsInstalled, isMobileBrowser } from '../../base/environment/utils'; import { translate } from '../../base/i18n'; diff --git a/react/features/connection-indicator/actions.ts b/react/features/connection-indicator/actions.web.ts similarity index 100% rename from react/features/connection-indicator/actions.ts rename to react/features/connection-indicator/actions.web.ts diff --git a/react/features/connection-indicator/functions.ts b/react/features/connection-indicator/functions.ts index 65fcb24a52..90dfbfb61c 100644 --- a/react/features/connection-indicator/functions.ts +++ b/react/features/connection-indicator/functions.ts @@ -1,6 +1,6 @@ import { JitsiParticipantConnectionStatus, JitsiTrackStreamingStatus } from '../base/lib-jitsi-meet'; import { IParticipant } from '../base/participants/types'; -import { ITrack } from '../base/tracks/reducer'; +import { ITrack } from '../base/tracks/types'; /** * Checks if the passed track's streaming status is active. diff --git a/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js b/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js index 17c05371a1..eab7cadc41 100644 --- a/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js +++ b/react/features/deep-linking/components/DeepLinkingDesktopPage.web.js @@ -8,7 +8,7 @@ import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics'; import { isSupportedBrowser } from '../../base/environment'; import { translate } from '../../base/i18n'; import { connect } from '../../base/redux'; -import { Button } from '../../base/ui/components/web'; +import Button from '../../base/ui/components/web/Button'; import { BUTTON_TYPES } from '../../base/ui/constants'; import { openDesktopApp, diff --git a/react/features/device-selection/actions.ts b/react/features/device-selection/actions.web.ts similarity index 100% rename from react/features/device-selection/actions.ts rename to react/features/device-selection/actions.web.ts diff --git a/react/features/device-selection/functions.ts b/react/features/device-selection/functions.web.ts similarity index 98% rename from react/features/device-selection/functions.ts rename to react/features/device-selection/functions.web.ts index 45da1c7798..1691e0ff63 100644 --- a/react/features/device-selection/functions.ts +++ b/react/features/device-selection/functions.web.ts @@ -6,13 +6,13 @@ import { setAudioInputDeviceAndUpdateSettings, setAudioOutputDevice, setVideoInputDeviceAndUpdateSettings -} from '../base/devices/actions'; +} from '../base/devices/actions.web'; import { areDeviceLabelsInitialized, getAudioOutputDeviceId, getDeviceIdByLabel, groupDevicesByKind -} from '../base/devices/functions'; +} from '../base/devices/functions.web'; import { isIosMobileBrowser } from '../base/environment/utils'; import JitsiMeetJS from '../base/lib-jitsi-meet'; import { toState } from '../base/redux/functions'; @@ -20,7 +20,7 @@ import { getUserSelectedCameraDeviceId, getUserSelectedMicDeviceId, getUserSelectedOutputDeviceId -} from '../base/settings/functions.any'; +} from '../base/settings/functions.web'; /** * Returns the properties for the device selection dialog from Redux state. diff --git a/react/features/external-api/middleware.js b/react/features/external-api/middleware.js index 13bf042f6b..fa1755a8d8 100644 --- a/react/features/external-api/middleware.js +++ b/react/features/external-api/middleware.js @@ -8,7 +8,7 @@ import { KICKED_OUT } from '../base/conference'; import { SET_CONFIG } from '../base/config'; -import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices'; +import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices/actionTypes'; import { JitsiConferenceErrors } from '../base/lib-jitsi-meet'; import { DOMINANT_SPEAKER_CHANGED, diff --git a/react/features/face-landmarks/types.ts b/react/features/face-landmarks/types.ts index 6c9f170d89..406815836f 100644 --- a/react/features/face-landmarks/types.ts +++ b/react/features/face-landmarks/types.ts @@ -1,4 +1,6 @@ export type DetectInput = { + + // @ts-ignore image: ImageBitmap | ImageData; threshold: number; }; diff --git a/react/features/participants-pane/components/native/ParticipantsPaneFooter.tsx b/react/features/participants-pane/components/native/ParticipantsPaneFooter.tsx index b68a661dc1..48c7d541a3 100644 --- a/react/features/participants-pane/components/native/ParticipantsPaneFooter.tsx +++ b/react/features/participants-pane/components/native/ParticipantsPaneFooter.tsx @@ -1,3 +1,4 @@ +/* eslint-disable lines-around-comment */ import React, { useCallback } from 'react'; import { View } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; @@ -7,10 +8,13 @@ import { IconHorizontalPoints } from '../../../base/icons/svg'; import Button from '../../../base/ui/components/native/Button'; import IconButton from '../../../base/ui/components/native/IconButton'; import { BUTTON_TYPES } from '../../../base/ui/constants'; +// @ts-ignore import MuteEveryoneDialog from '../../../video-menu/components/native/MuteEveryoneDialog'; import { isMoreActionsVisible, isMuteAllVisible } from '../../functions'; +// @ts-ignore import { ContextMenuMore } from './ContextMenuMore'; +// @ts-ignore import styles from './styles'; diff --git a/react/features/prejoin/components/web/dialogs/DialInDialog.tsx b/react/features/prejoin/components/web/dialogs/DialInDialog.tsx index 93cf1bc0e9..ca076b9303 100644 --- a/react/features/prejoin/components/web/dialogs/DialInDialog.tsx +++ b/react/features/prejoin/components/web/dialogs/DialInDialog.tsx @@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui'; import { translate } from '../../../../base/i18n/functions'; import Icon from '../../../../base/icons/components/Icon'; import { IconArrowLeft } from '../../../../base/icons/svg'; -import { Button } from '../../../../base/ui/components/web'; +import Button from '../../../../base/ui/components/web/Button'; // @ts-ignore import { getCountryCodeFromPhone } from '../../../utils'; // @ts-ignore diff --git a/react/features/prejoin/components/web/dialogs/DialOutDialog.tsx b/react/features/prejoin/components/web/dialogs/DialOutDialog.tsx index 5b77250083..aa2b9644b4 100644 --- a/react/features/prejoin/components/web/dialogs/DialOutDialog.tsx +++ b/react/features/prejoin/components/web/dialogs/DialOutDialog.tsx @@ -8,7 +8,7 @@ import { makeStyles } from 'tss-react/mui'; import { translate } from '../../../../base/i18n/functions'; import Icon from '../../../../base/icons/components/Icon'; import { IconClose } from '../../../../base/icons/svg'; -import { Button } from '../../../../base/ui/components/web'; +import Button from '../../../../base/ui/components/web/Button'; // @ts-ignore import Label from '../Label'; // @ts-ignore diff --git a/react/features/settings/components/web/CalendarTab.js b/react/features/settings/components/web/CalendarTab.js index 19e6e523e4..e5c211d724 100644 --- a/react/features/settings/components/web/CalendarTab.js +++ b/react/features/settings/components/web/CalendarTab.js @@ -5,7 +5,7 @@ import React, { Component } from 'react'; import { translate } from '../../../base/i18n'; import { connect } from '../../../base/redux'; -import { Button } from '../../../base/ui/components/web'; +import Button from '../../../base/ui/components/web/Button'; import { CALENDAR_TYPE, MicrosoftSignInButton, diff --git a/react/features/settings/components/web/ProfileTab.tsx b/react/features/settings/components/web/ProfileTab.tsx index 5817109409..5e8af2042c 100644 --- a/react/features/settings/components/web/ProfileTab.tsx +++ b/react/features/settings/components/web/ProfileTab.tsx @@ -11,7 +11,7 @@ import { AbstractDialogTab } from '../../../base/dialog'; // @ts-ignore import type { Props as AbstractDialogTabProps } from '../../../base/dialog'; import { translate } from '../../../base/i18n/functions'; -import { Button } from '../../../base/ui/components/web'; +import Button from '../../../base/ui/components/web/Button'; import Input from '../../../base/ui/components/web/Input'; // @ts-ignore import { openLogoutDialog } from '../../actions'; diff --git a/react/features/settings/components/web/audio/AudioSettingsPopup.js b/react/features/settings/components/web/audio/AudioSettingsPopup.js index d54b200a0f..fc9ac80f3c 100644 --- a/react/features/settings/components/web/audio/AudioSettingsPopup.js +++ b/react/features/settings/components/web/audio/AudioSettingsPopup.js @@ -4,11 +4,13 @@ import React from 'react'; import { areAudioLevelsEnabled } from '../../../../base/config/functions'; import { - getAudioInputDeviceData, - getAudioOutputDeviceData, setAudioInputDeviceAndUpdateSettings, setAudioOutputDevice as setAudioOutputDeviceAction -} from '../../../../base/devices'; +} from '../../../../base/devices/actions.web'; +import { + getAudioInputDeviceData, + getAudioOutputDeviceData +} from '../../../../base/devices/functions.web'; import Popover from '../../../../base/popover/components/Popover.web'; import { connect } from '../../../../base/redux'; import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants'; diff --git a/react/features/settings/components/web/video/VideoSettingsPopup.js b/react/features/settings/components/web/video/VideoSettingsPopup.js index a90748c821..a5eda7fe5e 100644 --- a/react/features/settings/components/web/video/VideoSettingsPopup.js +++ b/react/features/settings/components/web/video/VideoSettingsPopup.js @@ -3,9 +3,11 @@ import React from 'react'; import { - getVideoDeviceIds, setVideoInputDeviceAndUpdateSettings -} from '../../../../base/devices'; +} from '../../../../base/devices/actions.web'; +import { + getVideoDeviceIds +} from '../../../../base/devices/functions.web'; import Popover from '../../../../base/popover/components/Popover.web'; import { connect } from '../../../../base/redux'; import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants'; diff --git a/react/features/settings/functions.ts b/react/features/settings/functions.any.ts similarity index 82% rename from react/features/settings/functions.ts rename to react/features/settings/functions.any.ts index 50524b6950..01df467f95 100644 --- a/react/features/settings/functions.ts +++ b/react/features/settings/functions.any.ts @@ -5,7 +5,6 @@ import { isNameReadOnly } from '../base/config/functions'; import { SERVER_URL_CHANGE_ENABLED } from '../base/flags/constants'; import { getFeatureFlag } from '../base/flags/functions'; import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n/i18next'; -import { createLocalTrack } from '../base/lib-jitsi-meet/functions'; import { getLocalParticipant, isLocalParticipantModerator @@ -256,67 +255,6 @@ export function getSoundsTabProps(stateful: IStateful) { }; } -/** - * Returns a promise which resolves with a list of objects containing - * all the video jitsiTracks and appropriate errors for the given device ids. - * - * @param {string[]} ids - The list of the camera ids for which to create tracks. - * @param {number} [timeout] - A timeout for the createLocalTrack function call. - * - * @returns {Promise} - */ -export function createLocalVideoTracks(ids: string[], timeout?: number) { - return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout) - .then((jitsiTrack: any) => { - return { - jitsiTrack, - deviceId - }; - }) - .catch(() => { - return { - jitsiTrack: null, - deviceId, - error: 'deviceSelection.previewUnavailable' - }; - }))); -} - - -/** - * Returns a promise which resolves with a list of objects containing - * the audio track and the corresponding audio device information. - * - * @param {Object[]} devices - A list of microphone devices. - * @param {number} [timeout] - A timeout for the createLocalTrack function call. - * @returns {Promise<{ - * deviceId: string, - * hasError: boolean, - * jitsiTrack: Object, - * label: string - * }[]>} - */ -export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) { - return Promise.all( - devices.map(async ({ deviceId, label }) => { - let jitsiTrack = null; - let hasError = false; - - try { - jitsiTrack = await createLocalTrack('audio', deviceId, timeout); - } catch (err) { - hasError = true; - } - - return { - deviceId, - hasError, - jitsiTrack, - label - }; - })); -} - /** * Returns the visibility state of the audio settings. * diff --git a/react/features/settings/functions.native.ts b/react/features/settings/functions.native.ts new file mode 100644 index 0000000000..fb2a6bc39a --- /dev/null +++ b/react/features/settings/functions.native.ts @@ -0,0 +1 @@ +export * from './functions.any'; diff --git a/react/features/settings/functions.web.ts b/react/features/settings/functions.web.ts new file mode 100644 index 0000000000..d498bc8c7c --- /dev/null +++ b/react/features/settings/functions.web.ts @@ -0,0 +1,64 @@ +import { createLocalTrack } from '../base/lib-jitsi-meet/functions'; + +export * from './functions.any'; + +/** + * Returns a promise which resolves with a list of objects containing + * all the video jitsiTracks and appropriate errors for the given device ids. + * + * @param {string[]} ids - The list of the camera ids for which to create tracks. + * @param {number} [timeout] - A timeout for the createLocalTrack function call. + * + * @returns {Promise} + */ +export function createLocalVideoTracks(ids: string[], timeout?: number) { + return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout) + .then((jitsiTrack: any) => { + return { + jitsiTrack, + deviceId + }; + }) + .catch(() => { + return { + jitsiTrack: null, + deviceId, + error: 'deviceSelection.previewUnavailable' + }; + }))); +} + + +/** + * Returns a promise which resolves with a list of objects containing + * the audio track and the corresponding audio device information. + * + * @param {Object[]} devices - A list of microphone devices. + * @param {number} [timeout] - A timeout for the createLocalTrack function call. + * @returns {Promise<{ + * deviceId: string, + * hasError: boolean, + * jitsiTrack: Object, + * label: string + * }[]>} + */ +export function createLocalAudioTracks(devices: MediaDeviceInfo[], timeout?: number) { + return Promise.all( + devices.map(async ({ deviceId, label }) => { + let jitsiTrack = null; + let hasError = false; + + try { + jitsiTrack = await createLocalTrack('audio', deviceId, timeout); + } catch (err) { + hasError = true; + } + + return { + deviceId, + hasError, + jitsiTrack, + label + }; + })); +} diff --git a/react/features/video-layout/subscriber.ts b/react/features/video-layout/subscriber.ts index c52cc98138..1195d91ec2 100644 --- a/react/features/video-layout/subscriber.ts +++ b/react/features/video-layout/subscriber.ts @@ -3,7 +3,7 @@ import debounce from 'lodash/debounce'; import { getMultipleVideoSupportFeatureFlag } from '../base/config/functions'; import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import { equals } from '../base/redux/functions'; -import { ITrack } from '../base/tracks/reducer'; +import { ITrack } from '../base/tracks/types'; import { isFollowMeActive } from '../follow-me/functions'; import { setRemoteParticipantsWithScreenShare, virtualScreenshareParticipantsUpdated } from './actions.web'; diff --git a/tsconfig.native.json b/tsconfig.native.json index 5338986b05..59db91e8e4 100644 --- a/tsconfig.native.json +++ b/tsconfig.native.json @@ -24,6 +24,7 @@ "react/features/embed-meeting", "react/features/face-landmarks", "react/features/feedback", + "react/features/no-audio-signal", "react/features/noise-suppression", "react/features/screen-share", "react/features/stream-effects/noise-suppression",