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

在本快速入門中,您將瞭解如何從 Azure 通訊服務 使用者到 Teams 自動語音應答開始通話。 您將使用下列步驟來達成此目的:

  1. 使用 Teams 租使用者啟用 Azure 通訊服務 資源的同盟。
  2. 透過 Teams 管理員 中心選取或建立 Teams 自動語音應答。
  3. 透過 Teams 管理員 中心取得自動語音應答的電子郵件位址。
  4. 透過圖形 API 取得自動語音應答的物件識別碼。
  5. 使用 Azure 通訊服務呼叫 SDK 啟動呼叫。

如果您想要直接跳到結尾,您可以在 GitHub下載本快速入門作為範例。

在您的 Teams 租用戶中啟用互操作性

具有 Teams 系統管理員角色 的 Microsoft Entra 使用者可以使用 MicrosoftTeams 模組執行 PowerShell Cmdlet,以啟用租使用者中的通訊服務資源。

1.準備 Microsoft Teams 模組

首先,開啟 PowerShell,並使用下列命令驗證 Teams 模組是否存在:

Get-module *teams* 

如果您沒有看到模組 MicrosoftTeams ,請先加以安裝。 若要安裝模組,您必須以系統管理員身分執行 PowerShell。 然後執行下列命令:

	Install-Module -Name MicrosoftTeams

系統會通知您將要安裝的模組,您可以用 YA 答案進行確認。 如果模組已安裝但已過期,您可以執行下列命令來更新模組:

	Update-Module MicrosoftTeams

2. 連線 至 Microsoft Teams 課程模組

安裝模組並就緒時,您可以使用下列命令連線到MicrosftTeams模組。 系統會提示您輸入互動式視窗來登入。 您將使用的使用者帳戶必須具有 Teams 系統管理員許可權。 否則,您可能會在下一 access denied 個步驟中取得回應。

Connect-MicrosoftTeams

3.啟用租用戶設定

與通訊服務資源的互操作性是透過租用戶設定和指派的原則來控制。 Teams 租使用者具有單一租用戶設定,且 Teams 使用者已指派全域原則或自定義原則。 如需詳細資訊,請參閱 在Teams中指派原則。

成功登入之後,您可以執行 Cmdlet Set-CsTeamsAcsFederationConfiguration ,以在您的租用戶中啟用通訊服務資源。 以通訊資源中的不可變資源識別碼取代文字 IMMUTABLE_RESOURCE_ID 。 您可以在這裡找到有關如何取得這項資訊的詳細資訊。

$allowlist = @('IMMUTABLE_RESOURCE_ID')
Set-CsTeamsAcsFederationConfiguration -EnableAcsUsers $True -AllowedAcsResources $allowlist

4.啟用租用戶原則

每個 Teams 使用者都已指派 , External Access Policy 以判斷通訊服務使用者是否可以呼叫此 Teams 使用者。 使用 Cmdlet Set-CsExternalAccessPolicy 來確保指派給 Teams 使用者的原則已設定 EnableAcsFederationAccess$true

Set-CsExternalAccessPolicy -Identity Global -EnableAcsFederationAccess $true

建立或選取Teams自動語音應答

Teams 自動語音應答是系統,可為來電提供自動化通話處理系統。 它作為虛擬接待員,允許來電者自動路由傳送到適當的人員或部門,而不需要人類操作員。 您可以透過 Teams 管理員 中心選取現有或建立新的自動語音應答

在這裡深入瞭解如何使用Teams 管理員中心建立自動語音應答。

尋找自動語音應答的物件標識碼

建立自動語音應答之後,我們需要尋找相互關聯的物件標識符,以供稍後呼叫使用。 物件標識符會連線到連結至自動語音應答的資源帳戶 - 在 Teams 中開啟 [資源帳戶] 索引卷標,管理員 並尋找帳戶的電子郵件。 Teams 管理員 入口網站中資源帳戶的螢幕快照。資源帳戶的所有必要資訊都可以在 Microsoft Graph 總管中使用此電子郵件在搜尋中找到。

