【実装編】ACSで通話・ビデオ・電話・録音を組み込む|WebRTC・Teams相互運用・PSTNを実務目線で解説

Azure

自社アプリに通話・ビデオ通話を組み込みたい。あるいは「予約が入ったら本物の電話番号から発信したい」「サポート通話を録音してあとで確認したい」。そういうリアルタイム通信の要件が出てきたとき、多くの人がまず身構えます。音声や映像を遅延なくやり取りする仕組みは、自前で組もうとすると本当に骨が折れるからです。

私自身、以前に医療現場向けのリアルタイム音声翻訳アプリを一人で本番構築したことがあり、通話・音声をプロダクションで扱う難しさは体で分かっているつもりです。ネットワークがちょっと詰まっただけで音が途切れる、遅延が積み重なって会話にならない——リアルタイム通信は「動いた」から「実用に耐える」までの距離がとにかく長い。だからこそ、その土台をAzureが肩代わりしてくれるAzure Communication Services(ACS)の価値は大きいと感じています。

「チャットやSMSは分かった。でもビデオ通話や本物の電話への発信って、ACSでどこまでできるの?録音って勝手にやっていいの?」

この記事は、そういうリアルタイム通信の実装に絞ってACSを解説します。具体的には、WebRTCでの音声・ビデオ通話ACSユーザーがTeams会議に参加するTeams相互運用電話番号を取得して本物の電話にかけるPSTN通話通話やイベントをEvent Gridで受け取る連携、そして通話録音とそのコンプライアンスまで。公式のMicrosoft Learnは正確ですが機械翻訳まじりで手が止まりがちなので、つまずく所を私の実務目線で噛み砕いていきます。

なお、そもそもACSが何者で、チャットやSMSやメールをどう送るのかという全体像があやふやな方は、先に Azure Communication Servicesとは?通話・チャット・SMS・メールをAPIで組み込む方法 に目を通しておいてください。本記事はその親記事の子記事で、なかでも「リアルタイム通信」だけを深掘りする回という位置づけです。基本のチャット・SMS送信は親記事に任せます。

音声・ビデオ通話(WebRTC)を実装する

まずは本丸、ブラウザやアプリの中で2人がリアルタイムに音声・映像をやり取りする通話から。ここでACSが使っているのがWebRTCという技術です。

WebRTCをざっくり言うと、ブラウザ同士が直接、音声と映像を送り合うための標準規格です。ふつうのWeb通信はブラウザ→サーバー→ブラウザと必ずサーバーを経由しますが、通話は「できるだけ端末同士で直接つないで遅延を減らしたい」。そのための仕組みがブラウザに標準搭載されていて、ZoomのWeb版もGoogle MeetもこのWebRTCの上で動いています。ACSのCalling SDKは、この扱いが難しいWebRTCを、私たちが呼びやすいAPIの形に包んでくれたものだと思ってください。

通話が成立するまでの登場人物

コードに入る前に、通話が成立するまでの「役者」を整理しておくと迷いません。電話の交換手にたとえると分かりやすいです。

ユーザーID(Identity)は、通話に参加する一人ひとりの「電話の契約者番号」にあたります。ACSでは、Aさん・BさんそれぞれにこのユーザーIDを発行します。次にアクセストークン。これは「その番号の持ち主で間違いない」と証明する一時的な鍵で、有効期限があります。そしてCallClient / CallAgentが、実際に電話をかけたり受けたりする「受話器と交換手」の役割です。CallAgentから相手のユーザーIDへ発信すると、WebRTCがつながって音声・映像が流れはじめます。

ここで実務上とても大事なのが、ユーザーIDとアクセストークンの発行は、必ずサーバー側で行うという点です。トークンを作るにはACSの接続文字列(管理者権限に相当する秘密の鍵)が要ります。これをフロントエンド(ブラウザのJavaScript)に置いてしまうと、鍵が丸見えになり、誰でもあなたのACSリソースで通話・SMS・電話発信をし放題になります。つまり課金される穴を自分で開けることになる。ここは初心者が最もやりがちで、最も痛い事故なので強調しておきます。

