全部產品
Search
文件中心

Alibaba Cloud Model Studio:通過WebRTC使用qwen3.5-omni-plus-realtime實現即時通話

更新時間:Aug 26, 2026

本文檔說明如何在瀏覽器端通過 WebRTC + JavaScript 接入百鍊 Realtime API,實現與 qwen3.5-omni-plus-realtime 模型的即時音視訊通話。

說明WebRTC 適合瀏覽器端、低延遲語音情境,音頻通過 UDP 直接傳輸,內建回聲消除和降噪。WebRTC 僅支援服務端 VAD 模式(server_vadsemantic_vad),不支援手動模式。

前提條件及注意事項

  1. 配置 API Key並將其設定到環境變數
  2. 使用支援 WebRTC 的現代瀏覽器(Chrome、Edge、Firefox、Safari 等)。
  3. 瀏覽器需要麥克風許可權;如需視訊通話,還需網路攝影機許可權。
  4. 瀏覽器無法直接向服務端發起 SDP 交換請求(受 CORS 限制),Demo 中通過終端執行 curl 命令完成串連建立;正式使用時由業務 AppServer 代理完成,無此限制。

實現 AI 音視訊通話

以下時序圖展示了整個 WebRTC 音視訊通話的完整流程:

WebRTC 音視訊通話流程時序圖

image

建立 RTCPeerConnection

呼叫瀏覽器原生 RTCPeerConnection 建立串連執行個體,無需配置 ICE 伺服器(服務端會處理 NAT 穿透)。

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

註冊關鍵回調:

// 串連狀態監聽
pc.onconnectionstatechange = () => {
  if (!pc) return;
  if (pc.connectionState === 'connected') {
    setStatus('已串連,請說話', '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);
};

擷取本地媒體流

通過一次 getUserMedia 調用擷取所需的音頻(必須)和視頻(可選)。是否開啟視頻由使用者勾選"開啟視頻"複選框決定。

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);

說明音頻和視頻通過同一次 getUserMedia 調用擷取,而非分開請求。視頻預覽幀率為 30fps(本地流暢預覽),發送幀率會通過 Canvas 降至 2fps。

添加媒體軌道到 PeerConnection

添加音頻軌道:
localStream.getAudioTracks().forEach(t => {
  pc.addTrack(t, localStream);
  gatedAudioTracks.push(t);
});
添加視頻軌道(可選,通過 Canvas 降幀至 2fps):

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); // 2fps
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. 禁用所有軌道的 enabled
gateMedia(false);  // track.enabled = false

// 2. 將 sender 的 track 替換為 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

建立名為 oai-events 的 DataChannel,用於與 AI 服務端交換會話控制事件。

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);
  };
};

產生 Offer SDP

調用 createOffer() 並設定本地描述,等待 ICE 候選收集完成後擷取完整的 Offer SDP。

pc.onicegatheringstatechange = () => {
  if (!pc) return;
  if (pc.iceGatheringState === "complete" && pc.localDescription?.sdp) {
    const sdp = pc.localDescription.sdp;
    // ICE 收集完成,Offer SDP 可用
    // 自動產生 curl 命令供使用者使用
  }
};

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

說明必須等待 iceGatheringState === "complete" 後再使用 SDP,此時 SDP 中包含所有 ICE 候選資訊。

交換 SDP(通過 curl 命令或業務 AppServer)

將 Offer SDP 發送到百鍊服務端,擷取 Answer SDP。Demo 中通過 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 '<Offer SDP 內容>'

說明生產環境中,此步驟應由業務 AppServer 代理完成,避免前端暴露 API Key。{endpoint} 為 Realtime API 接入地址。

設定 Answer SDP 建立串連

將服務端返回的 Answer 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\nnormalizeSdpForSetRemote 負責處理不同來源的分行符號相容問題。

配置 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 的實際 track
    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.type 可設為 server_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;
  }
}

說明結束後可通過"下載遠端音頻"按鈕下載 AI 回複的錄音(WebM 格式)。

注意事項

  1. 媒體門控必須在 session.created 後解除:在服務端發送 session.created 之前推送媒體資料會被丟棄,必須通過 replaceTrack(null) 徹底阻斷髮送。
  2. 視頻降幀通過 Canvas 實現:本地預覽 30fps,發送至服務端僅 2fps,通過 captureStream(2) 控制,節省頻寬。
  3. SDP 格式正常化:設定 Answer SDP 前必須確保行尾為 \r\n,否則 setRemoteDescription 可能失敗。
  4. 視頻為可選功能:使用者未勾選視頻時,僅請求音頻許可權,不會觸發網路攝影機授權彈窗。
  5. 遠端音頻自動錄製:通過 MediaRecorder 錄製 AI 回複的音頻流,會話結束後可下載 WebM 格式檔案。
  6. WebRTC 僅支援服務端 VAD:不支援 manual 模式,可選 server_vad(音量檢測)或 semantic_vad(語義檢測)。

完整 demo 下載

完整範例程式碼請下載:webrtc_demo.html

相關文檔