https://graph.microsoft.com/v1.0/users/lab-test2-cq-@contoso.com

在結果中,我們可以找到 [標識符] 字段

    "userPrincipalName": "lab-test2-cq@contoso.com",
    "id": "31a011c2-2672-4dd0-b6f9-9334ef4999db"

必要條件

設定

建立新的 Node.js 應用程式

開啟終端機或命令視窗,為您的應用程式建立新的目錄,然後瀏覽至目錄。

mkdir calling-quickstart && cd calling-quickstart

Install the package

npm install使用 命令來安裝 Azure 通訊服務呼叫 SDK for JavaScript。

重要

這個快速入門使用 Azure 通訊服務 呼叫 SDK 版本 next

npm install @azure/communication-common@next --save
npm install @azure/communication-calling@next --save

設定應用程式架構

本快速入門會使用 webpack 來組合應用程式資產。 執行下列命令以安裝webpack、 和 webpack-dev-server npm 套件,webpack-cli並將其列為開發package.json相依性:

npm install copy-webpack-plugin@^11.0.0 webpack@^5.88.2 webpack-cli@^5.1.4 webpack-dev-server@^4.15.1 --save-dev

在專案的根目錄中建立 index.html 檔案。 我們將使用此檔案來設定基本版面配置,讓用戶能夠撥打 1:1 視訊通話。

程式碼如下:

<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <title>Azure Communication Services - Calling Web SDK</title>
    </head>
    <body>
        <h4>Azure Communication Services - Calling Web SDK</h4>
        <input id="user-access-token"
            type="text"
            placeholder="User access token"
            style="margin-bottom:1em; width: 500px;"/>
        <button id="initialize-teams-call-agent" type="button">Initialize Call Agent</button>
        <br>
        <br>
        <input id="application-object-id"
            type="text"
            placeholder="Enter application objectId identity in format: 'APP_GUID'"
            style="margin-bottom:1em; width: 500px; display: block;"/>
        <button id="start-call-button" type="button" disabled="true">Start Call</button>
        <button id="hangup-call-button" type="button" disabled="true">Hang up Call</button>
        <button id="accept-call-button" type="button" disabled="true">Accept Call</button>
        <button id="start-video-button" type="button" disabled="true">Start Video</button>
        <button id="stop-video-button" type="button" disabled="true">Stop Video</button>
        <br>
        <br>
        <div id="connectedLabel" style="color: #13bb13;" hidden>Call is connected!</div>
        <br>
        <div id="remoteVideoContainer" style="width: 40%;" hidden>Remote participants' video streams:</div>
        <br>
        <div id="localVideoContainer" style="width: 30%;" hidden>Local video stream:</div>
        <!-- points to the bundle generated from client.js -->
        <script src="./main.js"></script>
    </body>
</html>

Azure 通訊服務呼叫 Web SDK 物件模型

下列類別和介面會處理 Azure 通訊服務 呼叫 SDK 的一些主要功能:

名稱 描述
CallClient 呼叫 SDK 的主要進入點。
CallAgent 用來啟動和管理話務。
DeviceManager 用來管理媒體裝置。
Call 用於表示通話。
LocalVideoStream 用於在本機系統上建立相機裝置的本機視訊串流。
RemoteParticipant 用於代表通話中的遠程參與者。
RemoteVideoStream 用於表示來自遠端參與者的遠端視訊串流。

在名為 client.js 的專案根目錄中建立檔案,以包含本快速入門的應用程式邏輯。 將下列程式代碼新增至client.js:

