共用方式為


開始使用 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 應用程式,我們使用 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 連結庫呼叫複合。 此小工具模式是在使用者呼叫 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 通訊服務身分識別和資源帳戶標識碼。 以下是、 tokenuserIdteamsAppIdentifier輸入值。

./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 自動語音應答

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