次の方法で共有


クイック スタート: Azure Communication Services と Microsoft Teams の間で相互運用機能通話を行う

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

  1. Teams テナントでの Azure Communication Services リソースのフェデレーションを有効にします。
  2. Teams ユーザーの ID を取得します。
  3. Azure Communication Services Calling SDK を使用して通話を開始します。

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 モジュールに接続する

モジュールがインストールされ準備ができたら、次のコマンドを使って MicrosoftTeams モジュールに接続できます。 ログインするための対話型ウィンドウが表示されます。 使用するユーザー アカウントには、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

サンプル コード

このクイックスタートの最終的なコードは GitHub にあります。

前提条件

通話 UI コントロールを追加する

index.html のコードを次のスニペットに置き換えます。 ID を指定して、Teams ユーザーに通話します。

  • テキスト ボックスは、通話する予定の Teams ユーザー ID を入力するために使用します。 1 対 1 の通話の場合は 1 つの ID を、グループ通話の場合は複数の ID を入力します
<!DOCTYPE html>
<html>
<head>
    <title>Communication Client - Calling Sample</title>
</head>
<body>
    <h4>Azure Communication Services</h4>
    <h1>Teams interop calling quickstart</h1>
    <input id="teams-id-input" type="text" placeholder="Teams ID(s)"
           style="margin-bottom:1em; width: 300px;" />
    <p>Call state <span style="font-weight: bold" id="call-state">-</span></p>
    <p><span style="font-weight: bold" id="recording-state"></span></p>
    <div>
        <button id="start-call-button" type="button" disabled="false">
            Start Call
        </button>
        <button id="hang-up-button" type="button" disabled="true">
            Hang Up
        </button>
    </div>
    <br>
    <div>
        <button id="mute-button" type="button" disabled="true"> Mute </button>
        <button id="unmute-button" type="button" disabled="true"> Unmute </button>
    </div>
    <br>
    <div>
        <button id="start-video-button" type="button" disabled="true">Start Video</button>
        <button id="stop-video-button" type="button" disabled="true">Stop Video</button>
    </div>
    <br>
    <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>

client.js ファイルの内容を次のスニペットに置き換えます。

const { CallClient, Features, 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 call;
let callAgent;
let localVideoStream;
let localVideoStreamRenderer;
// UI widgets
const teamsIdInput = document.getElementById('teams-id-input');
const hangUpButton = document.getElementById('hang-up-button');
const startInteropCallButton = document.getElementById('start-call-button');
const muteButton = document.getElementById('mute-button')
const unmuteButton = document.getElementById('unmute-button')
const callStateElement = document.getElementById('call-state');
const recordingStateElement = document.getElementById('recording-state');
const startVideoButton = document.getElementById('start-video-button');
const stopVideoButton = document.getElementById('stop-video-button');
const remoteVideoContainer = document.getElementById('remoteVideoContainer');
const localVideoContainer = document.getElementById('localVideoContainer');
/**
 * Create an instance of CallClient. Initialize a CallAgent instance with a CommunicationUserCredential via created CallClient. CallAgent enables us to make outgoing calls. 
 * You can then use the CallClient.getDeviceManager() API instance to get the DeviceManager.
 */
async function init() {
    try {
        const callClient = new CallClient();
        const tokenCredential = new AzureCommunicationTokenCredential("<USER ACCESS TOKEN>");
        callAgent = await callClient.createCallAgent(tokenCredential, { displayName: 'ACS user' });
        // Set up a camera device to use.
        deviceManager = await callClient.getDeviceManager();
        await deviceManager.askDevicePermission({ video: true });
        await deviceManager.askDevicePermission({ audio: true });
        startInteropCallButton.disabled = false;
    } catch(error) {
        console.error(error);
    }
}
init();

muteButton.addEventListener("click", async () => {
    try {
        await call.mute();
    } catch (error) {
        console.error(error)
    }
})

unmuteButton.onclick = async () => {
    try {
        await call.unmute();
    } catch (error) {
        console.error(error)
    }
}

startInteropCallButton.addEventListener("click", async () => {
    if (!teamsIdInput.value) {
        return;
    }
    try {
        const localVideoStream = await createLocalVideoStream();
        const videoOptions = localVideoStream ? { localVideoStreams: [localVideoStream] } : undefined;
        const participants = teamsIdInput.value.split(',').map(id => {
            const participantId = id.replace(' ', '');
            return {
                microsoftTeamsUserId: `${participantId}`
            };
        })
        call = callAgent.startCall(participants, {videoOptions: videoOptions})
        // Subscribe to the call's properties and events.
        subscribeToCall(call);
    } catch (error) {
        console.error(error);
    }

    call.feature(Features.Recording).on('isRecordingActiveChanged', () => {
        if (call.feature(Features.Recording).isRecordingActive) {
            recordingStateElement.innerText = "This call is being recorded";
        }
        else {
            recordingStateElement.innerText = "";
        }
    });
});

// Subscribe to a call obj.
// Listen for property changes and collection updates.
subscribeToCall = (call) => {
    try {
        // Inspect the initial call.id value.
        console.log(`Call Id: ${call.id}`);
        //Subscribe 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}`);
            callStateElement.innerText = call.state;
            if(call.state === 'Connected') {
                startInteropCallButton.disabled = true;
                hangUpButton.disabled = false;
                startVideoButton.disabled = false;
                stopVideoButton.disabled = false;
                muteButton.disabled = false;
                unmuteButton.disabled = false;
            } else if (call.state === 'Disconnected') {
                startInteropCallButton.disabled = false;
                hangUpButton.disabled = true;
                startVideoButton.disabled = true;
                stopVideoButton.disabled = true;
                muteButton.disabled = true;
                unmuteButton.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 updates.
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 remoteParticipant 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;
                remoteVideoContainer.hidden = true;
            }
        }
    });
    // 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
hangUpButton.addEventListener("click", async () => {
    // end call
    await call.hangUp();
});

Teams ユーザー ID を取得する

Teams ユーザー ID は Graph API を使用して取得することができます (Graph のドキュメントの中で詳しく説明されています)。

https://graph.microsoft.com/v1.0/me

結果では "id" フィールドが取得されます。

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

または、Azure portal の [ユーザー] タブに同じ ID が表示されます。 Azure portal でのユーザー オブジェクト ID のスクリーンショット。

コードの実行

ローカルの Web サーバーにアプリケーション ホストをバンドルするには、次のコマンドを実行します。

npx webpack serve --config webpack.config.js

ブラウザーを開き、http://localhost:8080/. に移動します 次の画面が表示されます。

完成した JavaScript アプリケーションのスクリーンショット。

Teams ID をテキスト ボックス内に挿入し (複数の場合はコンマで区切ります)、[Start Call] (通話を開始) を押して、Communication Services アプリケーション内から通話を開始します。

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

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

次のステップ

Call Automation を使用した高度なフローについては、次の記事を参照してください。

詳細については、次の記事をご覧ください。