// Make sure to install the necessary dependencies
const { CallClient, VideoStreamRenderer, LocalVideoStream } = require('@azure/communication-calling');
const { AzureCommunicationTokenCredential } = require('@azure/communication-common');
const { AzureLogger, setLogLevel } = require("@azure/logger");
// Set the log level and output
setLogLevel('verbose');
AzureLogger.log = (...args) => {
    console.log(...args);
};
// Calling web sdk objects
let callAgent;
let deviceManager;
let call;
let incomingCall;
let localVideoStream;
let localVideoStreamRenderer;
// UI widgets
let userAccessToken = document.getElementById('user-access-token');
let applicationObjectId = document.getElementById('application-object-id');
let initializeCallAgentButton = document.getElementById('initialize-teams-call-agent');
let startCallButton = document.getElementById('start-call-button');
let hangUpCallButton = document.getElementById('hangup-call-button');
let acceptCallButton = document.getElementById('accept-call-button');
let startVideoButton = document.getElementById('start-video-button');
let stopVideoButton = document.getElementById('stop-video-button');
let connectedLabel = document.getElementById('connectedLabel');
let remoteVideoContainer = document.getElementById('remoteVideoContainer');
let localVideoContainer = document.getElementById('localVideoContainer');
/**
 * Create an instance of CallClient. Initialize a CallAgent instance with a AzureCommunicationTokenCredential via created CallClient. CallAgent enables us to make outgoing calls and receive incoming calls. 
 * You can then use the CallClient.getDeviceManager() API instance to get the DeviceManager.
 */
initializeCallAgentButton.onclick = async () => {
    try {
        const callClient = new CallClient(); 
        tokenCredential = new AzureCommunicationTokenCredential(userAccessToken.value.trim());
        callAgent = await callClient.createCallAgent(tokenCredential)
        // Set up a camera device to use.
        deviceManager = await callClient.getDeviceManager();
        await deviceManager.askDevicePermission({ video: true });
        await deviceManager.askDevicePermission({ audio: true });
        // Listen for an incoming call to accept.
        callAgent.on('incomingCall', async (args) => {
            try {
                incomingCall = args.incomingCall;
                acceptCallButton.disabled = false;
                startCallButton.disabled = true;
            } catch (error) {
                console.error(error);
            }
        });
        startCallButton.disabled = false;
        initializeCallAgentButton.disabled = true;
    } catch(error) {
        console.error(error);
    }
}
/**
 * Place a 1:1 outgoing video call to an Teams Auto attendant
 * Add an event listener to initiate a call when the `startCallButton` is selected.
 * Enumerate local cameras using the deviceManager `getCameraList` API.
 * In this quickstart, we're using the first camera in the collection. Once the desired camera is selected, a
 * LocalVideoStream instance will be constructed and passed within `videoOptions` as an item within the
 * localVideoStream array to the call method. When the call connects, your application will be sending a video stream to the other participant. 
 */
startCallButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
        call = callAgent.startCall([{ teamsAppId: applicationObjectId.value.trim(), cloud:"public" }], { videoOptions: videoOptions });
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }
}
/**
 * Accepting an incoming call with a video
 * Add an event listener to accept a call when the `acceptCallButton` is selected.
 * You can accept incoming calls after subscribing to the `CallAgent.on('incomingCall')` event.
 * You can pass the local video stream to accept the call with the following code.
 */
acceptCallButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
        call = await incomingCall.accept({ videoOptions });
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }
}
// Subscribe to a call obj.
// Listen for property changes and collection udpates.
subscribeToCall = (call) => {
    try {
        // Inspect the initial call.id value.
        console.log(`Call Id: ${call.id}`);
        //Subsribe to call's 'idChanged' event for value changes.
        call.on('idChanged', () => {
            console.log(`Call ID changed: ${call.id}`); 
        });
        // Inspect the initial call.state value.
        console.log(`Call state: ${call.state}`);
        // Subscribe to call's 'stateChanged' event for value changes.
        call.on('stateChanged', async () => {
            console.log(`Call state changed: ${call.state}`);
            if(call.state === 'Connected') {
                connectedLabel.hidden = false;
                acceptCallButton.disabled = true;
                startCallButton.disabled = true;
                hangUpCallButton.disabled = false;
                startVideoButton.disabled = false;
                stopVideoButton.disabled = false;
            } else if (call.state === 'Disconnected') {
                connectedLabel.hidden = true;
                startCallButton.disabled = false;
                hangUpCallButton.disabled = true;
                startVideoButton.disabled = true;
                stopVideoButton.disabled = true;
                console.log(`Call ended, call end reason={code=${call.callEndReason.code}, subCode=${call.callEndReason.subCode}}`);
            }   
        });

        call.on('isLocalVideoStartedChanged', () => {
            console.log(`isLocalVideoStarted changed: ${call.isLocalVideoStarted}`);
        });
        console.log(`isLocalVideoStarted: ${call.isLocalVideoStarted}`);
        call.localVideoStreams.forEach(async (lvs) => {
            localVideoStream = lvs;
            await displayLocalVideoStream();
        });
        call.on('localVideoStreamsUpdated', e => {
            e.added.forEach(async (lvs) => {
                localVideoStream = lvs;
                await displayLocalVideoStream();
            });
            e.removed.forEach(lvs => {
               removeLocalVideoStream();
            });
        });
        
        // Inspect the call's current remote participants and subscribe to them.
        call.remoteParticipants.forEach(remoteParticipant => {
            subscribeToRemoteParticipant(remoteParticipant);
        });
        // Subscribe to the call's 'remoteParticipantsUpdated' event to be
        // notified when new participants are added to the call or removed from the call.
        call.on('remoteParticipantsUpdated', e => {
            // Subscribe to new remote participants that are added to the call.
            e.added.forEach(remoteParticipant => {
                subscribeToRemoteParticipant(remoteParticipant)
            });
            // Unsubscribe from participants that are removed from the call
            e.removed.forEach(remoteParticipant => {
                console.log('Remote participant removed from the call.');
            });
        });
    } catch (error) {
        console.error(error);
    }
}
// Subscribe to a remote participant obj.
// Listen for property changes and collection udpates.
subscribeToRemoteParticipant = (remoteParticipant) => {
    try {
        // Inspect the initial remoteParticipant.state value.
        console.log(`Remote participant state: ${remoteParticipant.state}`);
        // Subscribe to remoteParticipant's 'stateChanged' event for value changes.
        remoteParticipant.on('stateChanged', () => {
            console.log(`Remote participant state changed: ${remoteParticipant.state}`);
        });
        // Inspect the remoteParticipants's current videoStreams and subscribe to them.
        remoteParticipant.videoStreams.forEach(remoteVideoStream => {
            subscribeToRemoteVideoStream(remoteVideoStream)
        });
        // Subscribe to the remoteParticipant's 'videoStreamsUpdated' event to be
        // notified when the remoteParticiapant adds new videoStreams and removes video streams.
        remoteParticipant.on('videoStreamsUpdated', e => {
            // Subscribe to newly added remote participant's video streams.
            e.added.forEach(remoteVideoStream => {
                subscribeToRemoteVideoStream(remoteVideoStream)
            });
            // Unsubscribe from newly removed remote participants' video streams.
            e.removed.forEach(remoteVideoStream => {
                console.log('Remote participant video stream was removed.');
            })
        });
    } catch (error) {
        console.error(error);
    }
}
/**
 * Subscribe to a remote participant's remote video stream obj.
 * You have to subscribe to the 'isAvailableChanged' event to render the remoteVideoStream. If the 'isAvailable' property
 * changes to 'true' a remote participant is sending a stream. Whenever the availability of a remote stream changes
 * you can choose to destroy the whole 'Renderer' a specific 'RendererView' or keep them. Displaying RendererView without a video stream will result in a blank video frame. 
 */
