Skip to content

Commit

Permalink
[FIX] Undefined MediaDevices error on HTTP (#26396)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinSchoeler authored and carlosrodrigues94 committed Aug 4, 2022
1 parent 0d7a720 commit fec3a1a
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 59 deletions.
71 changes: 49 additions & 22 deletions apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx
@@ -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';

Expand All @@ -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>({
Expand All @@ -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) => {
Expand All @@ -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>;
};
15 changes: 14 additions & 1 deletion apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx
@@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -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'>
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Expand Up @@ -1496,6 +1496,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",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Expand Up @@ -1439,6 +1439,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.",
Expand Down
30 changes: 12 additions & 18 deletions packages/ui-contexts/src/DeviceContext.ts
Expand Up @@ -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[]
Expand All @@ -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,
});
21 changes: 16 additions & 5 deletions packages/ui-contexts/src/hooks/useAvailableDevices.ts
@@ -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,
};
};
15 changes: 12 additions & 3 deletions packages/ui-contexts/src/hooks/useDeviceConstraints.ts
@@ -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;
21 changes: 16 additions & 5 deletions packages/ui-contexts/src/hooks/useSelectedDevices.ts
@@ -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,
};
};
12 changes: 10 additions & 2 deletions packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts
@@ -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;
};
13 changes: 11 additions & 2 deletions packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts
@@ -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

Expand All @@ -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;
};
3 changes: 2 additions & 1 deletion packages/ui-contexts/src/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down

0 comments on commit fec3a1a

Please sign in to comment.