Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Undefined MediaDevices error on HTTP #26396

Merged
merged 6 commits into from Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -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",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Expand Up @@ -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.",
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