正しい流れはこうです。フロントが自前のバックエンドに「トークンちょうだい」とリクエスト → バックエンドが接続文字列を使ってユーザーIDとトークンを発行して返す → フロントはそのトークンだけを使って通話する。接続文字列はサーバーの中だけに閉じ込めておくのが鉄則です。

最小構成のコードで流れをつかむ

まずサーバー側で、Aさん用のユーザーIDとアクセストークンを発行します(Node.jsの例)。voipというスコープを付けると、そのトークンで通話ができるようになります。

// server.js(バックエンド。接続文字列はここだけに置く)
const { CommunicationIdentityClient } = require("@azure/communication-identity");

const client = new CommunicationIdentityClient(process.env.ACS_CONNECTION_STRING);

// ユーザーIDと、通話用スコープ付きトークンをまとめて発行
const { user, token } = await client.createUserAndToken(["voip"]);
// このtokenだけをフロントに返す(接続文字列は絶対に返さない)

受け取ったトークンを使って、フロント側で発信します。ブラウザ用の@azure/communication-callingを使う例です。

// front.js(ブラウザ側)
const { CallClient } = require("@azure/communication-calling");
const { AzureCommunicationTokenCredential } = require("@azure/communication-common");

const callClient = new CallClient();
const credential = new AzureCommunicationTokenCredential(token); // ↑で受け取ったトークン
const callAgent = await callClient.createCallAgent(credential);

// 相手のユーザーID(communicationUserId)へ発信
const call = callAgent.startCall([{ communicationUserId: "8:acs:xxxx..." }]);

各パーツの役割を補足します。

  • CallClient:通話機能の入り口。ブラウザのマイク・カメラを扱う土台になります。
  • TokenCredential:サーバーから受け取ったトークンを包み、認証を担う部分。トークンは期限切れするので、実運用では期限が来る前に新しいトークンを取りに行く「リフレッシュ」を仕込みます。
  • CallAgent:発信・着信を実際に行う交換手。startCallで発信、着信はincomingCallイベントで受けてaccept()します。

映像を出したいときは、ローカルのカメラ映像を取得して通話に添える操作を追加します。DeviceManagerでカメラを選び、LocalVideoStreamを作ってstartCallのオプションに渡す、という流れです。ここで「音声は聞こえるのに自分の映像が出ない」というのは、たいていブラウザのカメラ権限をユーザーが許可していないか、LocalVideoStreamを通話に渡し忘れているかのどちらかです。

本番で効いてくる「遅延・帯域・弱回線」の勘所

ここからが、公式クイックスタートでは軽くしか触れられないけれど本番でいちばん差が出るところです。私が医療向けの音声アプリで痛感したのは、「開発中のオフィスWi-Fiでは完璧に動くのに、現場のLTEや古い院内ネットワークだと途端に破綻する」という現実でした。リアルタイム通信は、いちばん回線が悪いユーザーに品質が引きずられるのです。

押さえておきたいポイントを、実務目線で挙げておきます。

  • 遅延(レイテンシ):会話がかみ合うには片道150ミリ秒くらいが目安で、これを超えると「相手の返事を待ってしまう」ぎこちなさが出ます。地球の裏側とつなぐような構成は、それだけで不利になります。
  • 帯域(バンド幅):音声だけなら軽いですが、ビデオは解像度を上げるほど帯域を食います。ACSのSDKは回線状況に応じて自動的に画質を落として音声を守る挙動をするので、「弱回線では映像がカクつくが声は途切れない」のはむしろ正常な設計です。
  • 弱回線対策:まず映像より音声を優先する設計にする。会議アプリで「回線が不安定です、ビデオをオフにしますか」と出るのはこのためです。
  • 品質の可視化:ACSには通話品質を測る仕組み(Call Diagnosticsやログ)があり、「誰の回線が悪かったのか」を後から追えます。本番では”感覚”ではなくログで判断できるようにしておくと、クレーム対応がまるで違います。

要は、通話機能は「つながる」ことより「悪い回線でも会話が成立し続ける」ことを設計目標に置くべきで、そこはACSがかなり肩代わりしてくれる——というのが実装してみた率直な感想です。

Teams相互運用(ACSユーザーをTeams会議に参加させる)

次は、多くの企業で刺さる機能です。Teams相互運用(Teams Interop)——ざっくり言うと、自分のアプリで作った通話ユーザーを、Microsoft Teamsの会議にそのまま参加させられる仕組みです。

これがなぜ強力なのか。想像してみてください。あなたの会社が予約制のオンライン診療アプリを作っているとします。患者さんは自作アプリのボタンひとつで通話に入り、医師側はいつも使っているTeamsからその会議に入る——患者にTeamsアカウントを取らせる必要も、アプリを入れさせる必要もない。この「片方は自作アプリ、片方はTeamsでOK」を実現するのがTeams相互運用です。外部の顧客・患者・取引先を巻き込むシナリオで、導入のハードルを劇的に下げてくれます。

「2つの参加のしかた」を混同しない

ここで最初につまずくのが、Teams相互運用には性格の違う2つのパターンがある点です。ここを分けて理解しておかないと、公式ドキュメントを読んでも混乱します。

1つ目がACS Identity で参加するパターン。これはさっきの通話と同じ、ACSが発行した匿名的なユーザーIDで会議に入る方式です。患者・顧客のような「Teamsアカウントを持っていない外部の人」向けで、会議参加リンク(URL)さえあれば入れるのが特徴。オンライン診療や外部サポートは、たいていこちらです。

2つ目がTeams Identity(M365ユーザー)で参加するパターン。こちらは「その人自身のTeamsアカウント(Entra IDのユーザー)」としてACSのSDK経由で会議に入る方式で、社内向け・カスタムTeamsクライアントを作りたいときに使います。認証にEntra IDが絡むぶん構成は重くなります。

初心者がまず触るべきは1つ目のACS Identityパターンです。実装イメージは驚くほどシンプルで、通常の通話コードの「発信先」を相手ユーザーIDから会議参加リンクに差し替えるだけに近い。

// Teams会議のURLを渡して参加する
const locator = { meetingLink: "https://teams.microsoft.com/l/meetup-join/..." };
const call = callAgent.join(locator);

このmeetingLinkは、Teamsの会議を作ったときに発行される「会議に参加」リンクそのものです。会議リンクを取得する部分は、TeamsやGraph API側で会議を作成する処理が別途必要になりますが、ACSアプリから見れば「URLを渡してjoinする」という一点に集約されるのがポイントです。

「なるほど、外部の人はアプリからURLで入って、社内はTeamsでそのまま入る。両者が同じ会議で顔を合わせられるわけか」

実務での注意点も添えておきます。相互運用では、外部参加者(ACSユーザー)に表示名を必ず設定すること。無名のまま入るとTeams側の主催者から見て「誰これ?」となり、ロビーで止められて会議に入れないことがあります。また、会議の設定によっては外部参加者がロビーで承認待ちになる仕様なので、「joinしたのに入れない」ときはエラーではなく承認待ちを疑ってください。ここは私も一度ハマって、コードを疑い続けた末に「主催者が承認していないだけ」だったことがあります。

電話番号の取得とPSTN通話(本物の電話にかける)

ここまではアプリ内・ブラウザ内での通話でした。次は一段リアルな世界、PSTN通話です。PSTNは「公衆交換電話網」、要するに私たちが普段使っている電話回線のこと。ACSで電話番号を取得すれば、アプリから本物のスマホ・固定電話に発信したり、逆に着信を受けたりできます。

使いどころは明確です。「予約確定を自動音声で電話連絡する」「アプリ内の通話ボタンから、ネット回線を持たない相手の携帯にかける」「専用の受付番号を用意して着信を自動振り分けする」——ネットの外にいる人とつながる必要があるとき、PSTNの出番です。

番号取得は「審査・規制がある」と最初に知っておく

手順自体は、AzureポータルのACSリソース →「電話番号」→「取得」から、国・番号の種類(トールフリー/地理的番号)・用途(発信のみ/着信のみ/両方)・SMS対応の有無を選んでいくだけです。画面は素直で難しくありません。

