Voice Liveのアバター映像をWebSocketで受けてみた話
Voice Live APIは、音声で会話するエージェントを作るためのAPIで、アバターを使うと会話に合わせて話す人物の映像も出せます。このアバターは、これまで映像をWebRTCで受け取るのが前提でした。セッションの設定のあとSDPとICEをやり取りして、映像と音声のメディアストリームを張る必要があります。この仕組みは以前の記事で詳しく書きました。
現在は、もう1つの受け取り方としてWebSocketで映像を受ける方法があります。会話に使っているWebSocketに、そのまま映像も流れてきて、SDPもICEも要りません。本記事では、WebSocketで映像を受ける方法と、実際に受けてみてわかった映像データの形式、WebRTCとの使い分けについて紹介します。
本記事はVoice Live機能解説シリーズの第7回です。
シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)
-
Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
SDKに型が無い機能の分類 -
Voice Live APIで会話の「間」を扱う
先に話す・言いよどみを待つ・割り込む・待ち時間に話す -
Voice Live の音声入力の品質を上げる
ノイズ抑制・エコー除去・Live-Reference AEC の選び方 -
Voice LiveでMCPサーバのツールを使う
function callingとの違いと並列ツール呼び出し -
Voice LiveでFoundryのエージェントをツールとして呼ぶ
chat-supervisorパターンの使い方と前段モデルの選び方 -
30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
Personal Voice の申請から実装まで -
Voice Live APIのアバターを使い分ける
標準・カスタム写真・カスタムビデオの設定と実装 - Voice Liveのアバター映像をWebSocketで受ける(本記事)
WebRTCとの違いと映像の形式
番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ
SDPを1行ずつ突き合わせた記録
検証に使ったコードはこちらです。
本記事は2026年10月時点の情報です。対象はVoice Live API 2026-07-15(GA)と.NET SDK(Azure.AI.VoiceLive)1.2.0です。以下の観測はすべて2026-07-15で、Agent・Model両方のセッションで確認しています。
概要
3つの伝送方式
先に、Voice Liveで「WebSocket」「WebRTC」と言ったときに指すものを整理しておきます。混同しやすいのですが、3つの方式があります。
| 方式 | 会話の音声 | アバターの映像 | 接続先 | 状態(07-15) |
|---|---|---|---|---|
| ① 標準のWebSocket | WebSocketのイベント | ―(アバターなし) | /voice-live/realtime |
GA |
| ② WebRTCアバター | WebSocketのイベント | WebRTC(映像+音声) |
/voice-live/realtime+SDP/ICE |
GA |
| ③ WebSocketアバター(本記事) | ― | 同じWebSocket(映像+音声) | /voice-live/realtime |
GA |
| 参考:WebRTC音声接続 | WebRTC(RTP) | ― | /voice-live/realtime/calls |
preview |
②と③が本記事の比較対象です。最後のWebRTC音声接続は、会話の音声そのものをWebRTCで運ぶ別の仕組みです。名前が似ているので注意してください。
WebRTC音声接続のドキュメントには、2026年11月時点で「サイドバンド制御(WebRTCと並行して開くWebSocketの制御チャネル)ではアバターの設定に対応していない」と書かれています。アバターを使うなら、②か③を選ぶことになります。
Avatar configurations are currently unsupported with side-band control.
― Voice Live API with WebRTC
実装
WebSocketアバターの設定
アバターの設定にoutput_protocol: "websocket"を加えるだけです。
{
"type": "session.update",
"session": {
"avatar": {
"character": "lisa",
"style": "casual-sitting",
"customized": false,
"output_protocol": "websocket",
"video": {
"bitrate": 2000000,
"codec": "h264",
"crop": { "top_left": [560, 0], "bottom_right": [1360, 1080] },
"resolution": { "width": 1920, "height": 1080 },
"background": { "color": "#FFFFFFFF" }
}
}
}
}
WebRTCのときに必要だったsession.avatar.connect(クライアントのSDPを送る)は送らなくてよく、映像はresponse.video.deltaイベントとして、会話と同じWebSocketに届きます。
{
"type": "response.video.delta",
"delta": "<base64 でエンコードされたフレーム>",
"codec": "h264"
}
deltaはbase64でエンコードされた映像のバイト列です。
SDK 1.2.0で受ける
SDK 1.2.0にはServerEventResponseVideoDeltaの型があり、GetUpdatesAsyncの振り分けに1つ分岐を足すだけで受け取れます。サンプルでは、APIを直接叩く自作クライアント(Core)とSDKの両方から、表示側はまったく同じコード(後述のWebSocketAvatarVideoStreamer)に渡しています。違うのは受け取り口だけです。
await foreach (SessionUpdate update in session.GetUpdatesAsync(cancellationToken))
{
switch (update)
{
case ServerEventResponseVideoDelta videoDelta:
// 音声の Delta と違い、映像の Delta は base64 文字列のまま届く
byte[] frame = Convert.FromBase64String(videoDelta.Delta);
webSocketVideo.WriteFrame(frame);
break;
// ...
}
}
映像と音声でDeltaの型が揃っていません。音声のSessionUpdateResponseAudioDelta.Deltaはデコード済みのBinaryDataですが、映像のServerEventResponseVideoDelta.Deltaはbase64のstringのままです。XMLドキュメントはどちらも"base64-encoded"と書いているので、説明を読んでも区別できません。
型名にSessionUpdateの接頭辞が付いていない(ServerEvent...)ので見落としやすいのですが、SessionUpdateを継承しているのでGetUpdatesAsyncでそのまま拾えます。
動作確認
届くデータの形式
最初はcodec: h264という表記から、H.264の生ストリーム(Annex-B)が細切れで届くことを想定していたのですが、実際にデコードした先頭のバイトを見るとそうではありませんでした。
1つ目のフレーム : 00 00 00 18 66 74 79 70 ... → "ftyp"
2つ目以降 : 00 00 xx xx 6D 6F 6F 66 ... → "moof"
届いていたのはfragmented MP4(fMP4)でした。最初に初期化セグメント(ftyp+moov)が届き、そのあとは断片(moof+mdat)が続きます。
そのため、H.264の生ストリームを前提にSPS/PPSを補うような処理(WebRTCのときに必要だったもの)は要らず、受け取ったバイトをそのままMP4として扱えるプレイヤーに流せば再生できます。便利ですね。
音声のトラック
もう1つ、実際に受けてみてわかったことがあります。初期化セグメントの中を見ると、トラックが2本ありました。
| トラック | コーデック |
|---|---|
| 映像 | H.264(avc1) |
| 音声 | AAC(mp4a) |
そしてWebSocketアバターのときは、response.audio.delta(通常のPCM音声)は一度も届きませんでした(Agent・Modelどちらのセッションでも)。response.audio.doneやresponse.audio_transcript.doneは届くので、応答の区切りや文字起こしはいつもどおり取れます。
つまりWebSocketアバターは、映像と音声を1本のfMP4にまとめて、会話と同じTCPの上で届ける方式のようです。口の動きと声は同じストリームの中で揃っているので、クライアント側で同期を取る必要がありません。
WebSocketアバターのときにPCMの音声も鳴らす処理を残していると、将来サーバが両方送るようになった場合に二重に再生されます。サンプルでは、アバターを使っているときはresponse.audio.deltaを再生しないようにしています。
話し始め・話し終わりのイベント
WebSocketアバターでは、アバターが話し始めた・待機に戻ったことを示すイベントも届きます。
| イベント | SDKの型 | 意味 |
|---|---|---|
session.avatar.switch_to_speaking |
ServerEventSessionAvatarSwitchToSpeaking |
話し始めた(TurnIdに応答メッセージのID) |
session.avatar.switch_to_idle |
ServerEventSessionAvatarSwitchToIdle |
待機に戻った |
最初のswitch_to_idleは、まだ応答が無いのでTurnIdが空で届きます。
映像の表示
Voice Liveがやるのは、フレームをWebSocketで届けるところまでです。表示はアプリ側で行います。サンプルではffplayを起動し、受け取ったバイトを標準入力にそのまま書き込んでいます。
// fMP4 を標準入力から読む。低遅延のフラグを付けて再生ウィンドウを開く
const string args = "-hide_banner -loglevel warning -f mp4 " +
"-fflags nobuffer -flags low_delay -framedrop " +
"-window_title \"Avatar (WebSocket)\" -i pipe:0";
ffplayProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "ffplay",
Arguments = args,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardError = true
}
};
ffplayProcess.Start();
ffplayInput = ffplayProcess.StandardInput.BaseStream;
// ...
public void WriteFrame(byte[] frameData)
{
ffplayInput.Write(frameData, 0, frameData.Length);
ffplayInput.Flush();
}
ffplayは映像と音声の両方のトラックを再生するので、これだけで口の動きと声が揃って出ます。
ブラウザで表示するならMedia Source Extensions(MSE)にfMP4を流す、ゲームエンジンならその動画再生の仕組みに渡す、というように、fMP4を扱える再生の仕組みがあれば何でも構いません。
WebRTCとWebSocketの使い分け
| 観点 | WebRTCアバター | WebSocketアバター |
|---|---|---|
| 接続の手順 |
session.updatedのICEサーバで候補を集め、session.avatar.connectでSDPを交換 |
設定1行。交換なし |
| 使うポート・プロトコル | UDP(WebRTCのメディア) | 会話と同じWebSocket(TCP 443) |
| 届く形 | H.264映像+Opus音声のRTP | fMP4(H.264+AAC) |
| 表示に必要なもの | WebRTCのピア実装(ブラウザなら標準、.NETならSIPSorceryなど) | fMP4を再生できるもの(ffplay、MSEなど) |
| ネットワークが悪いとき | 欠落を許して遅延を抑える | TCPの再送を待つので遅延が伸びうる |
| 閉域・プロキシ・厳しいファイアウォール | UDPが通らないとTURNが要る | WebSocket(TCP 443)が通れば使える |
| どちらがいいというよりも用途に応じて切り替えるスタイルが正しそうです。選び方の目安は次のとおりです。 |
- ブラウザで動かす場合や、低遅延を優先する場合はWebRTCが向いていると思います。ブラウザにはWebRTCが標準で入っているので実装も素直です
- ネイティブアプリやサーバで受ける場合、ネットワークの制約が厳しい場合、実装を単純にしたい場合はWebSocketが向いていると思います。UDP・SDP・ICEを扱わなくて済みます
WebRTCを.NETで自前で受ける場合、SIPSorceryでピアを立て、H.264のSPS/PPSを補って再構成する、といった処理が必要でした(前述の過去記事参照)。WebSocketならそれがすべて要らなくなり、届いたバイトをプレイヤーに流すだけになります。実際のところWebRTCの実装はかなり苦労しました。公式ドキュメントの情報が少なくそもそも疎通に必要なSDP,ICE等の設定方法がわからないところから試行錯誤が必要でした。
その点WebSocketで完結する今回の方式はかなり楽でした。
一方で、TCPの上で運ぶ以上、パケットが落ちたときは再送を待つので、遅延が一時的に伸びることがあります。映像がカクついても低遅延を保ちたい用途なら、WebRTCのほうが向いています。
まとめ
-
output_protocol: "websocket"を設定すると、アバターの映像が会話と同じWebSocketにresponse.video.deltaとして届きます。 - WebSocketアバターではSDPとICEのやり取りは要りません。
- 届く映像はH.264の生ストリームではなくfMP4でした。
- 音声もAACのトラックとして同じfMP4に入っていて、
response.audio.deltaは届きませんでした。 - 表示はアプリ側で、fMP4を再生できるもの(サンプルではffplay)に流せば再生できます。
- SDK 1.2.0でも受けられますが、映像の
Deltaはbase64文字列のままなので自分でデコードします。 - 低遅延やブラウザでの表示にはWebRTC、実装を単純にしたい場合やネットワークの制約がある場合はWebSocketが向いています。