subscribeToRemoteVideoStream = async (remoteVideoStream) => {
    // Create a video stream renderer for the remote video stream.
    let videoStreamRenderer = new VideoStreamRenderer(remoteVideoStream);
    let view;
    const renderVideo = async () => {
        try {
            // Create a renderer view for the remote video stream.
            view = await videoStreamRenderer.createView();
            // Attach the renderer view to the UI.
            remoteVideoContainer.hidden = false;
            remoteVideoContainer.appendChild(view.target);
        } catch (e) {
            console.warn(`Failed to createView, reason=${e.message}, code=${e.code}`);
        }	
    }
    
    remoteVideoStream.on('isAvailableChanged', async () => {
        // Participant has switched video on.
        if (remoteVideoStream.isAvailable) {
            await renderVideo();
        // Participant has switched video off.
        } else {
            if (view) {
                view.dispose();
                view = undefined;
            }
        }
    });
    // Participant has video on initially.
    if (remoteVideoStream.isAvailable) {
        await renderVideo();
    }
}
// Start your local video stream.
// This will send your local video stream to remote participants so they can view it.
startVideoButton.onclick = async () => {
    try {
        const localVideoStream = await createLocalVideoStream();
        await call.startVideo(localVideoStream);
    } catch (error) {
        console.error(error);
    }
}
// Stop your local video stream.
// This will stop your local video stream from being sent to remote participants.
stopVideoButton.onclick = async () => {
    try {
        await call.stopVideo(localVideoStream);
    } catch (error) {
        console.error(error);
    }
}
/**
 * To render a LocalVideoStream, you need to create a new instance of VideoStreamRenderer, and then
 * create a new VideoStreamRendererView instance using the asynchronous createView() method.
 * You may then attach view.target to any UI element. 
 */
// Create a local video stream for your camera device
createLocalVideoStream = async () => {
    const camera = (await deviceManager.getCameras())[0];
    if (camera) {
        return new LocalVideoStream(camera);
    } else {
        console.error(`No camera device found on the system`);
    }
}
// Display your local video stream preview in your UI
displayLocalVideoStream = async () => {
    try {
        localVideoStreamRenderer = new VideoStreamRenderer(localVideoStream);
        const view = await localVideoStreamRenderer.createView();
        localVideoContainer.hidden = false;
        localVideoContainer.appendChild(view.target);
    } catch (error) {
        console.error(error);
    } 
}
// Remove your local video stream preview from your UI
removeLocalVideoStream = async() => {
    try {
        localVideoStreamRenderer.dispose();
        localVideoContainer.hidden = true;
    } catch (error) {
        console.error(error);
    } 
}
// End the current call
hangUpCallButton.addEventListener("click", async () => {
    // end the current call
    await call.hangUp();
});

新增 Webpack 本地伺服器程式代碼

在名為 webpack.config.js 的專案根目錄中建立檔案,以包含本快速入門的本機伺服器邏輯。 將下列程式代碼新增至 webpack.config.js

const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
    mode: 'development',
    entry: './client.js',
    output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        static: {
            directory: path.join(__dirname, './')
        },
    },
    plugins: [
        new CopyPlugin({
            patterns: [
                './index.html'
            ]
        }),
    ]
};

執行程式碼

webpack-dev-server使用來建置並執行您的應用程式。 執行下列命令,將應用程式主機組合在本機 Web 伺服器中:

npx webpack serve --config webpack.config.js

手動設定呼叫的步驟:

  1. 開啟瀏覽器並流覽至 http://localhost:8080/.
  2. 輸入有效的使用者存取令牌。 如果您還沒有可用的存取令牌,請參閱使用者存取令牌檔
  3. 按兩下 [初始化呼叫代理程式] 按鈕。
  4. 輸入自動語音應答物件標識符,然後選取 [開始通話] 按鈕。 應用程式會啟動具有指定物件標識碼之自動語音應答的傳出呼叫。
  5. 通話已連線到自動語音應答。
  6. 通訊服務用戶會根據其設定,透過自動語音應答路由傳送。

清除資源

如果您想要清除並移除通訊服務訂用帳戶,您可以刪除資源或資源群組。 刪除資源群組也會刪除與其相關聯的任何其他資源。 深入瞭解清除 資源

下一步

如需詳細資訊,請參閱下列文章: