Настройка микрофона и камеры перед вызовом с помощью библиотеки пользовательского интерфейса Службы коммуникации Azure
Важно!
Эта функция Службы коммуникации Azure сейчас доступна в предварительной версии.
Предварительная версия API и пакеты SDK предоставляются без соглашения об уровне обслуживания. Не рекомендуется использовать их для рабочих нагрузок. Некоторые функции могут не поддерживаться или иметь ограниченные возможности.
Дополнительные сведения см. в разделе Дополнительные условия использования предварительных версий Microsoft Azure.
Это руководство является продолжением серии из трех частей учебников по подготовке к звонкам и следует из предыдущих двух частей:
- Убедитесь, что пользователь находится в поддерживаемом браузере.
- Запрос доступа к камере и микрофону.
Скачать код
Полный код для этого руководства можно получить на сайте GitHub.
Предоставление пользователю возможности выбора камеры, микрофона и динамика
Из предыдущих двух частей учебника пользователь находится в поддерживаемом браузере, и он предоставил нам разрешение на доступ к камере и микрофону. Теперь мы можем убедиться, что пользователь может выбрать правильный микрофон, камеру и динамик, которые он хочет использовать для вызова. Мы представляем пользователю широкий интерфейс для выбора камеры, микрофона и динамика. Окончательный пользовательский интерфейс настройки устройства выглядит следующим образом:
Создание экрана конфигурации
Сначала мы создадим файл с именем DeviceSetup.tsx
и добавим код установки с обратным вызовом, который возвращает выбранные пользователями устройства обратно в приложение:
src/DeviceSetup.tsx
import { PrimaryButton, Stack } from '@fluentui/react';
export const DeviceSetup = (props: {
/** Callback to let the parent component know what the chosen user device settings were */
onDeviceSetupComplete: (userChosenDeviceState: { cameraOn: boolean; microphoneOn: boolean }) => void
}): JSX.Element => {
return (
<Stack tokens={{ childrenGap: '1rem' }} verticalAlign="center" verticalFill>
<PrimaryButton text="Continue" onClick={() => props.onDeviceSetupComplete({ cameraOn: false, microphoneOn: false })} />
</Stack>
);
}
Затем мы можем добавить этот DeviceSetup в наше приложение.
- После завершения preCallChecksComponent он перенаправит пользователя в
deviceSetup
состояние . - Когда пользователь находится в
deviceSetup
состоянии , компонент отрисовываетсяDeviceSetup
. - После завершения настройки устройства пользователь перенаправится в
finished
состояние . В рабочем приложении это обычно происходит при перемещении пользователя на экран вызова.
Сначала импортируйте созданный компонент DeviceSetup:
src/App.tsx
import { DeviceSetup } from './DeviceSetup';
Затем обновите приложение, чтобы получить новое тестовое состояние deviceSetup
:
type TestingState = 'runningEnvironmentChecks' | 'runningDeviceAccessChecks' | 'deviceSetup' | 'finished';
И, наконец, обновите наш App
компонент, чтобы перевести приложение на настройку устройства после завершения проверки доступа к устройству:
/**
* Entry point of a React app.
*
* This shows a PreparingYourSession component while the CallReadinessChecks are running.
* Once the CallReadinessChecks are finished, the TestComplete component is shown.
*/
const App = (): JSX.Element => {
const [testState, setTestState] = useState<TestingState>('runningEnvironmentChecks');
return (
<FluentThemeProvider>
<CallClientProvider callClient={callClient}>
{/* Show a Preparing your session screen while running the environment checks */}
{testState === 'runningEnvironmentChecks' && (
<>
<PreparingYourSession />
<EnvironmentChecksComponent onTestsSuccessful={() => setTestState('runningDeviceAccessChecks')} />
</>
)}
{/* Show a Preparing your session screen while running the device access checks */}
{testState === 'runningDeviceAccessChecks' && (
<>
<PreparingYourSession />
<DeviceAccessChecksComponent onTestsSuccessful={() => setTestState('deviceSetup')} />
</>
)}
{/* After the initial checks are complete, take the user to a device setup page call readiness checks are finished */}
{testState === 'deviceSetup' && (
<DeviceSetup
onDeviceSetupComplete={(userChosenDeviceState) => {
setTestState('finished');
}}
/>
)}
{/* After the device setup is complete, take the user to the call. For this sample we show a test complete page. */}
{testState === 'finished' && <TestComplete />}
</CallClientProvider>
</FluentThemeProvider>
);
}
Получение и обновление списков микрофонов, камер и динамиков из клиента с отслеживанием состояния
Чтобы представить пользователю список доступных для выбора камер, микрофонов и динамиков, можно использовать клиент вызова с отслеживанием состояния.
Здесь мы создадим серию React крючков. Эти обработчики React используют клиент вызова для запроса доступных устройств.
Перехватчики обеспечивают повторную отрисовку приложения при каждом изменении списка, например, если к компьютеру пользователя подключена новая камера.
Для этих перехватчиков мы создадим файл с именем deviceSetupHooks.ts
и создадим три обработчика: useMicrophones
и useSpeakers
useCameras
.
Каждый из этих перехватчиков использует useCallClientStateChange
для обновления своих списков каждый раз, когда пользователь подключает или отключает устройство:
src/deviceSetupHooks.ts
import { AudioDeviceInfo, VideoDeviceInfo } from "@azure/communication-calling";
import { CallClientState, StatefulDeviceManager, useCallClient, VideoStreamRendererViewState } from "@azure/communication-react";
import { useCallback, useEffect, useRef, useState } from "react";
/** A helper hook to get and update microphone device information */
export const useMicrophones = (): {
microphones: AudioDeviceInfo[],
selectedMicrophone: AudioDeviceInfo | undefined,
setSelectedMicrophone: (microphone: AudioDeviceInfo) => Promise<void>
} => {
const callClient = useCallClient();
useEffect(() => {
callClient.getDeviceManager().then(deviceManager => deviceManager.getMicrophones())
}, [callClient]);
const setSelectedMicrophone = async (microphone: AudioDeviceInfo) =>
(await callClient.getDeviceManager()).selectMicrophone(microphone);
const state = useCallClientStateChange();
return {
microphones: state.deviceManager.microphones,
selectedMicrophone: state.deviceManager.selectedMicrophone,
setSelectedMicrophone
};
}
/** A helper hook to get and update speaker device information */
export const useSpeakers = (): {
speakers: AudioDeviceInfo[],
selectedSpeaker: AudioDeviceInfo | undefined,
setSelectedSpeaker: (speaker: AudioDeviceInfo) => Promise<void>
} => {
const callClient = useCallClient();
useEffect(() => {
callClient.getDeviceManager().then(deviceManager => deviceManager.getSpeakers())
}, [callClient]);
const setSelectedSpeaker = async (speaker: AudioDeviceInfo) =>
(await callClient.getDeviceManager()).selectSpeaker(speaker);
const state = useCallClientStateChange();
return {
speakers: state.deviceManager.speakers,
selectedSpeaker: state.deviceManager.selectedSpeaker,
setSelectedSpeaker
};
}
/** A helper hook to get and update camera device information */
export const useCameras = (): {
cameras: VideoDeviceInfo[],
selectedCamera: VideoDeviceInfo | undefined,
setSelectedCamera: (camera: VideoDeviceInfo) => Promise<void>
} => {
const callClient = useCallClient();
useEffect(() => {
callClient.getDeviceManager().then(deviceManager => deviceManager.getCameras())
}, [callClient]);
const setSelectedCamera = async (camera: VideoDeviceInfo) =>
(await callClient.getDeviceManager() as StatefulDeviceManager).selectCamera(camera);
const state = useCallClientStateChange();
return {
cameras: state.deviceManager.cameras,
selectedCamera: state.deviceManager.selectedCamera,
setSelectedCamera
};
}
/** A helper hook to act when changes to the stateful client occur */
const useCallClientStateChange = (): CallClientState => {
const callClient = useCallClient();
const [state, setState] = useState<CallClientState>(callClient.getState());
useEffect(() => {
const updateState = (newState: CallClientState) => {
setState(newState);
}
callClient.onStateChange(updateState);
return () => {
callClient.offStateChange(updateState);
};
}, [callClient]);
return state;
}
Создание раскрывающихся списков для выбора устройств
Чтобы разрешить пользователю выбирать камеру, микрофон и динамик, мы используем Dropdown
компонент из fluent UI React.
Мы создаем новые компоненты, которые используют обработчики, созданные в deviceSetupHooks.tsx
, чтобы заполнить раскрывающийся список и обновить выбранное устройство, когда пользователь выбирает другое устройство из раскрывающегося списка.
Чтобы разместить эти новые компоненты, мы создадим файл с именем DeviceSelectionComponents.tsx
, который экспортирует три новых компонента: CameraSelectionDropdown
и MicrophoneSelectionDropdown
SpeakerSelectionDropdown
.
src/DeviceSelectionComponents.tsx
import { Dropdown } from '@fluentui/react';
import { useCameras, useMicrophones, useSpeakers } from './deviceSetupHooks';
/** Dropdown that allows the user to choose their desired camera */
export const CameraSelectionDropdown = (): JSX.Element => {
const { cameras, selectedCamera, setSelectedCamera } = useCameras();
return (
<DeviceSelectionDropdown
placeholder={cameras.length === 0 ? 'No cameras found' : 'Select a camera'}
label={'Camera'}
devices={cameras}
selectedDevice={selectedCamera}
onSelectionChange={(selectedDeviceId) => {
const newlySelectedCamera = cameras.find((camera) => camera.id === selectedDeviceId);
if (newlySelectedCamera) {
setSelectedCamera(newlySelectedCamera);
}
}}
/>
);
};
/** Dropdown that allows the user to choose their desired microphone */
export const MicrophoneSelectionDropdown = (): JSX.Element => {
const { microphones, selectedMicrophone, setSelectedMicrophone } = useMicrophones();
return (
<DeviceSelectionDropdown
placeholder={microphones.length === 0 ? 'No microphones found' : 'Select a microphone'}
label={'Microphone'}
devices={microphones}
selectedDevice={selectedMicrophone}
onSelectionChange={(selectedDeviceId) => {
const newlySelectedMicrophone = microphones.find((microphone) => microphone.id === selectedDeviceId);
if (newlySelectedMicrophone) {
setSelectedMicrophone(newlySelectedMicrophone);
}
}}
/>
);
};
/** Dropdown that allows the user to choose their desired speaker */
export const SpeakerSelectionDropdown = (): JSX.Element => {
const { speakers, selectedSpeaker, setSelectedSpeaker } = useSpeakers();
return (
<DeviceSelectionDropdown
placeholder={speakers.length === 0 ? 'No speakers found' : 'Select a speaker'}
label={'Speaker'}
devices={speakers}
selectedDevice={selectedSpeaker}
onSelectionChange={(selectedDeviceId) => {
const newlySelectedSpeaker = speakers.find((speaker) => speaker.id === selectedDeviceId);
if (newlySelectedSpeaker) {
setSelectedSpeaker(newlySelectedSpeaker);
}
}}
/>
);
};
const DeviceSelectionDropdown = (props: {
placeholder: string,
label: string,
devices: { id: string, name: string }[],
selectedDevice: { id: string, name: string } | undefined,
onSelectionChange: (deviceId: string | undefined) => void
}): JSX.Element => {
return (
<Dropdown
placeholder={props.placeholder}
label={props.label}
options={props.devices.map((device) => ({ key: device.id, text: device.name }))}
selectedKey={props.selectedDevice?.id}
onChange={(_, option) => props.onSelectionChange?.(option?.key as string | undefined)}
/>
);
};
Добавление раскрывающихся списков в раздел "Настройка устройства"
Затем раскрывающийся список камеры, микрофона и динамика можно добавить в компонент "Настройка устройства".
Сначала импортируйте новые раскрывающийся список:
src/DeviceSetup.tsx
import { CameraSelectionDropdown, MicrophoneSelectionDropdown, SpeakerSelectionDropdown } from './DeviceSelectionComponents';
Затем создайте компонент с именем DeviceSetup
, который содержит эти раскрывающиеся списки. Этот компонент содержит локальный предварительный просмотр видео, который мы создадим позже.
export const DeviceSetup = (props: {
/** Callback to let the parent component know what the chosen user device settings were */
onDeviceSetupComplete: (userChosenDeviceState: { cameraOn: boolean; microphoneOn: boolean }) => void
}): JSX.Element => {
return (
<Stack verticalFill verticalAlign="center" horizontalAlign="center" tokens={{ childrenGap: '1rem' }}>
<Stack horizontal tokens={{ childrenGap: '2rem' }}>
<Stack tokens={{ childrenGap: '1rem' }} verticalAlign="center" verticalFill>
<CameraSelectionDropdown />
<MicrophoneSelectionDropdown />
<SpeakerSelectionDropdown />
<Stack.Item styles={{ root: { paddingTop: '0.5rem' }}}>
<PrimaryButton text="Continue" onClick={() => props.onDeviceSetupComplete({ cameraOn: false, microphoneOn: false })} />
</Stack.Item>
</Stack>
</Stack>
</Stack>
);
};
Создание локального предварительного просмотра видео
Помимо раскрывающихся списков, мы создадим локальный предварительный просмотр видео, чтобы позволить пользователю видеть, что захватывает его камера. Он содержит небольшую панель элементов управления вызовами с кнопками камеры и микрофона для включения и выключения камеры, а также включения звука и включения микрофона.
Сначала мы добавим новый крючок к с deviceSetupHooks.ts
именем useLocalPreview
. Этот перехватчик предоставляет нашему компоненту react localPreview для отрисовки и функции для запуска и остановки локального предварительного просмотра:
src/deviceSetupHooks.ts
/** A helper hook to providing functionality to create a local video preview */
export const useLocalPreview = (): {
localPreview: VideoStreamRendererViewState | undefined,
startLocalPreview: () => Promise<void>,
stopLocalPreview: () => void
} => {
const callClient = useCallClient();
const state = useCallClientStateChange();
const localPreview = state.deviceManager.unparentedViews[0];
const startLocalPreview = useCallback(async () => {
const selectedCamera = state.deviceManager.selectedCamera;
if (!selectedCamera) {
console.warn('no camera selected to start preview with');
return;
}
callClient.createView(
undefined,
undefined,
{
source: selectedCamera,
mediaStreamType: 'Video'
},
{
scalingMode: 'Crop'
}
);
}, [callClient, state.deviceManager.selectedCamera]);
const stopLocalPreview = useCallback(() => {
if (!localPreview) {
console.warn('no local preview ti dispose');
return;
}
callClient.disposeView(undefined, undefined, localPreview)
}, [callClient, localPreview]);
const selectedCameraRef = useRef(state.deviceManager.selectedCamera);
useEffect(() => {
if (selectedCameraRef.current !== state.deviceManager.selectedCamera) {
stopLocalPreview();
startLocalPreview();
selectedCameraRef.current = state.deviceManager.selectedCamera;
}
}, [startLocalPreview, state.deviceManager.selectedCamera, stopLocalPreview]);
return {
localPreview: localPreview?.view,
startLocalPreview,
stopLocalPreview
}
}
Затем мы создадим новый компонент с именем LocalPreview.tsx
, который использует этот обработчик для отображения локального предварительного просмотра видео для пользователя:
src/LocalPreview.tsx
import { StreamMedia, VideoTile, ControlBar, CameraButton, MicrophoneButton, useTheme } from '@azure/communication-react';
import { Stack, mergeStyles, Text, ITheme } from '@fluentui/react';
import { VideoOff20Filled } from '@fluentui/react-icons';
import { useEffect } from 'react';
import { useCameras, useLocalPreview } from './deviceSetupHooks';
/** LocalPreview component has a camera and microphone toggle buttons, along with a video preview of the local camera. */
export const LocalPreview = (props: {
cameraOn: boolean,
microphoneOn: boolean,
cameraToggled: (isCameraOn: boolean) => void,
microphoneToggled: (isMicrophoneOn: boolean) => void
}): JSX.Element => {
const { cameraOn, microphoneOn, cameraToggled, microphoneToggled } = props;
const { localPreview, startLocalPreview, stopLocalPreview } = useLocalPreview();
const canTurnCameraOn = useCameras().cameras.length > 0;
// Start and stop the local video preview based on if the user has turned the camera on or off and if the camera is available.
useEffect(() => {
if (!localPreview && cameraOn && canTurnCameraOn) {
startLocalPreview();
} else if (!cameraOn) {
stopLocalPreview();
}
}, [canTurnCameraOn, cameraOn, localPreview, startLocalPreview, stopLocalPreview]);
const theme = useTheme();
const shouldShowLocalVideo = canTurnCameraOn && cameraOn && localPreview;
return (
<Stack verticalFill verticalAlign="center">
<Stack className={localPreviewContainerMergedStyles(theme)}>
<VideoTile
renderElement={shouldShowLocalVideo ? <StreamMedia videoStreamElement={localPreview.target} /> : undefined}
onRenderPlaceholder={() => <CameraOffPlaceholder />}
>
<ControlBar layout="floatingBottom">
<CameraButton
checked={cameraOn}
onClick={() => {
cameraToggled(!cameraOn)
}}
/>
<MicrophoneButton
checked={microphoneOn}
onClick={() => {
microphoneToggled(!microphoneOn)
}}
/>
</ControlBar>
</VideoTile>
</Stack>
</Stack>
);
};
/** Placeholder shown in the local preview window when the camera is off */
const CameraOffPlaceholder = (): JSX.Element => {
const theme = useTheme();
return (
<Stack style={{ width: '100%', height: '100%' }} verticalAlign="center">
<Stack.Item align="center">
<VideoOff20Filled primaryFill="currentColor" />
</Stack.Item>
<Stack.Item align="center">
<Text variant='small' styles={{ root: { color: theme.palette.neutralTertiary }}}>Your camera is turned off</Text>
</Stack.Item>
</Stack>
);
};
/** Default styles for the local preview container */
const localPreviewContainerMergedStyles = (theme: ITheme): string =>
mergeStyles({
minWidth: '25rem',
maxHeight: '18.75rem',
minHeight: '16.875rem',
margin: '0 auto',
background: theme.palette.neutralLighter,
color: theme.palette.neutralTertiary
});
Добавление локального предварительного просмотра в настройку устройства
Затем компонент локальной предварительной версии можно добавить в программу настройки устройства:
src/DeviceSetup.tsx
import { LocalPreview } from './LocalPreview';
import { useState } from 'react';
export const DeviceSetup = (props: {
/** Callback to let the parent component know what the chosen user device settings were */
onDeviceSetupComplete: (userChosenDeviceState: { cameraOn: boolean; microphoneOn: boolean }) => void
}): JSX.Element => {
const [microphoneOn, setMicrophoneOn] = useState(false);
const [cameraOn, setCameraOn] = useState(false);
return (
<Stack verticalFill verticalAlign="center" horizontalAlign="center" tokens={{ childrenGap: '1rem' }}>
<Stack horizontal tokens={{ childrenGap: '2rem' }}>
<Stack.Item>
<LocalPreview
cameraOn={cameraOn}
microphoneOn={microphoneOn}
cameraToggled={setCameraOn}
microphoneToggled={setMicrophoneOn}
/>
</Stack.Item>
<Stack tokens={{ childrenGap: '1rem' }} verticalAlign="center" verticalFill>
<CameraSelectionDropdown />
<MicrophoneSelectionDropdown />
<SpeakerSelectionDropdown />
<Stack.Item styles={{ root: { paddingTop: '0.5rem' }}}>
<PrimaryButton text="Continue" onClick={() => props.onDeviceSetupComplete({ cameraOn, microphoneOn })} />
</Stack.Item>
</Stack>
</Stack>
</Stack>
);
};
Запуск интерфейса
Теперь вы создали экран конфигурации устройства, вы можете запустить приложение и просмотреть интерфейс: