通話之前使用 Azure 通訊服務 UI 程式庫進行麥克風和相機設定
重要
此 Azure 通訊服務功能目前處於預覽狀態。
提供的預覽 API 和 SDK 並無服務等級協定。 建議您不要將其用於生產工作負載。 部分功能可能不受支援,或是在功能上有所限制。
如需詳細資訊,請參閱 Microsoft Azure 預覽版增補使用規定。
本教學課程接續通話整備教學課程的三部分系列,且是先前兩個部分的後續內容:
下載程式碼
在 GitHub 上存取本教學課程的完整程式碼。
讓使用者選擇其相機、麥克風和喇叭
在教學課程的前兩個部分中,使用者位於支援的瀏覽器上,而且已給予我們存取其相機和麥克風的權限。 我們現在可以確保使用者可以選擇要用於通話的正確麥克風、相機和喇叭。 我們向使用者呈現豐富的介面,供其選擇相機、麥克風和喇叭。 我們最終的裝置設定 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
的檔案,並建立三個掛勾: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
中建立的掛勾來填入下拉式清單,並在使用者從下拉式清單中選取不同的裝置時更新所選裝置。
為了容納這些新元件,我們會建立稱為 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>
);
};
建立本機視訊預覽
除了下拉式清單之外,我們還會建立本機視訊預覽,讓使用者可以查看其相機正在擷取的內容。 其包含一個具有相機和麥克風按鈕的小型通話控制列,可切換相機開啟/關閉,以及麥克風靜音/取消靜音。
首先,我們會在稱為 useLocalPreview
的 deviceSetupHooks.ts
中新增掛勾。 此掛勾為我們的 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>
);
};
執行體驗
既然您已建立裝置設定畫面,就可以執行應用程式並查看體驗: