你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn。
开始使用针对 Teams 语音应用的 Azure 通信服务 UI 库调用
此项目旨在指导开发人员使用 Azure 通信 UI 库从 Azure 通信服务呼叫 Web SDK 向 Teams 呼叫队列和自动助理发起呼叫。
根据要求,可能需要为客户提供简单的方式来联系你,但无需任何复杂的设置。
调用 Teams 呼叫队列和自动助理是简单而有效的概念,有助于与客户支持、财务顾问和其他面向客户的团队进行即时互动。 本教程的目标是帮助你在客户单击 Web 上的按钮时启动与客户的交互。
如果想尝试它,可以从 GitHub 下载代码。
遵循本教程将:
- 允许根据客户方案控制客户的音频和视频体验
- 教你如何构建一个小组件,以便使用 UI 库在 webapp 上启动调用。
先决条件
若为了循本教程,需要执行这些步骤。 请联系 Teams 管理员了解最后两项,以确保设置正确。
- 安装在某个受支持的平台上的 Visual Studio Code。
- 建议使用 Node.js 活动 LTS(长期支持)和版本 Node 20。 可以使用
node --version
命令检查你的版本。 - 一个 Azure 通信服务资源。 创建通信资源
- 完成 Teams 租户设置,以便与 Azure 通信服务资源互操作
- 使用 Teams 呼叫队列和 Azure 通信服务。
- 使用 Teams 自动助理和 Azure 通信服务。
检查 Node 和 Visual Studio Code
可以使用此命令检查是否已正确安装 Node。
node -v
输出会告知已有的版本,如果未安装 Node 并将其添加到 PATH
,它将失败。 与 Node 一样,可以检查是否已使用此命令安装 VS Code。
code --version
与 Node 一样,如果在计算机上安装 VS Code 时出现问题,此命令将失败。
使用入门
本教程包含 7 个步骤,最终应用将能够调用 Teams 语音应用程序。 步骤如下:
1.设置项目
只有在创建新应用程序时才使用此步骤。
要设置 react 应用,使用 create-react-app
命令行工具。 此工具创建了由 React 支持的易于运行的 TypeScript 应用程序。
若要确保已在计算机上安装 Node,请在 PowerShell 或终端中运行以下命令以查看 Node 版本:
node -v
如果计算机上未安装 create-react-app
,请运行以下命令将其安装为全局命令:
npm install -g create-react-app
安装该命令后,运行以下命令以创建新的 react 应用程序以生成示例:
# Create an Azure Communication Services App powered by React.
npx create-react-app ui-library-calling-widget-app --template typescript
# Change to the directory of the newly created App.
cd ui-library-calling-widget-app
这些命令完成后,需要在 VS Code 中打开创建的项目。 可以使用以下命令打开项目。
code .
2.取得依赖项
然后,你需要更新 package.json
中的依赖项数组,以包括来自 Azure 通信服务的一些包,从而实现我们将要构建的小组件体验:
"@azure/communication-calling": "^1.23.1",
"@azure/communication-chat": "^1.4.0",
"@azure/communication-react": "^1.15.0",
"@azure/communication-calling-effects": "1.0.1",
"@azure/communication-common": "2.3.0",
"@fluentui/react-icons": "~2.0.203",
"@fluentui/react": "~8.98.3",
若要安装所需的包,请运行以下 Node 包管理器命令。
npm install
安装这些包后,将全部设置为开始编写生成应用程序的代码。 在本教程中,我们将修改 src
目录中的文件。
3.初始应用设置
首先,我们将提供的 App.tsx
内容替换为主页,该主页将:
- 存储创建 CallAdapter 以增强呼叫体验所需的所有 Azure 通信信息
- 显示向最终用户公开的小组件。
App.tsx
文件应如下所示:
src/App.tsx
import "./App.css";
import {
CommunicationIdentifier,
MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
Spinner,
Stack,
initializeIcons,
registerIcons,
Text,
} from "@fluentui/react";
import { CallAdd20Regular, Dismiss20Regular } from "@fluentui/react-icons";
import logo from "./logo.svg";
import { CallingWidgetComponent } from "./components/CallingWidgetComponent";
registerIcons({
icons: { dismiss: <Dismiss20Regular />, callAdd: <CallAdd20Regular /> },
});
initializeIcons();
function App() {
/**
* Token for local user.
*/
const token = "<Enter your ACS Token here>";
/**
* User identifier for local user.
*/
const userId: CommunicationIdentifier = {
communicationUserId: "Enter your ACS Id here",
};
/**
* Enter your Teams voice app identifier from the Teams admin center here
*/
const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
teamsAppId: "<Enter your Teams Voice app id here>",
cloud: "public",
};
const widgetParams = {
userId,
token,
teamsAppIdentifier,
};
if (!token || !userId || !teamsAppIdentifier) {
return (
<Stack verticalAlign="center" style={{ height: "100%", width: "100%" }}>
<Spinner
label={"Getting user credentials from server"}
ariaLive="assertive"
labelPosition="top"
/>
</Stack>
);
}
return (
<Stack
style={{ height: "100%", width: "100%", padding: "3rem" }}
tokens={{ childrenGap: "1.5rem" }}
>
<Stack tokens={{ childrenGap: "1rem" }} style={{ margin: "auto" }}>
<Stack
style={{ padding: "3rem" }}
horizontal
tokens={{ childrenGap: "2rem" }}
>
<Text style={{ marginTop: "auto" }} variant="xLarge">
Welcome to a Calling Widget sample
</Text>
<img
style={{ width: "7rem", height: "auto" }}
src={logo}
alt="logo"
/>
</Stack>
<Text>
Welcome to a Calling Widget sample for the Azure Communication
Services UI Library. Sample has the ability to connect you through
Teams voice apps to a agent to help you.
</Text>
<Text>
As a user all you need to do is click the widget below, enter your
display name for the call - this will act as your caller id, and
action the <b>start call</b> button.
</Text>
</Stack>
<Stack
horizontal
tokens={{ childrenGap: "1.5rem" }}
style={{ overflow: "hidden", margin: "auto" }}
>
<CallingWidgetComponent
widgetAdapterArgs={widgetParams}
onRenderLogo={() => {
return (
<img
style={{ height: "4rem", width: "4rem", margin: "auto" }}
src={logo}
alt="logo"
/>
);
}}
/>
</Stack>
</Stack>
);
}
export default App;
在这个片段中,我们注册了两个新的图标 <Dismiss20Regular/>
和 <CallAdd20Regular>
。 这些新图标在下一部分创建的小组件组件中使用。
4.创建小组件
现在我们需要制作一个可以以三种不同模式显示的小组件:
- 正在等待:此小组件状态是组件在进行调用之前和之后的状态
- 设置:此状态是指小组件从用户请求信息(例如姓名)时的状态。
- 在调用中:此处的小组件被替换为 UI 库通话复合。 此小组件模式是用户呼叫 Voice 应用或与代理交谈时的模式。
让我们创建一个名为 src/components
的文件夹。 在此文件夹中,创建一个名为 CallingWidgetComponent.tsx
的新文件。 该文件应如以下代码片段所示:
CallingWidgetComponent.tsx
import {
IconButton,
PrimaryButton,
Stack,
TextField,
useTheme,
Checkbox,
Icon,
Spinner,
} from "@fluentui/react";
import React, { useEffect, useRef, useState } from "react";
import {
callingWidgetSetupContainerStyles,
checkboxStyles,
startCallButtonStyles,
callingWidgetContainerStyles,
callIconStyles,
logoContainerStyles,
collapseButtonStyles,
} from "../styles/CallingWidgetComponent.styles";
import {
AzureCommunicationTokenCredential,
CommunicationUserIdentifier,
MicrosoftTeamsAppIdentifier,
} from "@azure/communication-common";
import {
CallAdapter,
CallAdapterState,
CallComposite,
CommonCallAdapterOptions,
StartCallIdentifier,
createAzureCommunicationCallAdapter,
} from "@azure/communication-react";
// lets add to our react imports as well
import { useMemo } from "react";
import { callingWidgetInCallContainerStyles } from "../styles/CallingWidgetComponent.styles";
/**
* Properties needed for our widget to start a call.
*/
export type WidgetAdapterArgs = {
token: string;
userId: CommunicationUserIdentifier;
teamsAppIdentifier: MicrosoftTeamsAppIdentifier;
};
export interface CallingWidgetComponentProps {
/**
* arguments for creating an AzureCommunicationCallAdapter for your Calling experience
*/
widgetAdapterArgs: WidgetAdapterArgs;
/**
* Custom render function for displaying logo.
* @returns
*/
onRenderLogo?: () => JSX.Element;
}
/**
* Widget for Calling Widget
* @param props
*/
export const CallingWidgetComponent = (
props: CallingWidgetComponentProps
): JSX.Element => {
const { onRenderLogo, widgetAdapterArgs } = props;
const [widgetState, setWidgetState] = useState<"new" | "setup" | "inCall">(
"new"
);
const [displayName, setDisplayName] = useState<string>();
const [consentToData, setConsentToData] = useState<boolean>(false);
const [useLocalVideo, setUseLocalVideo] = useState<boolean>(false);
const [adapter, setAdapter] = useState<CallAdapter>();
const callIdRef = useRef<string>();
const theme = useTheme();
// add this before the React template
const credential = useMemo(() => {
try {
return new AzureCommunicationTokenCredential(widgetAdapterArgs.token);
} catch {
console.error("Failed to construct token credential");
return undefined;
}
}, [widgetAdapterArgs.token]);
const adapterOptions: CommonCallAdapterOptions = useMemo(
() => ({
callingSounds: {
callEnded: { url: "/sounds/callEnded.mp3" },
callRinging: { url: "/sounds/callRinging.mp3" },
callBusy: { url: "/sounds/callBusy.mp3" },
},
}),
[]
);
const callAdapterArgs = useMemo(() => {
return {
userId: widgetAdapterArgs.userId,
credential: credential,
targetCallees: [
widgetAdapterArgs.teamsAppIdentifier,
] as StartCallIdentifier[],
displayName: displayName,
options: adapterOptions,
};
}, [
widgetAdapterArgs.userId,
widgetAdapterArgs.teamsAppIdentifier.teamsAppId,
credential,
displayName,
]);
useEffect(() => {
if (adapter) {
adapter.on("callEnded", () => {
/**
* We only want to reset the widget state if the call that ended is the same as the current call.
*/
if (
adapter.getState().acceptedTransferCallState &&
adapter.getState().acceptedTransferCallState?.id !== callIdRef.current
) {
return;
}
setDisplayName(undefined);
setWidgetState("new");
setConsentToData(false);
setAdapter(undefined);
adapter.dispose();
});
adapter.on("transferAccepted", (e) => {
console.log("transferAccepted", e);
});
adapter.onStateChange((state: CallAdapterState) => {
if (state?.call?.id && callIdRef.current !== state?.call?.id) {
callIdRef.current = state?.call?.id;
console.log(`Call Id: ${callIdRef.current}`);
}
});
}
}, [adapter]);
/** widget template for when widget is open, put any fields here for user information desired */
if (widgetState === "setup") {
return (
<Stack
styles={callingWidgetSetupContainerStyles(theme)}
tokens={{ childrenGap: "1rem" }}
>
<IconButton
styles={collapseButtonStyles}
iconProps={{ iconName: "Dismiss" }}
onClick={() => {
setDisplayName(undefined);
setConsentToData(false);
setUseLocalVideo(false);
setWidgetState("new");
}}
/>
<Stack tokens={{ childrenGap: "1rem" }} styles={logoContainerStyles}>
<Stack style={{ transform: "scale(1.8)" }}>
{onRenderLogo && onRenderLogo()}
</Stack>
</Stack>
<TextField
label={"Name"}
required={true}
placeholder={"Enter your name"}
onChange={(_, newValue) => {
setDisplayName(newValue);
}}
/>
<Checkbox
styles={checkboxStyles(theme)}
label={
"Use video - Checking this box will enable camera controls and screen sharing"
}
onChange={(_, checked?: boolean | undefined) => {
setUseLocalVideo(!!checked);
setUseLocalVideo(true);
}}
></Checkbox>
<Checkbox
required={true}
styles={checkboxStyles(theme)}
disabled={displayName === undefined}
label={
"By checking this box, you are consenting that we will collect data from the call for customer support reasons"
}
onChange={async (_, checked?: boolean | undefined) => {
setConsentToData(!!checked);
if (callAdapterArgs && callAdapterArgs.credential) {
setAdapter(
await createAzureCommunicationCallAdapter({
displayName: displayName ?? "",
userId: callAdapterArgs.userId,
credential: callAdapterArgs.credential,
targetCallees: callAdapterArgs.targetCallees,
options: callAdapterArgs.options,
})
);
}
}}
></Checkbox>
<PrimaryButton
styles={startCallButtonStyles(theme)}
onClick={() => {
if (displayName && consentToData && adapter) {
setWidgetState("inCall");
adapter?.startCall(callAdapterArgs.targetCallees, {
audioOptions: { muted: false },
});
}
}}
>
{!consentToData && `Enter your name`}
{consentToData && !adapter && (
<Spinner ariaLive="assertive" labelPosition="top" />
)}
{consentToData && adapter && `StartCall`}
</PrimaryButton>
</Stack>
);
}
if (widgetState === "inCall" && adapter) {
return (
<Stack styles={callingWidgetInCallContainerStyles(theme)}>
<CallComposite
adapter={adapter}
options={{
callControls: {
cameraButton: useLocalVideo,
screenShareButton: useLocalVideo,
moreButton: false,
peopleButton: false,
displayType: "compact",
},
localVideoTile: !useLocalVideo ? false : { position: "floating" },
}}
/>
</Stack>
);
}
return (
<Stack
horizontalAlign="center"
verticalAlign="center"
styles={callingWidgetContainerStyles(theme)}
onClick={() => {
setWidgetState("setup");
}}
>
<Stack
horizontalAlign="center"
verticalAlign="center"
style={{
height: "4rem",
width: "4rem",
borderRadius: "50%",
background: theme.palette.themePrimary,
}}
>
<Icon iconName="callAdd" styles={callIconStyles(theme)} />
</Stack>
</Stack>
);
};
在 CallAdapterOptions
中,我们看到引用了一些声音文件,这些文件会使用通话声音功能中的 CallComposite
。 如果你有兴趣使用声音,请参阅完整的代码来下载声音文件。
5.设置小组件样式
我们需要编写一些样式来确保小组件看起来合适并且可以容纳我们的通话复合。 如果复制添加到文件 CallingWidgetComponent.tsx
的代码片段,则应在小组件中使用这些样式。
让我们创建一个名为 src/styles
的新文件夹,并在此文件夹中创建一个名为 CallingWidgetComponent.styles.ts
的文件。 该文件应如以下代码片段所示:
import {
IButtonStyles,
ICheckboxStyles,
IIconStyles,
IStackStyles,
Theme,
} from "@fluentui/react";
export const checkboxStyles = (theme: Theme): ICheckboxStyles => {
return {
label: {
color: theme.palette.neutralPrimary,
},
};
};
export const callingWidgetContainerStyles = (theme: Theme): IStackStyles => {
return {
root: {
width: "5rem",
height: "5rem",
padding: "0.5rem",
boxShadow: theme.effects.elevation16,
borderRadius: "50%",
bottom: "1rem",
right: "1rem",
position: "absolute",
overflow: "hidden",
cursor: "pointer",
":hover": {
boxShadow: theme.effects.elevation64,
},
},
};
};
export const callingWidgetSetupContainerStyles = (
theme: Theme
): IStackStyles => {
return {
root: {
width: "18rem",
minHeight: "20rem",
maxHeight: "25rem",
padding: "0.5rem",
boxShadow: theme.effects.elevation16,
borderRadius: theme.effects.roundedCorner6,
bottom: 0,
right: "1rem",
position: "absolute",
overflow: "hidden",
cursor: "pointer",
background: theme.palette.white,
},
};
};
export const callIconStyles = (theme: Theme): IIconStyles => {
return {
root: {
paddingTop: "0.2rem",
color: theme.palette.white,
transform: "scale(1.6)",
},
};
};
export const startCallButtonStyles = (theme: Theme): IButtonStyles => {
return {
root: {
background: theme.palette.themePrimary,
borderRadius: theme.effects.roundedCorner6,
borderColor: theme.palette.themePrimary,
},
textContainer: {
color: theme.palette.white,
},
};
};
export const logoContainerStyles: IStackStyles = {
root: {
margin: "auto",
padding: "0.2rem",
height: "5rem",
width: "10rem",
zIndex: 0,
},
};
export const collapseButtonStyles: IButtonStyles = {
root: {
position: "absolute",
top: "0.2rem",
right: "0.2rem",
zIndex: 1,
},
};
export const callingWidgetInCallContainerStyles = (
theme: Theme
): IStackStyles => {
return {
root: {
width: "35rem",
height: "25rem",
padding: "0.5rem",
boxShadow: theme.effects.elevation16,
borderRadius: theme.effects.roundedCorner6,
bottom: 0,
right: "1rem",
position: "absolute",
overflow: "hidden",
cursor: "pointer",
background: theme.semanticColors.bodyBackground,
},
};
};
6.设置标识值
在运行应用之前,请转到 App.tsx
,并将该处的占位符值替换为 Azure 通信服务标识以及 Teams Voice 应用程序的资源帐户标识符。 下面是 token
、userId
和 teamsAppIdentifier
的输入值。
./src/App.tsx
/**
* Token for local user.
*/
const token = "<Enter your ACS Token here>";
/**
* User identifier for local user.
*/
const userId: CommunicationIdentifier = {
communicationUserId: "Enter your ACS Id here",
};
/**
* Enter your Teams voice app identifier from the Teams admin center here
*/
const teamsAppIdentifier: MicrosoftTeamsAppIdentifier = {
teamsAppId: "<Enter your Teams Voice app id here>",
cloud: "public",
};
7.运行应用
我们终于可以运行应用程序来拨打电话了! 运行以下命令来安装依赖项并运行应用。
# Install the new dependencies
npm install
# run the React app
npm run start
应用程序运行后,可以在浏览器的 http://localhost:3000
上看到它。 应会看到以下初始屏幕:
然后,在操作小组件按钮时,应会看到一个小菜单:
填写姓名后,单击“开始通话”,通话就会开始。 开始通话后小组件应如下所示:
后续步骤
有关 Teams 语音应用程序的详细信息,请查看有关 Teams 自动助理和 Teams 呼叫队列的文档。 或者,请参阅有关如何使用 JavaScript 捆绑包生成类似体验的教程。
快速入门:开始使用针对 Teams 呼叫队列和自动助理的 Azure 通信服务 UI 库 JavaScript 捆绑包调用