開始使用 Azure 通訊服務 UI 程式庫與 Teams 語音應用程式通話

Warning

Azure 通訊服務 正在推出突破性變更,部分服務也將被淘汰。 想了解更多,請參閱退休 與變更指南。

此專案旨在引導開發人員使用 Azure Communication UI Library,透過 Azure 通訊服務 Calling Web SDK 對 Teams 通話佇列和自動話務員發起通話。

視您的需求而定,您可能需要為客戶提供簡單的方法來連絡您,而不需要任何複雜的設定。

撥打 Teams 通話佇列與自動語音應答是一個簡單卻有效的概念,可協助您立即與客戶支援、財務顧問及其他面向客戶的團隊互動。 本教學課程的目標是協助您在客戶按一下 Web 上的按鈕時,起始與客戶的互動。

如果您想要試用,您可以從 GitHub 下載程式碼。

依照本教學操作將可:

  • 可讓您根據客戶案例來控制客戶的音訊和視訊體驗
  • 教您如何使用 UI 程式庫建立可在您的 Web 應用程式中發起通話的小工具。

通話小工具範例應用程式的首頁

必要條件

若要遵循本教學課程,需要這些步驟。 請就最後兩個項目聯絡您的 Teams 管理員,以確保已為您妥善設定。

檢查是否已安裝 Node 和 Visual Studio Code

您可以使用此命令檢查 是否已正確安裝 Node 。

node -v

輸出會告訴您您擁有的版本,如果 Node 未安裝並新增至您的 PATH,就會失敗。 就像 Node 一樣,您可以用這個命令檢查是否已安裝 VS Code。

code --version

就像使用 Node 一樣,如果電腦上安裝 VS Code 時發生問題,此命令就會失敗。

開始使用

本教學課程有 7 個步驟,最後應用程式將能夠呼叫 Teams 語音應用程式。 步驟是:

  1. 設定專案
  2. 取得相依性
  3. 初始應用程式設定
  4. 建立小工具
  5. 設定小工具的樣式
  6. 設定識別值
  7. 執行應用程式

1.設定專案

只有在您要建立新的應用程式時,才使用此步驟。

若要建立 react 應用程式,我們使用 vite。 它是一種建置工具,旨在為新式 Web 專案提供更快速且更精簡的開發體驗。 您可以在這裡深入瞭解vite如何從頭開始建立 React 應用程式和開始使用 Vite

請注意,Azure 通訊服務 UI 庫僅支援最高至 React 18。 請在建立 React 應用程式後確認您使用的是支援的版本,並檢查 package.json 以驗證。

若要確定您已在電腦上安裝 Node,請在 PowerShell 或終端機中執行此命令,以查看您的節點版本:

node -v

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 程式庫的 Call Composite 取代此小工具。 此小工具模式是在使用者呼叫 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 dev

應用程式啟動後,您可以在瀏覽器中於 http://localhost:3000 查看它。 您應該會看到下列啟動顯示畫面:

呼叫小工具範例應用程式首頁的小工具關閉狀態螢幕擷取畫面。

接著,當您按一下小工具按鈕時,應該會看到一個小型選單:

呼叫小工具範例應用程式首頁小工具開啟的螢幕快照。

填寫名稱之後,按兩下 [開始呼叫],呼叫應該開始。 小工具在開始呼叫之後看起來應該像這樣:

按兩下即可呼叫範例應用程式首頁的螢幕快照,其中包含內嵌在Widget中的通話體驗。

下一步

如需 Teams 語音應用程式的詳細資訊,請參閱 Teams 自動語音應答和 Teams 通話佇列的相關文件。 或者,另請參閱我們的教學課程,瞭解如何使用JavaScript套件組合建置類似的體驗。

快速入門:將您的通話應用程式加入 Teams 通話佇列

快速入門:將您的通話應用程式連接到 Teams 自動總機

快速入門:開始使用 Azure 通訊服務 UI 程式庫 JavaScript 套件組合,撥打 Teams 通話佇列與自動語音應答