Sdílet prostřednictvím


Nastavení mikrofonu a kamery před voláním pomocí knihovny uživatelského rozhraní Azure Communication Services

Důležité

Tato funkce Azure Communication Services je aktuálně ve verzi Preview.

Rozhraní API a sady SDK verze Preview se poskytují bez smlouvy o úrovni služeb. Doporučujeme je nepoužívat pro produkční úlohy. Některé funkce nemusí být podporované nebo můžou mít omezené možnosti.

Další informace najdete v dodatečných podmínkách použití pro verze Preview Microsoft Azure.

Tento kurz je pokračováním třídílné série kurzů připravenosti na volání a navazuje na předchozí dvě části:

Stažení kódu

Získejte přístup k úplnému kódu pro tento kurz na GitHubu.

Nastavení kamery, mikrofonu a reproduktoru uživateli

V předchozích dvou částech kurzu je uživatel v podporovaném prohlížeči a udělil nám oprávnění pro přístup ke kameře a mikrofonu. Teď můžeme zajistit, aby uživatel mohl zvolit správný mikrofon, kameru a reproduktor, které chce pro svůj hovor použít. Uživateli nabízíme bohaté rozhraní pro výběr kamery, mikrofonu a reproduktoru. Naše konečné uživatelské rozhraní pro nastavení zařízení vypadá takto:

Obrázek stránky nastavení zařízení

Vytvoření konfigurační obrazovky

Nejprve vytvoříme nový soubor s názvem DeviceSetup.tsx a přidáme instalační kód se zpětným voláním, které vrátí zařízení zvolená uživateli zpět do aplikace:

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

Potom můžeme tuto sadu DeviceSetup přidat do naší aplikace.

  • Po dokončení PreCallChecksComponent předá uživatele do deviceSetup stavu.
  • Když je uživatel ve stavu , vykreslíme deviceSetup komponentu DeviceSetup .
  • Po dokončení nastavení zařízení se uživatel předá do finished stavu . V produkční aplikaci k tomu obvykle dochází, když uživatele přesunete na obrazovku volání.

Nejprve naimportujte komponentu DeviceSetup, kterou jsme vytvořili:

src/App.tsx

import { DeviceSetup } from './DeviceSetup';

Pak aplikaci aktualizujte tak, aby měla nový testovací stav deviceSetup:

type TestingState = 'runningEnvironmentChecks' | 'runningDeviceAccessChecks' | 'deviceSetup' | 'finished';

Nakonec aktualizujte naši App komponentu tak, aby po dokončení kontrol přístupu zařízení přecháděla aplikaci na nastavení zařízení:

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

Načítání a aktualizace seznamu mikrofonu, kamery a mluvčího ze stavového klienta

Pokud chcete uživateli prezentovat seznam vybraných kamer, mikrofonů a reproduktorů, můžeme použít klienta stavového volání. Tady vytvoříme řadu React háků. Tyto React volání používají klienta volání k dotazování na dostupná zařízení. Háčky zajišťují, že se naše aplikace znovu vykreslí pokaždé, když se seznam změní, například když se k počítači uživatele připojí nová kamera. Pro tyto hooky vytvoříme soubor s názvem deviceSetupHooks.ts a vytvoříme tři hooky: useMicrophones, useSpeakers a useCameras. Každý z těchto háčků používá useCallClientStateChange k aktualizaci svých seznamů, kdykoli uživatel připojí nebo odpojí zařízení:

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

Vytváření rozevíracích seznamu pro výběr zařízení

Abychom uživateli umožnili zvolit kameru, mikrofon a reproduktor, používáme komponentu Dropdown z rozhraní Fluent UI React. Vytvoříme nové komponenty, které pomocí zátahů, které jsme vytvořili v deviceSetupHooks.tsx , vyplní rozevírací seznam a aktualizují zvolené zařízení, když uživatel vybere z rozevíracího seznamu jiné zařízení. K umístění těchto nových komponent vytvoříme soubor s názvem DeviceSelectionComponents.tsx , který exportuje tři nové komponenty: CameraSelectionDropdown, MicrophoneSelectionDropdown a 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)}
    />
  );
};
Přidání rozevíracích seznamu do nastavení zařízení

Rozevírací seznamy kamery, mikrofonu a reproduktoru pak můžete přidat do komponenty Nastavení zařízení.

Nejprve naimportujte nové rozevírací seznamy:

src/DeviceSetup.tsx

import { CameraSelectionDropdown, MicrophoneSelectionDropdown, SpeakerSelectionDropdown } from './DeviceSelectionComponents';

Pak vytvořte komponentu s názvem DeviceSetup , která obsahuje tyto rozevírací seznamy. Tato komponenta obsahuje místní náhled videa, který vytvoříme později.

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

Vytvoření místního náhledu videa

Vedle rozevíracích seznamu vytvoříme místní náhled videa, aby uživatel viděl, co jeho kamera zachycuje. Obsahuje malý panel ovládacích prvků volání s tlačítky kamery a mikrofonu pro zapnutí a vypnutí kamery a ztlumení nebo zrušení ztlumení mikrofonu.

Nejprve do s deviceSetupHooks.ts názvem useLocalPreviewpřidáme nový háček . Tento hook poskytuje naší komponentě React místní zobrazení pro vykreslení a funkce pro spuštění a zastavení místního náhledu:

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

Pak vytvoříme novou komponentu s názvem LocalPreview.tsx , která použije tento háček k zobrazení náhledu místního videa uživateli:

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
  });
Přidání místního náhledu do nastavení zařízení

Místní komponentu Preview je pak možné přidat do nastavení zařízení:

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

Spuštění prostředí

Teď, když jste vytvořili obrazovku konfigurace zařízení, můžete aplikaci spustit a podívat se na prostředí:

Gif znázorňující kompletní prostředí kontrol připravenosti volání a nastavení zařízení

Další kroky