Configuración de micrófono y cámara antes de una llamada mediante la biblioteca de interfaz de usuario de Azure Communication Services
Importante
Esta característica de Azure Communication Services se encuentra actualmente en versión preliminar.
Las API y los SDK en versión preliminar se proporcionan sin contrato de nivel de servicio. Se recomienda no usarlos para las cargas de trabajo de producción. Es posible que algunas características no sean compatibles o que sus funcionalidades estén limitadas.
Para obtener más información, consulte Términos de uso complementarios para las Versiones preliminares de Microsoft Azure.
Este tutorial es una continuación de una serie de tres partes de tutoriales de preparación de llamadas y sigue a partir de las dos partes anteriores:
- Asegúrese de que el usuario esté en un explorador compatible.
- Solicitud de acceso a la cámara y al micrófono.
Descarga de código
Acceda al código completo de este tutorial sobre GitHub.
Permitir al usuario elegir su cámara, micrófono y altavoz
En las dos partes anteriores del tutorial, el usuario está en un explorador compatible y nos ha concedido permiso para acceder a su cámara y micrófono. Ahora podemos asegurarnos de que el usuario pueda elegir el micrófono, la cámara y el altavoz que quiera para su llamada. Presentamos al usuario una interfaz enriquecida para elegir su cámara, micrófono y altavoz. La interfaz de usuario de configuración del dispositivo final tiene el siguiente aspecto:
Creación de una pantalla de configuración
En primer lugar, creamos un nuevo archivo denominado DeviceSetup.tsx
y agregamos código de configuración, con una devolución de llamada que devuelve los dispositivos elegidos por los usuarios a la aplicación:
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>
);
}
A continuación, podemos agregar este DeviceSetup a nuestra aplicación.
- Cuando se completa PreCallChecksComponent, reenvía al usuario al estado
deviceSetup
. - Cuando el usuario está en el estado
deviceSetup
, representamos el componenteDeviceSetup
. - Una vez completada la configuración del dispositivo, se dirige al usuario al estado
finished
. En una aplicación de producción, esto sucede al llevar al usuario a una pantalla de llamada.
En primer lugar, importe el componente DeviceSetup que hemos creado:
src/App.tsx
import { DeviceSetup } from './DeviceSetup';
A continuación, actualice la aplicación para que tenga un nuevo estado deviceSetup
de prueba:
type TestingState = 'runningEnvironmentChecks' | 'runningDeviceAccessChecks' | 'deviceSetup' | 'finished';
Y, por último, actualice nuestro componente App
para realizar la transición de la aplicación a la configuración del dispositivo una vez completadas las comprobaciones de acceso del dispositivo:
/**
* 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>
);
}
Recuperar y actualizar listas de micrófonos, cámaras y altavoces del cliente con estado
Para presentar una lista de cámaras seleccionables, micrófonos y altavoces al usuario, podemos usar el cliente de llamada con estado.
Aquí creamos una serie de enlaces de React. Estos enlaces de React usan el cliente de llamada para consultar los dispositivos disponibles.
Los enlaces garantizan que la aplicación vuelva a representarse cada vez que cambie la lista, por ejemplo, si se conecta una nueva cámara a la máquina del usuario.
Para estos enlaces, creamos un archivo denominado deviceSetupHooks.ts
y tres enlaces: useMicrophones
, useSpeakers
y useCameras
.
Cada uno de estos enlaces usa useCallClientStateChange
para actualizar sus listas en cualquier momento que el usuario conecte o desconecte un dispositivo:
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;
}
Creación de listas desplegables para elegir dispositivos
Para permitir que el usuario elija su cámara, micrófono y altavoz, usamos el componente Dropdown
de Fluent UI React.
Creamos nuevos componentes que usan los enlaces creados en deviceSetupHooks.tsx
para rellenar la lista desplegable y actualizar el dispositivo elegido cuando el usuario seleccione un dispositivo diferente en la lista desplegable.
Para hospedar estos nuevos componentes, se crea un archivo denominado DeviceSelectionComponents.tsx
que exporta tres nuevos componentes: CameraSelectionDropdown
, MicrophoneSelectionDropdown
y 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)}
/>
);
};
Agregar listas desplegables al programa de instalación del dispositivo
Las listas desplegables de cámara, micrófono y altavoz se pueden agregar al componente Configuración del dispositivo.
En primer lugar, importe las nuevas listas desplegables:
src/DeviceSetup.tsx
import { CameraSelectionDropdown, MicrophoneSelectionDropdown, SpeakerSelectionDropdown } from './DeviceSelectionComponents';
A continuación, cree un componente denominado DeviceSetup
que contenga estas listas desplegables. Este componente contiene la versión preliminar del vídeo local que creamos más adelante.
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>
);
};
Creación de una vista previa de vídeo local
Junto a las listas desplegables, creamos una vista previa de vídeo local para permitir al usuario ver qué está capturando su cámara. Contiene una pequeña barra de controles de llamada con botones de cámara y micrófono para activar/desactivar la cámara y silenciar/desactivar el micrófono.
En primer lugar, agregamos un nuevo enlace a nuestro deviceSetupHooks.ts
llamado useLocalPreview
. Este enlace proporciona nuestro componente React con una vista previa local para representar y funciones para iniciar y detener la versión preliminar local:
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
}
}
A continuación, creamos un nuevo componente denominado LocalPreview.tsx
que usa ese enlace para mostrar la vista previa del vídeo local al usuario:
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
});
Agregar la versión preliminar local a la configuración del dispositivo
Después, el componente de versión preliminar local se puede agregar al programa de instalación del dispositivo:
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>
);
};
Ejecución de la experiencia
Ahora que ha creado la pantalla de configuración del dispositivo, puede ejecutar la aplicación y ver la experiencia: