すべてのプロダクト
Search
ドキュメントセンター

Alibaba Cloud Model Studio:qwen3.5-omni-plus-realtime で WebRTC を使用したリアルタイム通話

最終更新日:Aug 26, 2026

本ドキュメントでは、ブラウザで WebRTC と JavaScript を使用して Realtime API に接続し、qwen3.5-omni-plus-realtime モデルとのリアルタイム音声/ビデオ通話を有効にする方法について説明します。

注記WebRTC は、ブラウザベースの低レイテンシーな音声シナリオに適しています。音声は UDP 上で直接送信され、エコーキャンセルとノイズリダクション機能が組み込まれています。WebRTC は、サーバーサイドの VAD (音声アクティビティ検出) モード (server_vad または semantic_vad) のみをサポートします。手動モードはサポートされていません。

前提条件と注意事項

  1. API キーを設定して、環境変数として設定します。
  2. WebRTC をサポートする最新のブラウザ (Chrome、Edge、Firefox、Safari など) を使用してください。
  3. ブラウザにはマイクの使用許可が必要です。ビデオ通話の場合は、カメラの使用許可も必要です。
  4. CORS の制約により、ブラウザは SDP 交換リクエストをサーバーに直接送信できません。デモでは、ターミナルで curl コマンドを実行して接続を完了します。本番環境では、リクエストがアプリケーションサーバーを介してプロキシされる場合、この制約は適用されません。

AI 音声/ビデオ通話の実装

以下のシーケンス図は、WebRTC 音声/ビデオ通話の完全なフローを示しています。

WebRTC 音声/ビデオ通話のシーケンス図

image

RTCPeerConnection の作成

ブラウザのネイティブ RTCPeerConnection コンストラクタを呼び出して、接続インスタンスを作成します。ICE サーバーを設定する必要はありません (サーバーが NAT トラバーサルを処理します)。

pc = new RTCPeerConnection({ iceServers: [ ] });

主要なコールバックを登録します。

// 接続状態リスナー
pc.onconnectionstatechange = () => {
  if (!pc) return;
  if (pc.connectionState === 'connected') {
    setStatus('Connected, please speak', 'connected');
  } else if (["failed", "closed", "disconnected"].includes(pc.connectionState)) {
    endSession(true);
  }
};

// リモート音声ストリームの受信と再生、および録音の開始
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream);
};

ローカルメディアストリームの取得

1 回の getUserMedia 呼び出しで、音声 (必須) とビデオ (任意) をリクエストします。ビデオを有効にするかどうかは、ユーザーの [Enable video] チェックボックスによって決まります。

const wantVideo = !!sendVideoCheckbox.checked;

const constraints = wantVideo
  ? {
      audio: true,
      video: {
        facingMode: { ideal: "user" },
        frameRate: { ideal: 30, max: 30 },
        width: { ideal: 640 },
        height: { ideal: 480 },
      }
    }
  : { audio: true };

localStream = await navigator.mediaDevices.getUserMedia(constraints);

注記音声とビデオは、別々ではなく、1 回getUserMedia 呼び出しで取得します。ビデオプレビューは 30 fps で実行されます (スムーズなローカルプレビューのため)。送信フレームレートは Canvas を介して 2 fps に削減されます。

PeerConnection へのメディアトラックの追加

音声トラックの追加:
localStream.getAudioTracks().forEach(t => {
  pc.addTrack(t, localStream);
  gatedAudioTracks.push(t);
});
ビデオトラックの追加 (任意、Canvas によるフレーム削減で 2 fps に):

Canvas の寸法は、ハードコーディングではなく、実際のカメラ解像度から動的に取得されます。

const sendFps = 2;
const settings = localStream.getVideoTracks()[0].getSettings();
sendCanvas = document.createElement("canvas");
sendCanvas.width = settings.width || 640;   // 実際の幅を動的に取得します
sendCanvas.height = settings.height || 480;  // 実際の高さを動的に取得します
sendCanvasCtx = sendCanvas.getContext("2d", { alpha: false });

sendCanvasStream = sendCanvas.captureStream(sendFps); // 2 fps
const lowFpsTrack = sendCanvasStream.getVideoTracks()[0];
pc.addTrack(lowFpsTrack, sendCanvasStream);
gatedVideoTracks.push(lowFpsTrack);

// requestAnimationFrame ループ:カメラフレームを Canvas に描画します
const pump = () => {
  if (!sendCanvasCtx || !sendCanvas) return;
  try { sendCanvasCtx.drawImage(localVideo, 0, 0, sendCanvas.width, sendCanvas.height); } catch {}
  sendRafId = requestAnimationFrame(pump);
};
sendRafId = requestAnimationFrame(pump);
メディアのゲーティング (重要):