ただし、ここが通話・ビデオと決定的に違う点なので強調します。電話番号の取得には国ごとの規制・審査がある。電話番号は各国の通信規制の対象で、「誰が何のために使うのか」を申請・確認するプロセスが挟まります。国によっては会社の住所確認などの書類提出が必要で、番号がすぐには使えるようにならないことがあります。「クイックスタート通りにやったのに番号が取れない/有効化されない」のは、多くの場合バグではなく規制上の審査待ちです。日本の番号を扱いたい場合は特に、対応状況と条件を公式ドキュメントで最新の情報を確認してから設計に組み込んでください。

発信のコード自体は、通話とほとんど同じ形です。発信先がユーザーIDではなく電話番号になり、自分の発信元番号を指定する点だけが違います。

// 取得した番号を発信元に、相手の電話番号へPSTN発信
const call = callAgent.startCall(
  [{ phoneNumber: "+81xxxxxxxxxx" }],           // 相手の電話番号(E.164形式)
  { alternateCallerId: { phonenumber: "+81yyyyyyyyyy" } } // 自分の取得済み番号
);

電話番号はE.164形式+と国番号から始まる国際表記。日本なら先頭の0を外して+81を付ける)で指定するのが約束事です。ここを国内表記のまま090...で渡すと発信に失敗するので、番号の正規化は入力時に済ませておくのが定石です。なお、PSTN通話は通話ごとに従量課金が発生します。アプリ内通話(WebRTC)と違って「かけた分だけお金がかかる」ので、テスト時のかけっぱなしにも注意してください。

マネージドID・Event Grid連携(通話やSMSのイベントを受け取る)

ここまでで「かける・つなぐ」はできました。次は「起きたことを知る」仕組み、Event Grid連携です。

ACSは通話やメッセージに関して、いろいろな出来事をイベントとして発行します。「通話が始まった」「通話が終わった」「SMSを受信した」「配信レポートが返ってきた」——こうした出来事を、ACSがEvent Gridという”イベントの配達係”に投げ込み、Event Gridがあなたの用意した受け取り先(関数やWebhook)に配達してくれる、という流れです。

たとえるなら、ACSは「郵便を出す人」、Event Gridは「宛先ごとに手紙を仕分けて配る郵便局」です。あなたは「SMSを受信したという手紙が来たら、この関数に届けて」と郵便局に登録しておくだけ。以後は該当イベントが起きるたび、自動でその関数が呼ばれます。ポーリング(定期的に「何か起きた?」と問い合わせる)で自作しなくていいのが大きな利点です。

典型的な使い道はこんな具合です。

  • SMS受信イベント → Azure Functionsを起動し、内容を解析して自動返信する(双方向SMSボットの土台)。
  • 通話終了イベント → 通話時間や参加者を記録し、課金・分析用のデータとして保存する。
  • 録音完了イベント → 録音ファイルの準備ができたことを受け取り、ストレージへの保存処理へ回す(後述の録音とセットでよく使う)。

設定の勘所と「マネージドID」の役割

設定はポータルでACSリソース →「Events」→「イベントサブスクリプションの作成」から行います。受け取りたいイベントの種類(例:SMS Received)を選び、配達先(Azure FunctionsやWebhookのURL)を指定するだけです。

ここで一緒に覚えておきたいのがマネージドIDです。イベントを受けて次の処理(たとえばBlob Storageへ録音を保存する)をするとき、その処理は他のAzureリソースにアクセスする権限を持っていなければなりません。この認証を、パスワードやキーをコードに埋め込まずに済ませてくれるのがマネージドIDです。ACSでも、接続文字列(キー)の代わりにマネージドIDで認証する構成が推奨されていて、「秘密の文字列を持ち歩かない」ぶん安全で運用がラクになります。マネージドID自体の考え方は別記事で深掘りしているので、下の「あわせて読みたい」から参照してください。

つまずきポイントを一つ。Event Gridを初めて使うサブスクリプションでは、「Microsoft.EventGrid」リソースプロバイダーの登録が必要なことがあります。「イベントサブスクリプションが作れない/エラーになる」ときは、まずここの登録を疑うと早いです。

通話録音とコンプライアンス

最後は通話録音(Call Recording)です。技術的には簡単に始められる一方、いちばん法務・コンプライアンスの配慮が要る機能でもあります。ここは実装より「守るべき前提」の話が中心になります。

