クイックスタート: 通話アプリを Teams 自動応答に参加させる

このクイックスタートでは、Azure Communication Services ユーザーから Teams 自動応答への通話を開始する方法について説明します。 これを実現するには、次の手順を実行します。

  1. Teams テナントでの Azure Communication Services リソースのフェデレーションを有効にします。
  2. Teams 管理センターを使用して、Teams 自動応答を選択または作成します。
  3. Teams 管理センターを介して自動応答のメール アドレスを取得します。
  4. Graph API を使用して自動応答のオブジェクト ID を取得します。
  5. Azure Communication Services Calling SDK を使用して通話を開始します。

最後までスキップしたい場合は、GitHub のサンプルとしてこのクイックスタートをダウンロードできます。

Teams テナントで相互運用性を有効にする

Teams 管理者ロールを持つ Microsoft Entra ユーザーは、MicrosoftTeams モジュールを使って PowerShell コマンドレットを実行し、テナント内の Communication Services リソースを有効にすることができます。

1.Microsoft Teams モジュールを準備する

まず、PowerShell を開き、次のコマンドを使って Teams モジュールの存在を検証します。

Get-module *teams* 

MicrosoftTeams モジュールが表示されない場合は、最初にインストールします。 モジュールをインストールするには、管理者として PowerShell を実行する必要があります。 次に、次のコマンドを実行します。

	Install-Module -Name MicrosoftTeams

インストールされるモジュールが表示されるので、Y または A で応答して確認します。 モジュールがインストールされていても古くなっている場合は、次のコマンドを実行してモジュールを更新できます。

	Update-Module MicrosoftTeams

2.Microsoft Teams モジュールに接続する

モジュールがインストールされ準備ができたら、次のコマンドを使って MicrosftTeams モジュールに接続できます。 ログインするための対話型ウィンドウが表示されます。 使用するユーザー アカウントには、Teams 管理者のアクセス許可が必要です。 それ以外の場合は、次の手順で access denied 応答が返される可能性があります。

Connect-MicrosoftTeams

3.テナント構成を有効にする

Communication Services リソースとの相互運用性は、テナント構成と割り当てられたポリシーによって制御されます。 Teams テナントには 1 つのテナント構成があり、Teams ユーザーにはグローバル ポリシーまたはカスタム ポリシーが割り当てられます。 詳細については、「Teams でポリシーを割り当てる」を参照してください。

ログインに成功したら、コマンドレット Set-CsTeamsAcsFederationConfiguration を実行して、テナントで Communication Services リソースを有効にすることができます。 テキスト IMMUTABLE_RESOURCE_ID を、コミュニケーション リソース内の不変リソース ID に置き換えます。 この情報を取得する方法の詳細については、こちらを参照してください。

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

4.テナント ポリシーを有効にする

各 Teams ユーザーには、Communication Services ユーザーがこの Teams ユーザーを呼び出すことができるかどうかを決定する External Access Policy が割り当てられます。 コマンドレット Set-CsExternalAccessPolicy を使って、Teams ユーザーに割り当てられたポリシーで EnableAcsFederationAccess$true に設定されていることを確認します

Set-CsExternalAccessPolicy -Identity Global -EnableAcsFederationAccess $true

Teams 自動応答を作成または選択する

Teams 自動応答は、着信通話用の自動通話処理システムを提供するシステムです。 仮想の受付担当として機能し、人間のオペレーターを必要とせずに、適切な担当者または部門に発信者を自動的にルーティングできます。 Teams 管理センターを使用して、既存の自動応答を選択するか、新しく作成できます。

Teams 管理センターを使用して自動応答を作成する方法の詳細については、こちらを参照してください。

自動応答のオブジェクト ID を見つける

自動応答を作成したら、後で通話に使用するために、関連づけられたオブジェクト ID を見つける必要があります。 オブジェクト ID は、自動応答にアタッチされたリソース アカウントに関連付けられています。Teams 管理の [リソース アカウント] タブを開き、アカウントのメールを検索します。 Teams 管理ポータルの [リソース アカウント] のスクリーンショット。 リソース アカウントに必要なすべての情報は、検索でこのメールを使用して、Microsoft Graph Explorer で確認できます。

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

結果で、"ID" フィールドが見つかります。

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

前提条件

設定

新しい Node.js アプリケーションを作成する

ターミナルまたはコマンド ウィンドウを開き、自分のアプリ用に新しいディレクトリを作成して、そのディレクトリに移動します。

mkdir calling-quickstart && cd calling-quickstart

パッケージをインストールする

npm install コマンドを使用して、JavaScript 用の Azure Communication Services Calling SDK をインストールします。

重要

このクイックスタートでは、Azure Communication Services Calling SDK バージョン next を使用します。

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

アプリのフレームワークを設定する

このクイックスタートでは、webpack を使用してアプリケーション資産をバンドルします。 次のコマンドを実行して webpackwebpack-cliwebpack-dev-server の 3 つの npm パッケージをインストールし、開発の依存関係として 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 Communication Services Calling Web SDK オブジェクト モデル

Azure Communication Services Calling SDK の主な機能のいくつかは、次のクラスとインターフェイスによって処理されます。

名前 説明
CallClient Calling 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. 両方のタブで、[Initialize Call Agent] (通話エージェントの初期化) ボタンをクリックします。
  4. 自動応答のオブジェクト ID を入力し、[Start Call] (通話を開始) ボタンを選択します。 アプリケーションによって、指定したオブジェクト ID を使用して自動応答への発信通話が開始されます。
  5. 通話は自動応答に接続されます。
  6. Communication Services ユーザーは、その構成に基づいて自動応答を介してルーティングされます。

リソースをクリーンアップする

Communication Services サブスクリプションをクリーンアップして解除する場合は、リソースまたはリソース グループを削除できます。 リソース グループを削除すると、それに関連付けられている他のリソースも削除されます。 詳細については、リソースのクリーンアップに関する記事を参照してください。

次の手順

詳細については、次の記事を参照してください。