0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ - SDPを1行ずつ突き合わせた記録

0
Last updated at Posted at 2026-11-10

Voice LiveのWebRTC音声接続を.NETから使った話

Voice Live APIには、会話の音声そのものをWebRTCで運ぶWebRTC音声接続(/voice-live/realtime/calls)があります。通常のWebSocketでは音声をbase64のイベントでやり取りしますが、WebRTCならUDP/RTPで運ぶので、低遅延でパケットの欠落にも強くなります。
これを.NETのコンソールアプリから、WebRTCライブラリのSIPSorceryを使ってつなごうとしたところ、

allocate client error: Remote client allocation failed: ServerError(InternalServerError, "No details provided")

という、詳細の無いエラーで失敗し続けました。最終的に双方向の音声会話まで通せたのですが、いくつかポイントがあります。しかも、プレビューなのでどれもドキュメントにはまだ書かれていないようでした。
本記事では、SIPSorceryでWebRTC音声接続を通すために必要だった5つの対処と、その調べ方について紹介します。

Voice Live機能解説シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)

  1. Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
    SDKに型が無い機能の分類
  2. Voice Live APIで会話の「間」を扱う
    先に話す・言いよどみを待つ・割り込む・待ち時間に話す
  3. Voice Live の音声入力の品質を上げる
    ノイズ抑制・エコー除去・Live-Reference AEC の選び方
  4. Voice LiveでMCPサーバのツールを使う
    function callingとの違いと並列ツール呼び出し
  5. Voice LiveでFoundryのエージェントをツールとして呼ぶ
    chat-supervisorパターンの使い方と前段モデルの選び方
  6. 30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
    Personal Voice の申請から実装まで
  7. Voice Live APIのアバターを使い分ける
    標準・カスタム写真・カスタムビデオの設定と実装
  8. Voice Liveのアバター映像をWebSocketで受ける
    WebRTCとの違いと映像の形式

番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ(本記事)
SDPを1行ずつ突き合わせた記録

本記事は2026年7月の検証に基づきます。WebRTC音声接続は2026年10月時点でもpreview(GAの2026-07-15でもpreviewのまま)で、SLAはありません。サーバ側の挙動が変わっている可能性があります。

WebRTC音声接続の仕組み

先に、アバターのWebRTCと混同しないように整理しておきます。

WebRTC音声接続(本記事) WebRTCアバター
WebRTCで運ぶもの 会話の音声そのもの アバターの映像と音声だけ
会話の音声 RTP WebSocketのイベント
接続先 /voice-live/realtime/calls /voice-live/realtime
アバター 制御チャネル(サイドバンド制御)ではアバターの設定に非対応(公式ドキュメント) ―

WebRTC音声接続では、クライアントとVoice Liveの間に3つの経路を張ります。最初にWebSocketを開いて接続の交渉を行い、その結果としてRTCPeerConnection(音声トラックとデータチャネル)ができます。

経路 通るもの 主なイベント
WebSocket(制御チャネル) 接続の交渉、セッションの設定、ツール呼び出し、エラー rtc.call.sdp.create / rtc.call.sdp.created / rtc.call.error、session.update、response.function_call_arguments.*、error
RTPの音声トラック(Opus) マイクの音声(上り)とエージェントの音声(下り) イベントではなく、途切れない音声として流れる
データチャネル(voice-live-events) VADの検出、文字起こし、応答の状態 input_audio_buffer.speech_started / speech_stopped、response.created / response.done、response.audio_transcript.*

接続の流れは次のとおりです。

  1. /voice-live/realtime/calls?api-version=...&model=...にWebSocketで接続する
  2. RTCPeerConnectionを作り、音声トラックを追加してofferを作る。ICEの候補集めが終わるのを待つ
  3. rtc.call.sdp.createでoffer(とセッション設定)を送る
  4. rtc.call.sdp.createdでanswerを受け取り、setRemoteDescriptionする
  5. ICEが接続されると音声が流れ始める
rtc.call.sdp.create
{
  "type": "rtc.call.sdp.create",
  "sdp_offer": "v=0\r\no=- ...",
  "session": {
    "modalities": ["text", "audio"],
    "voice": { "name": "ja-JP-Nanami:DragonHDLatestNeural", "type": "azure-standard" }
  }
}

/callsでは最初のsession.updateを送れません。セッション設定はrtc.call.sdp.createのsessionに入れ、modelはURLで指定します。

準備

リソースの作成とAPIキーの確認までは、公式のクイックスタートに沿って進めると準備できます。対応リージョンは、Voice Liveの概要ページで確認できます。