録音の始め方(技術面)

録音はCall Recording APIをサーバー側から呼ぶ形で行います。進行中の通話を識別する情報を使って「録音開始」を叩き、終わったら「停止」する。録音ファイルが用意できると、先ほどのEvent Gridに録音完了イベントが飛んでくるので、それを合図にファイルを取得して自社のストレージへ保存する、という流れが定番です。

// サーバー側で通話録音を開始する(イメージ)
const recording = await callRecording.start({
  callLocator: { id: serverCallId, kind: "serverCallLocator" }
});
// → 停止後、録音完了イベントがEvent Gridに届く → ファイルを取得して保存

ポイントは、録音の制御は必ずサーバー側で行うこと。通話と同じく、録音の開始・停止をフロントに任せると悪用の余地が生まれます。録音は「誰が・いつ・どの通話を」開始したかをサーバーで管理下に置くべき機能です。

実務でいちばん大事な「同意・所在地・保持」

ここが本題です。録音は「動かせる」ことと「やっていい」ことがまったく別物で、私が医療系のリアルタイム音声を扱ったときも、技術よりこの3点の設計に時間を使いました。

1. 同意(consent)。多くの国・地域で、通話録音には参加者への通知や同意が求められます。「この通話は録音されます」のアナウンスを冒頭で流す、アプリに明示する、といった配慮が要ります。ACSには録音中であることを参加者に伝える仕組みもあるので、それを黙って切らないこと。無断録音は技術的に可能でも、法的・倫理的にアウトになり得ます。

2. データ所在地(data residency)。録音には声という強い個人情報が含まれます。医療・金融のような分野では「そのデータが物理的にどの国のデータセンターに置かれるか」が規制で決まっていることがあります。ACSはデータの保存地域(ジオ)を意識して構成できるので、リソースを作る段階で正しい地域を選ぶことが重要です。あとから移すのは大変なので、ここは最初に決めるべき設計事項です。

3. 保持(retention)。ACSが録音ファイルを一時的に保持する期間は限られています。期限を過ぎると取得できなくなるので、必要なら録音完了イベントを合図に速やかに自社ストレージへ退避し、そこで「何日保存して、いつ削除するか」という保持ポリシーを自分で管理します。個人情報は「必要な期間だけ持ち、不要になったら消す」が原則。録音を無期限にため込むのは、それ自体がリスクです。

「録音は”実装できるか”より”どう扱うか”が本番なんだな。同意・保存地域・保持期間、この3つは最初に決めておく」

まとめ

この記事では、ACSのリアルタイム通信を、実装の順につまずき所を交えて見てきました。要点を振り返ります。

  • 音声・ビデオ通話(WebRTC):Calling SDKで2クライアント間をつなぐ。ユーザーIDとトークンの発行は必ずサーバー側で。本番は「弱回線でも会話が成立し続ける」ことを設計目標に。
  • Teams相互運用:外部の人は自作アプリからURLで、社内はTeamsで、同じ会議に同席できる。まずは会議リンクをjoinするACS Identityパターンから。表示名とロビー承認に注意。
  • PSTN通話:番号を取得すれば本物の電話に発信・着信できる。ただし国ごとの規制・審査があり即使えないことがある。番号はE.164形式・従量課金。
  • Event Grid連携:通話開始・SMS受信・録音完了などのイベントを配達係が届けてくれる。認証はマネージドIDでキーレスにするのが安全。
  • 通話録音:制御はサーバー側で。技術より同意・データ所在地・保持の3点が本番の肝。

ここまで押さえれば、自社アプリにビデオ通話や電話を”本番品質”で組み込むための地図はそろいました。あとは、いちばん回線の悪いユーザーを想定しながら、同意と保持のルールを最初に決めて設計に落とし込むだけ。リアルタイム通信は難しく見えますが、土台の面倒な部分はACSがかなり引き受けてくれます。私が一人で医療向けの音声アプリを本番まで持っていけたのも、こうしたマネージドな通信基盤があってこそでした。あなたのアプリでも、ぜひ通話・ビデオ・電話を実戦投入してみてください。

あわせて読みたい:

タイトルとURLをコピーしました