共用方式為


開始使用 Azure 通訊服務 UI 連結庫呼叫 Teams 語音應用程式

此專案旨在引導開發人員使用 Azure 通訊 UI 連結庫,從 Azure 通訊服務通話 Web SDK 到 Teams 通話佇列和自動語音應答起始通話。

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

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

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

請遵循本教學課程:

  • 可讓您根據客戶案例來控制客戶的音訊和視訊體驗
  • 教您如何建置小工具,以使用 UI 連結庫在Webapp上啟動呼叫。

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

必要條件

若要遵循本教學課程,需要這些步驟。 請連絡 Teams 管理員,以取得最後兩個專案,以確定您已正確設定。

檢查節點和Visual StudioCode

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

node -v

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

code --version

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

開始使用

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

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

1.設定專案

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

若要設定 react 應用程式,我們會使用 create-react-app 命令行工具。 此工具會建立一個容易執行由 React 提供的 TypeScript 應用程式。

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

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 ,並將該處的佔位元元值取代為您的Teams語音應用程式 Azure 通訊服務 身分識別和資源帳戶標識碼。 以下是、 userIdteamsAppIdentifiertoken輸入值。

./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 newe dependencies
npm install

# run the React app
npm run start

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

已關閉呼叫小工具範例應用程式首頁小工具的螢幕快照。

然後,當您執行小工具按鈕時,您應該會看到一個小功能表:

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

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

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

下一步

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

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

快速入門: 將您的通話應用程式加入 Teams 自動語音應答

快速入門:開始使用呼叫 Teams 通話佇列和自動語音應答的 Azure 通訊服務 UI 連結庫 JavaScript 套件組合