Azure Communication Services の UI ライブラリを使用した、通話前のマイクとカメラのセットアップ
重要
Azure Communication Services のこの機能は、現在プレビュー段階にあります。
プレビューの API と SDK は、サービス レベル アグリーメントなしに提供されます。 運用環境のワークロードには使用しないことをお勧めします。 一部の機能はサポート対象ではなく、機能が制限されることがあります。
詳細については、「Microsoft Azure プレビューの追加利用規約」を確認してください。
このチュートリアルは、3 部構成のシリーズの Call Readiness チュートリアルの続きであり、以下の 2 つのパートの続編です。
コードをダウンロードする
このチュートリアルの完全なコードには GitHub でアクセスしてください。
ユーザーが自分のカメラ、マイク、スピーカーを選択できるようにする
前の 2 つのパートのチュートリアルでは、ユーザーがサポートされているブラウザーを使用していることを確認しました。また、ユーザーのカメラとマイクへのアクセス許可を取得しました。 次は、ユーザーが通話で使用する適当なマイク、カメラ、スピーカーを選択できるようにします。 ユーザーが自分のカメラ、マイク、スピーカーを選択できる高度なインターフェイスを表示します。 最終的なデバイスのセットアップ UI は次のようになります。
構成画面を作成する
まず、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
というファイルを作成して、3 つのフック、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;
}
デバイスを選択するためのドロップダウンを作成する
ユーザーが自分のカメラ、マイク、スピーカーを選択できるようにするために、Fluent UI React の Dropdown
コンポーネントを使用します。
ユーザーがドロップダウンで別のデバイスを選択したときに、deviceSetupHooks.tsx
で作成したフックを使用してドロップダウンの内容を設定し、選択したデバイスを更新する新しいコンポーネントを作成します。
これらの新しいコンポーネントを格納するために、3 つの新しいコンポーネント、CameraSelectionDropdown
、MicrophoneSelectionDropdown
、SpeakerSelectionDropdown
をエクスポートする DeviceSelectionComponents.tsx
というファイルを作成します。
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)}
/>
);
};
Device Setup にドロップダウンを追加する
ここで、カメラ、マイク、スピーカーのドロップダウンを Device Setup コンポーネントに追加できます。
まず、新しいドロップダウンをインポートします。
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>
);
};
ローカル ビデオ プレビューを作成する
ドロップダウンの隣にローカル ビデオ プレビューを作成して、カメラが捉えている映像をユーザーが確認できるようにします。 このローカル ビデオ プレビューには小さな通話コントロール バーがあり、カメラのオン/オフとマイクのミュート/ミュート解除の切り替えを行えるカメラとマイクのボタンがここに表示されます。
まず、useLocalPreview
という名前の deviceSetupHooks.ts
に新しいフックを追加します。 このフックが、レンダリングする localPreview と、ローカル プレビューの開始と停止を行う関数を react コンポーネントに提供します。
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
});
ローカル プレビューをデバイスのセットアップに追加する
これで、ローカル プレビュー コンポーネントを Device Setup に追加できます。
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>
);
};
動作を確認する
これでデバイスの構成画面を作成できたので、アプリを実行して動作を確認できます。