このクイックスタートでは、Azure Communication Services ユーザーから Teams ユーザーへの通話を開始する方法について説明します。 これを実現するには、次の手順を実行します。
- Teams テナントでの Azure Communication Services リソースのフェデレーションを有効にします。
- Teams ユーザーの ID を取得します。
- 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 が表示されます。
コードの実行
ローカルの Web サーバーにアプリケーション ホストをバンドルするには、次のコマンドを実行します。
npx webpack serve --config webpack.config.js
ブラウザーを開き、http://localhost:8080/. に移動します 次の画面が表示されます。
Teams ID をテキスト ボックス内に挿入し (複数の場合はコンマで区切ります)、[Start Call] (通話を開始) を押して、Communication Services アプリケーション内から通話を開始します。
リソースをクリーンアップする
Communication Services サブスクリプションをクリーンアップして解除する場合は、リソースまたはリソース グループを削除できます。 リソース グループを削除すると、それに関連付けられている他のリソースも削除されます。 詳細については、リソースのクリーンアップに関する記事を参照してください。
次のステップ
Call Automation を使用した高度なフローについては、次の記事を参照してください。
詳細については、次の記事をご覧ください。
- 通話のヒーロー サンプルを確認する
- UI ライブラリを使ってみる
- Calling SDK の機能について確認する
- 通話のしくみの詳細について確認する