トラックを追加した後、すぐに送信をブロックし、session.created が受信される前にメディアがプッシュされないようにします。

// 1. すべての track.enabled を無効化します
gateMedia(false);  // track.enabled = false

// 2. 送信を完全にブロックするために、sender のトラックを null に置き換えます
audioSender = pc.getSenders().find(s => s.track?.kind === 'audio');
videoSender = pc.getSenders().find(s => s.track?.kind === 'video');
audioTrack = audioSender?.track;
videoTrack = videoSender?.track;
await audioSender?.replaceTrack(null);
await videoSender?.replaceTrack(videoTrack ? null : undefined);

注記これは、他の SDK の enableSendMediaStream(false) に相当します。session.created を受信するまで、送信を復元してはいけません。

DataChannel の作成

AI サーバーとセッション制御イベントを交換するために、oai-events という名前のデータチャネルを作成します。

const dc = pc.createDataChannel('oai-events');

dc.onopen = () => console.log("DC open");
dc.onmessage = (e) => {
  handleDcMessage(e.data, dc);
};

// サーバーによって作成された DataChannel もリッスンします
pc.ondatachannel = (event) => {
  const ch = event.channel;
  ch.onmessage = (e) => {
    handleDcMessage(e.data, ch);
  };
};

オファー SDP の生成

createOffer() を呼び出してローカルディスクリプションを設定します。完全なオファー SDP を使用する前に、ICE ギャザリングが完了するまで待機します。

pc.onicegatheringstatechange = () => {
  if (!pc) return;
  if (pc.iceGatheringState === "complete" && pc.localDescription?.sdp) {
    const sdp = pc.localDescription.sdp;
    // ICE 収集が完了し、オファー SDP の準備が整いました
    // ユーザー向けの curl コマンドを自動生成します
  }
};

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);

注記iceGatheringState === "complete" になるまで待ってから、SDP を使用してください。その時点で、SDP にはすべての ICE 候補情報が含まれています。

SDP 交換 (curl コマンドまたはアプリケーションサーバー経由)

オファー SDP をサーバーに送信し、アンサー SDP を取得します。デモでは、これは curl コマンドを介して行われます。

curl -X POST 'https://{endpoint}/api/v1/webrtc/realtime?model=qwen3.5-omni-plus-realtime' \
  -H 'Content-Type: application/sdp' \
  -H 'Authorization: Bearer $DASHSCOPE_API_KEY' \
  --data-binary '<オファー SDP コンテンツ>'

注記本番環境では、フロントエンドで API キーが公開されないように、AppServer を介してこのステップをプロキシします。 {endpoint} はリアルタイム API のエンドポイントです。

アンサー SDP を設定して接続を確立

サーバーから返されたアンサー SDP をリモート記述として設定し、WebRTC 接続を確立します。設定する前に SDP 形式を正規化します。

function normalizeSdpForSetRemote(sdp) {
  sdp = String(sdp).trim().replace(/\r?\n/g, "\r\n");
  if (!sdp.endsWith("\r\n")) sdp += "\r\n";
  return sdp;
}

const answerSdp = normalizeSdpForSetRemote(txt);
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });

注記SDP 仕様では、改行コードとして\r\nが必要です。 normalizeSdpForSetRemote は、さまざまなソースからの改行コードの互換性を処理します。

AI セッションの設定 (session.update)

接続が確立されると、サーバーから DataChannel 経由で session.created イベントが送信されます。これを受信すると:

  1. メディアのゲーティングを解放して、音声/ビデオの送信を復元します
  2. session.update を送信してセッションパラメーターを設定します
ゲートの解放とメディアの復元:
function handleDcMessage(data, channel) {
  let obj;
  try { obj = JSON.parse(data); } catch (err) { return; }

  if (obj?.type === "session.created") {
    // ゲートを解放:track.enabled を復元します
    gateMedia(true);
    // 実際のトラックを sender に復元します
    if (audioSender) audioSender.replaceTrack(audioTrack);
    if (videoSender && videoTrack) videoSender.replaceTrack(videoTrack);
    // セッション設定を送信します
    sendUpdate(channel);
  }
}
session.update メッセージボディ:
const update = {
  event_id: `event_${Date.now()}`,
  type: "session.update",
  session: {
    input_audio_format: "pcm",
    input_audio_transcription: { model: "qwen3-asr-flash-realtime" },
    instructions: "You are a helpful assistant.",
    modalities: ["text", "audio"],
    output_audio_format: "pcm",
    smooth_output: false,
    turn_detection: {
      prefix_padding_ms: 500,
      silence_duration_ms: 800,
      threshold: 0.5,
      type: "server_vad",
    },
  },
};
if (channel && channel.readyState === "open") channel.send(JSON.stringify(update));

