[FIX] Undefined MediaDevices error on HTTP (#26396)

pull/26507/head
Martin Schoeler 3 years ago committed by murtaza98
parent 935c2403bd
commit 4577d28156
  1. 71
      apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx
  2. 15
      apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx
  3. 1
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  4. 1
      apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  5. 30
      packages/ui-contexts/src/DeviceContext.ts
  6. 21
      packages/ui-contexts/src/hooks/useAvailableDevices.ts
  7. 15
      packages/ui-contexts/src/hooks/useDeviceConstraints.ts
  8. 5
      packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts
  9. 21
      packages/ui-contexts/src/hooks/useSelectedDevices.ts
  10. 12
      packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts
  11. 13
      packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts
  12. 3
      packages/ui-contexts/src/index.ts

@ -1,5 +1,6 @@
import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '@rocket.chat/ui-contexts';
import React, { ReactElement, ReactNode, useEffect, useState } from 'react';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from '@rocket.chat/ui-contexts';
import React, { ReactElement, ReactNode, useEffect, useState, useMemo } from 'react';
import { isSetSinkIdAvailable } from './lib/isSetSinkIdAvailable';
@ -8,6 +9,7 @@ type DeviceProviderProps = {
};
export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement => {
const [enabled] = useState(typeof isSecureContext && isSecureContext);
const [availableAudioOutputDevices, setAvailableAudioOutputDevices] = useState<Device[]>([]);
const [availableAudioInputDevices, setAvailableAudioInputDevices] = useState<Device[]>([]);
const [selectedAudioOutputDevice, setSelectedAudioOutputDevice] = useState<Device>({
@ -21,23 +23,32 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement
type: 'audio',
});
const setAudioOutputDevice = ({
outputDevice,
HTMLAudioElement,
}: {
outputDevice: Device;
HTMLAudioElement: IExperimentalHTMLAudioElement;
}): void => {
if (!isSetSinkIdAvailable()) {
throw new Error('setSinkId is not available in this browser');
const setAudioInputDevice = (device: Device): void => {
if (!isSecureContext) {
throw new Error('Device Changes are not available on insecure contexts');
}
setSelectedAudioOutputDevice(outputDevice);
HTMLAudioElement.setSinkId(outputDevice.id);
setSelectedAudioInputDevice(device);
};
const setAudioOutputDevice = useMutableCallback(
({ outputDevice, HTMLAudioElement }: { outputDevice: Device; HTMLAudioElement: IExperimentalHTMLAudioElement }): void => {
if (!isSetSinkIdAvailable()) {
throw new Error('setSinkId is not available in this browser');
}
if (!enabled) {
throw new Error('Device Changes are not available on insecure contexts');
}
setSelectedAudioOutputDevice(outputDevice);
HTMLAudioElement.setSinkId(outputDevice.id);
},
);
useEffect(() => {
if (!enabled) {
return;
}
const setMediaDevices = (): void => {
navigator.mediaDevices.enumerateDevices().then((devices) => {
navigator.mediaDevices?.enumerateDevices().then((devices) => {
const audioInput: Device[] = [];
const audioOutput: Device[] = [];
devices.forEach((device) => {
@ -57,21 +68,37 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement
});
};
navigator.mediaDevices.addEventListener('devicechange', setMediaDevices);
navigator.mediaDevices?.addEventListener('devicechange', setMediaDevices);
setMediaDevices();
return (): void => {
navigator.mediaDevices.removeEventListener('devicechange', setMediaDevices);
navigator.mediaDevices?.removeEventListener('devicechange', setMediaDevices);
};
}, []);
}, [enabled]);
const contextValue = {
availableAudioOutputDevices,
const contextValue = useMemo((): DeviceContextValue => {
if (!enabled) {
return {
enabled,
};
}
return {
enabled,
availableAudioOutputDevices,
availableAudioInputDevices,
selectedAudioOutputDevice,
selectedAudioInputDevice,
setAudioOutputDevice,
setAudioInputDevice,
};
}, [
availableAudioInputDevices,
selectedAudioOutputDevice,
availableAudioOutputDevices,
enabled,
selectedAudioInputDevice,
selectedAudioOutputDevice,
setAudioOutputDevice,
setAudioInputDevice: setSelectedAudioInputDevice,
};
]);
return <DeviceContext.Provider value={contextValue}>{children}</DeviceContext.Provider>;
};

@ -1,5 +1,12 @@
import { Modal, Field, Select, ButtonGroup, Button, SelectOption, Box } from '@rocket.chat/fuselage';
import { useTranslation, useAvailableDevices, useToastMessageDispatch, useSetModal, useSelectedDevices } from '@rocket.chat/ui-contexts';
import {
useTranslation,
useAvailableDevices,
useToastMessageDispatch,
useSetModal,
useSelectedDevices,
useIsDeviceManagementEnabled,
} from '@rocket.chat/ui-contexts';
import React, { ReactElement, useState } from 'react';
import { useForm, Controller, SubmitHandler } from 'react-hook-form';
@ -14,6 +21,7 @@ type FieldValues = {
const DeviceSettingsModal = (): ReactElement => {
const setModal = useSetModal();
const onCancel = (): void => setModal();
const isDeviceManagementEnabled = useIsDeviceManagementEnabled();
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const selectedAudioDevices = useSelectedDevices();
@ -60,6 +68,11 @@ const DeviceSettingsModal = (): ReactElement => {
</Box>
</Box>
)}
{!isDeviceManagementEnabled && (
<Box color='danger-600' display='flex' flexDirection='column'>
{t('Device_Changes_Not_Available_Insecure_Context')}
</Box>
)}
<Field>
<Field.Label>{t('Microphone')}</Field.Label>
<Field.Row w='full' display='flex' flexDirection='column' alignItems='stretch'>

@ -1486,6 +1486,7 @@
"Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled",
"Details": "Details",
"Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.",
"Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)",
"Device_Management": "Device management",
"Device_ID": "Device ID",
"Device_Info": "Device Info",

@ -1438,6 +1438,7 @@
"Desktop_Notifications_Not_Enabled": "Notificações da área de trabalho estão desabilitadas",
"Details": "Detalhes",
"Device_Changes_Not_Available": "Mudanças de dispositivo não estão disponíveis neste navegador, para disponíbilidade garantida, use o aplicativo desktop oficial do Rocket.Chat.",
"Device_Changes_Not_Available_Insecure_Context": "Mudanças de dispositivo somente estão disponíveis em contextos seguros. (https://)",
"Different_Style_For_User_Mentions": "Estilo diferente para as menções do usuário",
"Direct_Message": "Mensagem direta",
"Direct_message_creation_description": "Você está prestes a criar uma conversa com vários usuários. Adicione os usuários com quem gostaria de conversar, todos no mesmo local, utilizando mensagens diretas.",

@ -10,7 +10,8 @@ export interface IExperimentalHTMLAudioElement extends HTMLAudioElement {
setSinkId: (sinkId: string) => void;
}
type DeviceContextValue = {
type EnabledDeviceContextValue = {
enabled: true;
availableAudioOutputDevices: Device[];
availableAudioInputDevices: Device[];
// availableVideoInputDevices: Device[]
@ -22,22 +23,15 @@ type DeviceContextValue = {
// setVideoInputDevice: (device: Device) => void;
};
type DisabledDeviceContextValue = {
enabled: false;
};
export type DeviceContextValue = EnabledDeviceContextValue | DisabledDeviceContextValue;
export const isDeviceContextEnabled = (context: DeviceContextValue): context is EnabledDeviceContextValue =>
(context as EnabledDeviceContextValue).enabled;
export const DeviceContext = createContext<DeviceContextValue>({
availableAudioOutputDevices: [],
availableAudioInputDevices: [],
// availableVideoInputDevices: [],
selectedAudioOutputDevice: {
id: 'default',
label: '',
type: 'audio',
},
selectedAudioInputDevice: {
id: 'default',
label: '',
type: 'audio',
},
// selectedVideoInputDevice: undefined,
setAudioOutputDevice: () => undefined,
setAudioInputDevice: () => undefined,
// setVideoInputDevice: () => undefined,
enabled: false,
});

@ -1,13 +1,24 @@
import { useContext } from 'react';
import { DeviceContext, Device } from '../DeviceContext';
import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext';
type AvailableDevices = {
audioInput?: Device[];
audioOutput?: Device[];
};
export const useAvailableDevices = (): AvailableDevices => ({
audioInput: useContext(DeviceContext).availableAudioInputDevices,
audioOutput: useContext(DeviceContext).availableAudioOutputDevices,
});
export const useAvailableDevices = (): AvailableDevices | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
return {
audioInput: context.availableAudioInputDevices,
audioOutput: context.availableAudioOutputDevices,
};
};

@ -1,8 +1,17 @@
import { useContext } from 'react';
import { DeviceContext } from '../DeviceContext';
import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext';
export const useDeviceConstraints = (): MediaStreamConstraints => {
const selectedAudioInputDeviceId = useContext(DeviceContext).selectedAudioInputDevice?.id;
export const useDeviceConstraints = (): MediaStreamConstraints | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
const selectedAudioInputDeviceId = context.selectedAudioInputDevice?.id;
return { audio: selectedAudioInputDeviceId === 'default' ? true : { deviceId: { exact: selectedAudioInputDeviceId } } };
};

@ -0,0 +1,5 @@
import { useContext } from 'react';
import { DeviceContext } from '../DeviceContext';
export const useIsDeviceManagementEnabled = (): boolean => useContext(DeviceContext).enabled;

@ -1,13 +1,24 @@
import { useContext } from 'react';
import { DeviceContext, Device } from '../DeviceContext';
import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext';
type SelectedDevices = {
audioInput?: Device;
audioOutput?: Device;
};
export const useSelectedDevices = (): SelectedDevices => ({
audioInput: useContext(DeviceContext).selectedAudioInputDevice,
audioOutput: useContext(DeviceContext).selectedAudioOutputDevice,
});
export const useSelectedDevices = (): SelectedDevices | null => {
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return null;
}
return {
audioInput: context.selectedAudioInputDevice,
audioOutput: context.selectedAudioOutputDevice,
};
};

@ -1,9 +1,17 @@
import { useContext } from 'react';
import { DeviceContext, Device } from '../DeviceContext';
import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext';
type setInputMediaDevice = (inputDevice: Device) => void;
export const useSetInputMediaDevice = (): setInputMediaDevice => {
return useContext(DeviceContext).setAudioInputDevice;
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return () => undefined;
}
return context.setAudioInputDevice;
};

@ -1,6 +1,6 @@
import { useContext } from 'react';
import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '../DeviceContext';
import { DeviceContext, Device, IExperimentalHTMLAudioElement, isDeviceContextEnabled } from '../DeviceContext';
// This allows different places to set the output device by providing a HTMLAudioElement
@ -13,5 +13,14 @@ type setOutputMediaDevice = ({
}) => void;
export const useSetOutputMediaDevice = (): setOutputMediaDevice => {
return useContext(DeviceContext).setAudioOutputDevice;
const context = useContext(DeviceContext);
if (!isDeviceContextEnabled(context)) {
console.warn(
'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts',
);
return () => undefined;
}
return context.setAudioOutputDevice;
};

@ -13,7 +13,7 @@ export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessages
export { TooltipContext, TooltipContextValue } from './TooltipContext';
export { TranslationContext, TranslationContextValue } from './TranslationContext';
export { UserContext, UserContextValue } from './UserContext';
export { DeviceContext, Device, IExperimentalHTMLAudioElement } from './DeviceContext';
export { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from './DeviceContext';
export { useAbsoluteUrl } from './hooks/useAbsoluteUrl';
export { useAllPermissions } from './hooks/useAllPermissions';
@ -76,6 +76,7 @@ export { useUserSubscriptions } from './hooks/useUserSubscriptions';
export { useSelectedDevices } from './hooks/useSelectedDevices';
export { useDeviceConstraints } from './hooks/useDeviceConstraints';
export { useAvailableDevices } from './hooks/useAvailableDevices';
export { useIsDeviceManagementEnabled } from './hooks/useIsDeviceManagementEnabled';
export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice';
export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice';

Loading…
Cancel
Save