そのうえで、本記事では次を用意しました。

  1. .NETのコンソールアプリを作り、SIPSorceryのパッケージを追加する

    dotnet add package SIPSorcery --version 10.0.14
    
  2. 比較用に、公式のブラウザ向けサンプル(WebRTCのstandaloneサンプル)を同じリソースに向けて動かせるようにしておく

SIPSorceryのバージョンにも注意が必要でした。2026年8月に公開された脆弱性を解消するために上げたところ、10.0.13ではWebRTC音声接続の音声が壊れ、10.0.14で正常になりました(サンプルは現在10.0.14)。

接続できるまでの調査

サービス側の切り分け

厄介なのはエラーには何の手がかりもなかったことです。詳細がなぜか出ていない状態で、サービス側の障害なのか、自分のクライアントの問題なのかをまず切り分けました。使ったのは、公式のブラウザ向けサンプル(WebRTCのstandaloneサンプル)です。同じリソースに対してChromeから接続したところ、別のエラーになりました。

SEEP payload error: Failed to parse SDP: Unsupported parsing error:
Unknown rtcpfb ack parameter: "ccfb" in line(28): a=rtcp-fb:111 ack ccfb

最近のChromeはofferにa=rtcp-fb:111 ack ccfb(RFC 8888の輻輳制御フィードバック)を含めます。検証した時点では、この行があるとSDP全体のパースで止まっていたので、ブラウザから試す場合は、まずこの行を除いてみてください。
そこで、送る前にofferからack ccfbの行を取り除いたところ、connectedまで到達し、双方向で会話できました。これで2つのことがわかりました。

  • サービス自体は正常に動いている
  • ブラウザのofferなら(ccfbさえ除けば)通る(あまり直接的には関係ない)

answerを見ると、o=str0m-0.19.0 ...とあったので、サーバ側のWebRTC実装はRustのstr0mのようです。ここからは、通るChromeのofferと、通らないSIPSorceryのofferを1行ずつ突き合わせる作業になりました。

1. 認証方法

最初に引っかかったのは認証でした。

認証の渡し方 WebSocketの接続(HTTP 101) その後のallocation
Entra IDのBearerトークン 通る 失敗(同じInternalServerError)
APIキーをヘッダで 通る 失敗
APIキーをURLのクエリ(&api-key=)で 通る 通る

接続自体は成功するのに、そのあとで同じ詳細の無いエラーになるので、認証の問題だと気づきにくいです。ブラウザのサンプルはAPIキーをクエリに載せていたので、それに合わせました。

URLにAPIキーが入るので、ログにURLをそのまま出さないように注意してください。サンプルではapi-key=***に伏せて出力しています。

2. SDPのofferに足す属性

SIPSorceryが既定で作るofferには、ブラウザが送っている属性がいくつか足りません。Chromeの成功したofferと突き合わせて、次を補いました。

補ったもの 内容
a=rtcp-fb:111 transport-cc 輻輳制御のフィードバック
a=extmap:1..4 音量レベル、送信時刻、transport-wide-cc、midの拡張ヘッダ
a=msid+a=ssrc ... msid ストリームの識別子
a=msid-semantic / a=extmap-allow-mixed セッションレベルの属性
a=rtcp-rsize 縮小サイズのRTCP
a=fmtp:111 minptime=10;useinbandfec=1 Opusのパラメータ
m= / c=のアドレスを実際のホスト候補のIPとポートに SIPSorceryの既定0.0.0.0:9だと失敗する
s=- SIPSorceryの既定s=sipsorceryは不可
a=ice-options:trickle ice2を取り除く

特に気づきにくかったのがm= / c=のアドレスです。ICEの候補はa=candidateで提示しているのに、m=行の既定のアドレスが0.0.0.0:9だとallocationに失敗しました。

コーデックのブロック(payload番号とa=rtpmap)は、SIPSorceryが作ったものをそのまま使ってください。最初はChromeのofferに丸ごと置き換えようとしたのですが、そうするとSIPSorceryの内部の状態とofferの内容が食い違い、次のOpusの交渉が壊れました。足りない属性を足すだけにすると動きました。

3. answerのOpusの扱い

offerが通ってICEが接続されても、今度はこちらの声がサーバに届きません。原因はanswerでした。str0mはOpusを小文字のopus/48000/2(動的なpayload番号)で返してきます。SIPSorceryはこれを既知のOpusと認識できず、コーデックをUnknownとして扱っていました。その結果、SetAudioSourceFormat(Unknown)でエンコーダが動かず、マイクの音声が1つも生成されていませんでした。

交渉が終わったときのイベント(OnAudioFormatsNegotiated)で、payload番号はそのままに、Opusのフォーマットとして作り直して設定し直しました。

