Поделиться через


Настройка микрофона и камеры перед вызовом с помощью библиотеки пользовательского интерфейса Службы коммуникации 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и useSpeakersuseCameras. Каждый из этих перехватчиков использует 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и MicrophoneSelectionDropdownSpeakerSelectionDropdown.

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>
  );
};

Запуск интерфейса

Теперь вы создали экран конфигурации устройства, вы можете запустить приложение и просмотреть интерфейс:

GIF-файл, показывающий сквозное взаимодействие с проверками готовности к вызову и настройкой устройства

Дальнейшие действия