注記turn_detection.typeserver_vad (音量ベースの検出) または semantic_vad (意味検出) に設定できます。WebRTC モードでは手動 VAD はサポートされていません。

リアルタイムでの対話

接続が確立されると、RTP を介してオーディオ/ビデオがリアルタイムで送信されます。リモート AI 音声応答は ontrack コールバックを介して受信され、再生されます。MediaRecorder を使用して、ダウンロード用に応答が録音されます。

リモート音声の受信と録音:
pc.ontrack = async (e) => {
  const stream = e.streams[0];
  ensureHiddenAudioEl();
  hiddenRemoteAudioEl.srcObject = stream;
  try { await hiddenRemoteAudioEl.play(); } catch {}
  startRecordingRemoteStream(stream); // 録音を開始します
};

function startRecordingRemoteStream(remoteStream) {
  const audioTracks = remoteStream.getAudioTracks();
  if (!audioTracks.length) return;
  const audioStream = new MediaStream(audioTracks);

  recordedChunks = [ ];

  mediaRecorder = new MediaRecorder(audioStream, { mimeType: 'audio/webm' });
  mediaRecorder.ondataavailable = (e) => {
    if (e.data && e.data.size > 0) recordedChunks.push(e.data);
  };
  mediaRecorder.onstop = () => {
    audioBlob = new Blob(recordedChunks, { type: 'audio/webm' });
    // 録音停止後にダウンロード可能です
  };
  mediaRecorder.start();
}
DataChannel イベントの表示:

イベントパネルには、DataChannel を介して送受信されるすべてのイベント(session.createdresponse.audio_transcript.done など)が表示され、展開して完全な JSON を表示できます:

function pushEventFromDataChannel(eventObj) {
  const ts = eventObj.timestamp || nowTs();
  events.unshift({ event: eventObj, timestamp: ts });
  renderEvents();
}

セッションの終了とリソースの解放

通話を終了する際は、すべてのリソースを順番に解放します。この順序は重要です。

function endSession(silent = false) {
  // 1. Canvas フレームレート削減ループを停止します
  if (sendRafId) cancelAnimationFrame(sendRafId);
  sendRafId = 0;
  if (sendCanvasStream) sendCanvasStream.getTracks().forEach(t => t.stop());
  sendCanvasStream = null; sendCanvasCtx = null; sendCanvas = null;

  // 2. 録音を停止します
  try { if (mediaRecorder && mediaRecorder.state !== "inactive") mediaRecorder.stop(); } catch {}
  mediaRecorder = null;

  // 3. ローカルメディアストリームを停止します
  if (localStream) {
    localStream.getTracks().forEach(t => t.stop());
    localStream = null;
  }

  // 4. PeerConnection を閉じます
  if (pc) { try { pc.close(); } catch {} pc = null; }

  // 5. リモート音声要素をクリーンアップします
  if (hiddenRemoteAudioEl) {
    try { hiddenRemoteAudioEl.pause(); } catch {}
    hiddenRemoteAudioEl.srcObject = null;
    hiddenRemoteAudioEl.remove();
    hiddenRemoteAudioEl = null;
  }
}

注記セッション終了後、[Download remote audio] ボタンを使用して、AI の応答録音を WebM 形式で保存できます。

重要な注意事項

  1. メディアの送信は session.created の後に開始する必要があります:サーバーが session.created を送信する前に送信されたメディアデータは破棄されます。それまで送信を完全にブロックするには、replaceTrack(null) を使用できます。
  2. ビデオフレームの間引きは Canvas を使用して実装: ローカルプレビューは 30 fps で実行されますが、captureStream(2) によって制御され、サーバーには 2 fps のみが送信されます。これにより、帯域幅が節約されます。
  3. SDP 形式の正規化: Answer SDP を設定する前に、改行コードが \r\n であることを確認してください。そうでない場合、setRemoteDescription が失敗する可能性があります。
  4. ビデオは任意です:ユーザーがビデオを有効にしない場合、マイクの使用許可のみが要求され、カメラの使用許可プロンプトは表示されません。
  5. リモート音声は自動的に録音されます:MediaRecorder は AI の応答音声ストリームを録音します。セッション終了後、WebM ファイルがダウンロード可能になります。
  6. WebRTC はサーバーサイドの VAD のみをサポートします: manual モードはサポートされていません。server_vad (音量検出) または semantic_vad (意味検出) を使用してください。

完全なデモのダウンロード

完全なサンプルコードのダウンロード:webrtc_demo.html

関連ドキュメント