VoiceLiveWebRtcCall.cs(抜粋)
pc.OnAudioFormatsNegotiated += formats =>
{
    var negotiated = formats[0];

    // answer の小文字 "opus"(動的 payload)を SIPSorcery が Unknown と扱うので、
    // payload 番号はそのままに Opus として作り直す
    var format = negotiated.Codec != AudioCodecsEnum.OPUS &&
                 string.Equals(negotiated.FormatName, "opus", StringComparison.OrdinalIgnoreCase)
        ? new AudioFormat(AudioCodecsEnum.OPUS, negotiated.FormatID, 48000, 2, negotiated.Parameters)
        : negotiated;

    // マイク(source)とスピーカー(sink)は同じフォーマットにする
    audioEndPoint.SetAudioSourceFormat(format);
    audioEndPoint.SetAudioSinkFormat(format);
};

WindowsAudioEndPointはフォーマットの管理を1つしか持っていないようで、sourceとsinkに別々のフォーマットを設定すると、先に設定したほうが黙って壊れました。両方に同じものを設定します。

4. スピーカーの開始

こちらの声は届くようになったのに、今度はエージェントの声が聞こえません。

SIPSorceryのWindowsAudioEndPoint.StartAudio()は、マイク(source)しか開始しません。スピーカー(sink)はStartAudioSink()を別に呼ぶ必要があり、マイクは送れているのに無音だった原因はこれでした。

VoiceLiveWebRtcCall.cs(抜粋)
await audioEndPoint.StartAudio();      // マイク(source)
await audioEndPoint.StartAudioSink();  // スピーカー(sink)は別に開始する

5. APIバージョン

最後がAPIバージョンです。/callsに2026-06-01-previewを指定すると、接続の時点でHTTP 401が返りました。APIキーのクエリでもEntra IDでも同じだったので、認証ではなくバージョンの制約のようです。
2026-01-01-previewなら通るので、サンプルではWebRTC音声接続のときだけバージョンを2026-01-01-previewに固定しています。

検証した2026年7月の時点では、rtc.call.*のイベントが定義されているのは2026-01-01-previewのリファレンスで、WebRTCのhow-toもエンドポイント例に2026-01-01-previewを使っていました。2026年11月時点のhow-toでは、エンドポイント例がapi-version=2026-07-15に変わっています。現在は新しい版で通る可能性がありますが、再検証はしていません。
なお401は認証エラーのコードなので、キーや権限を疑いたくなりますが、ここでは版の指定を先に見直すとよさそうです。

動作確認

5つをすべて入れると、次の流れが最後まで動きました。

  1. マイクの音声がRTPで送られる
  2. サーバのVADが発火する(input_audio_buffer.speech_started / speech_stopped / committed)
  3. 応答が生成される(response.*)
  4. エージェントの音声がRTPで返ってきて、スピーカーから鳴る

VADや文字起こし、応答の状態は、データチャネルvoice-live-eventsで受け取れます。

今後変わる可能性がある点

今回のSDPの書き換えは、ライブラリの既定のofferと、サーバ側のWebRTC実装(str0m)が受け付ける形の差を、クライアント側で埋めたものです。ブラウザが送るofferはそのまま通るので、WebRTCとしては正しいが、ブラウザ以外のスタックでは追加の属性が要るという状態です。

WebRTC音声接続はpreviewで、サーバ側の実装もライブラリも更新されます。次のことは、いずれ不要になる可能性があります。

  • SDPへの属性の追加(将来SIPSorceryのネイティブな設定で書けるようになるのが望ましい形です)
  • APIキーをURLのクエリで渡す必要
  • api-versionを2026-01-01-previewに固定する必要(how-toの例はすでに2026-07-15になっています)

実際、WebRTCのドキュメントはエラー応答の形式が明確になりました。rtc.call.errorにerror.type(invalid_request_error / server_error)・error.code(機械可読)・error.message(人間可読)が定義され、{"code": "missing_sdp", "message": "SDP offer is required"}のような具体例も載っています。原因の切り分けは当時より楽になっているはずです。また、SDPのofferとanswerを、WebSocketの代わりにHTTPのPOSTで交換する方法も追加されています。

まとめ

Voice LiveのWebRTC音声接続をSIPSorceryでつなぐのに必要だったのは、次の5つでした。

  1. APIキーをURLのクエリで渡します(Bearer・ヘッダはallocationで失敗します)
  2. offerをstr0mが受け付ける形に補います。コーデックのブロックは触りません
  3. answerの小文字のopusを、Opusとして作り直します
  4. スピーカーを明示的に開始します
  5. APIバージョンは2026-01-01-previewを使います

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?