Skip to main content

WebRTC 接入

本文介紹通過 WebRTC 接入 Realtime API 的通用流程:準備鑒權、初始化連接對象、配置媒體方向、完成 SDP 交換、按目標模型協議交互,以及釋放資源。

WebRTC 使用媒體軌道傳輸音視頻,使用 DataChannel 傳輸模型事件和文本。Web 端可使用瀏覽器原生 API,其他端可使用支持標準 WebRTC 的庫。百鍊不提供專用 WebRTC SDK。

前提條件

  • 完成接入概覽中的準備工作。
  • 在 Realtime API 概述中確認目標模型支持 WebRTC。當前支持範圍包含實時全模態、實時語音對話、實時語音翻譯和多模態交互套件;語音合成、語音識別模型應選擇支持它們的 AOQ 或 WebSocket。
  • 業務 AppServer 已準備好匹配地域與業務空間的 API Key,用於代理 SDP 交換。鑒權字段見 Token 鑒權。
瀏覽器採集媒體時,使用安全上下文並申請所需的麥克風或攝像頭權限;接收音頻時處理瀏覽器自動播放限制。

整體流程

WebRTC 接入時序圖
  1. 業務 AppServer 準備 API Key 和目標模型參數。
  2. 客戶端創建連接對象並註冊回調。
  3. 配置媒體方向及 DataChannel,暫不掛載上行媒體軌道。
  4. 通過 AppServer 交換 SDP,等待連接及事件通道就緒。
  5. 按模型協議完成配置,收到確認後掛載所需上行媒體軌道。
  6. 完成交互後停止採集、關閉連接並釋放資源。
  • 音視頻通過 WebRTC 媒體軌道傳輸;應用負責獲取本地媒體軌道、綁定遠端播放器,並按目標模型的事件定義收發 DataChannel 消息。
  • WebRTC 的鑒權在 SDP 交換時完成,無需先獲取 AOQ Token。瀏覽器通過業務 AppServer 完成鑒權請求與 SDP 轉發,避免將長期 API Key 放入前端代碼。
  • 連接成功表示傳輸通道已建立,但仍需等待目標模型初始化成功後再發送媒體。例如,Realtime 模型需要等待 session.updated。因此,客戶端應在模型初始化成功後,才能開啟所需的上行媒體。

1. 準備鑒權和連接參數

業務 AppServer 根據目標模型、地域和業務空間配置 SDP 交換請求。標準 Realtime 建連參數如下;應用專屬接入參數以對應最佳實踐為準。
配置項值
方法POST
地址https://{endpoint}/api/v1/webrtc/realtime?model={model_name}
Content-Typeapplication/sdp
AuthorizationBearer <API_KEY>
請求體Offer SDP
成功響應Answer SDP
完整地域、地址和鑒權說明見 Token 鑒權。業務 AppServer 的接口由業務自行實現,不屬於百鍊 SDK API。

2. 初始化連接對象並註冊回調

Web 端創建 RTCPeerConnection,其他端執行對應庫的初始化操作。建立連接前註冊以下處理邏輯:
  • 連接狀態變化:判斷連接是否成功,以及失敗或斷開。
  • DataChannel 建立、打開、消息和關閉:承載模型事件。
  • 遠端媒體軌道:接收並播放音頻,或處理目標應用支持的其他媒體。
以下代碼片段共享同一作用域,按章節組合到一個腳本中,在所有變量、函數和回調註冊完成後再由用戶操作調用 startSession()。示例每次創建一條連接;結束後再次通話需重新創建這些對象。
const pc = new RTCPeerConnection({ iceServers: [ ] });

const channels = new Set();
const senders = new Map();
const remoteAudio = document.createElement("audio");
remoteAudio.controls = true; // 自动播放被阻止时,用户可手动播放。
remoteAudio.autoplay = true;
document.body.appendChild(remoteAudio);

let localStream = null;
let eventChannel = null;
let stopped = false;
let started = false;
let sessionCreated = false;
let updateSent = false;
let modelReady = false;
let mediaStarted = false;
const requestController = new AbortController();
註冊消息監聽應早於模型初始化,避免遺漏服務端主動發送的會話事件。不要只監聽客戶端創建的通道:服務端會通過名為 txt 的通道推送事件,應處理 datachannel 回調。

3. 配置媒體方向和 DataChannel

選擇四個媒體方向

以下方向均以客戶端為視角。WebRTC 通過媒體收發器協商發送與接收能力:雙向為 sendrecv,僅發送為 sendonly,僅接收為 recvonly。最終可用能力還取決於服務端 SDP 應答及模型支持範圍。
方向配置內容
推音頻協商音頻發送能力,準備麥克風或外部音頻軌道;模型就緒後掛載
拉音頻協商音頻接收能力,在遠端軌道回調中綁定音頻播放
推視頻目標模型支持視覺輸入時協商視頻發送能力,準備攝像頭或外部視頻軌道
拉視頻僅在目標模型/應用明確支持視頻輸出時協商接收能力,並實現渲染
例如,語音對話一般使用雙向音頻,帶視覺輸入的語音對話還需要視頻上行。Offer SDP 需要包含音頻媒體段 m=audio;不能僅因業務無需播放音頻就刪除該媒體段。

各模型如何選擇

模型/應用推音頻拉音頻推視頻拉視頻
Qwen-Omni-Realtime語音輸入時開啓需要語音回復時開啓需要視覺輸入時開啓不支持開啓
Qwen-Audio-Realtime語音對話時開啓需要語音回復時開啓不支持開啓不支持開啓
Qwen-LiveTranslate-Realtime語音輸入時開啓需要語音譯文時開啓按版本及場景配置不支持開啓
multimodal-dialog按應用輸入配置按應用輸出配置按應用能力配置僅在應用明確支持時配置
該表描述業務需要的媒體方向,不承諾所有方向組合都可單獨協商。實際配置以目標模型和當前服務端協商能力為準。

準備媒體但延後上行

在創建 Offer 前預先協商所需媒體方向。對需要等待初始化確認的模型,先保留發送能力而不掛載真實採集軌道;模型就緒後再通過對應 sender 的 replaceTrack 等操作開始發送。接收媒體的處理邏輯可以提前註冊。 音頻發送和接收、視頻發送和接收應分別按需配置,不要把打開麥克風、播放音頻和模型初始化合併為一個操作。

媒體配置示例

下面以雙向語音、可選視頻上行為例。media 的四個開關描述業務需要;拉視頻僅在目標應用明確支持時開啓。addTransceiver 預先協商方向,採集到的軌道先保存在本地,直到模型就緒後再掛載到 sender。
const media = {
  sendAudio: true,
  receiveAudio: true,
  sendVideo: false,
  receiveVideo: false,
};

function mediaDirection(send, receive) {
  if (send && receive) return "sendrecv";
  if (send) return "sendonly";
  if (receive) return "recvonly";
  return "inactive";
}

async function configureMedia() {
  for (const kind of ["audio", "video"]) {
    const send = kind === "audio" ? media.sendAudio : media.sendVideo;
    const receive = kind === "audio" ? media.receiveAudio : media.receiveVideo;
    // 保留 m=audio;具体方向组合须由目标服务支持。
    if (kind === "audio" || send || receive) {
      const transceiver = pc.addTransceiver(kind, {
        direction: mediaDirection(send, receive),
      });
      if (send) senders.set(kind, transceiver.sender);
    }
  }
  if (!media.sendAudio && !media.sendVideo) return;
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: media.sendAudio,
    video: media.sendVideo ? {
      width: { ideal: 640 }, height: { ideal: 480 },
      frameRate: { ideal: 2, max: 2 },
    } : false,
  });
  // 用户可能在权限弹窗期间结束通话。
  if (stopped) {
    stream.getTracks().forEach(track => track.stop());
    throw new Error("会话已结束");
  }
  localStream = stream;
  // 此时 sender.track 仍为空,不向模型发送采集数据。
}
視頻分辨率和幀率按目標模型要求設置;需要將本地預覽與上行幀率分離時,可參考最佳實踐中的 Canvas 降幀實現。

創建事件通道

在創建 Offer 前創建 DataChannel,使 SDP 包含數據通道協商。客戶端創建通道及服務端創建通道的處理方式,參見下方 WebRTC 最佳實踐。 DataChannel 負責發送模型初始化、文本和控制事件,並接收模型結果、狀態和錯誤。媒體軌道負責音視頻傳輸。
// bindDataChannel 的完整实现见“Data 通道发送和接收事件”。
pc.ondatachannel = ({ channel }) => bindDataChannel(channel);
const clientChannel = pc.createDataChannel("oai-events");
bindDataChannel(clientChannel);

4. 建立連接

按以下順序完成建連:
  1. 調用 createOffer 和 setLocalDescription。
  2. 按服務端支持的 ICE 方式完成候選信息收集;本示例等待 ICE 收集完成後發送本地 SDP。
  3. 將本地 SDP 發送到業務 AppServer,由 AppServer 攜帶 API Key 請求百鍊 SDP 交換接口。
  4. 檢查 HTTP 狀態,成功後以返回的 Answer SDP 調用 setRemoteDescription。
  5. 等待連接狀態成功,並確認用於發送模型事件的 DataChannel 已打開。
SDP 交換成功或 setRemoteDescription 完成,只表示完成相應協商步驟,不能直接當作媒體連接已經成功。DataChannel 未處於 open 狀態時也不能發送模型事件。

建連代碼及狀態回調

監聽 connectionstatechange 判斷連接狀態,監聽 icegatheringstatechange 等待完整的本地 SDP。模型初始化還需要 DataChannel 的 open 和模型的就緒事件,不能僅依靠 SDP 交換完成判斷。
pc.onconnectionstatechange = () => {
  console.log("WebRTC 状态:", pc.connectionState);
  if (pc.connectionState === "connected") {
    tryInitializeModel();
  } else if (["failed", "disconnected", "closed"].includes(pc.connectionState)) {
    // 本示例选择结束连接;业务可按需要设计断线恢复策略。
    endSession();
  }
};

function waitForIceComplete() {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => finish(new Error("ICE 收集超时")), 15000);
    function finish(error) {
      clearTimeout(timer);
      pc.removeEventListener("icegatheringstatechange", check);
      requestController.signal.removeEventListener("abort", cancel);
      error ? reject(error) : resolve();
    }
    function check() {
      if (pc.iceGatheringState === "complete") finish();
    }
    function cancel() { finish(new Error("会话已结束")); }
    pc.addEventListener("icegatheringstatechange", check);
    requestController.signal.addEventListener("abort", cancel, { once: true });
    if (stopped) cancel();
    else check();
  });
}

function normalizeAnswerSdp(sdp) {
  return String(sdp).trim().replace(/\r?\n/g, "\r\n") + "\r\n";
}

async function startSession() {
  if (started || stopped) return;
  started = true;
  try {
    await configureMedia();
    if (stopped) return;
    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    await waitForIceComplete();

    // 这是业务自行实现的同源接口,不是百炼官方接口。
    // AppServer 为目标模型转发 SDP,附带 API Key,并原样返回 Answer SDP。
    const response = await fetch("/api/realtime/sdp", {
      method: "POST",
      headers: { "Content-Type": "application/sdp" },
      body: pc.localDescription.sdp,
      signal: requestController.signal,
    });
    if (!response.ok) {
      throw new Error(`SDP 交换失败:${response.status} ${await response.text()}`);
    }
    const answerSdp = normalizeAnswerSdp(await response.text());
    if (stopped) return;
    await pc.setRemoteDescription({ type: "answer", sdp: answerSdp });
    // 后续由 connectionstatechange、DataChannel 和模型事件驱动。
  } catch (error) {
    if (!stopped) console.error("建连失败:", error);
    endSession();
  }
}
業務 AppServer 應按第 1 節的地址和請求頭轉發 Offer;示例接口約定返回純 SDP 文本。不要將含有 HTTP 響應頭、日誌或 JSON 包裝的內容直接傳給 setRemoteDescription。

5. 按模型協議交互並收發媒體

初始化模型

連接及事件通道可用後,按目標模型協議處理初始化。應用需要保存已收到的服務端初始化事件,並結合通道狀態推進流程,避免依賴回調到達的固定順序。 例如,使用 session.update 配置會話的模型,應按其協議構造事件,等待配置確認後再發送輸入。音色、輸出模態、VAD、採樣率等參數都應來自當前模型文檔。多模態交互套件使用其自己的事件協議。 session.created 表示會話創建,不能統一視為配置已生效。對於要求確認會話配置的模型,應等待 session.updated。其他模型或應用使用各自定義的就緒條件。 各模型事件文檔可從 WebSocket 接入概覽中的模型導航進入。這裡只復用模型事件定義,媒體傳輸方式和 WebRTC 限制仍以 WebRTC 接入說明為準。

模型事件示例(Omni)

以下示例單獨演示 Omni 的 session.created → session.update → session.updated 流程。切換其他模型時,替換初始化事件構造和回復處理邏輯,遵循後文鏈接的模型事件定義。收到 session.updated 確認配置完成後,再掛載上行媒體軌道。
function tryInitializeModel() {
  if (stopped || updateSent || !sessionCreated ||
      pc.connectionState !== "connected" ||
      eventChannel?.readyState !== "open") return;
  sendModelEvent({
    event_id: `event_${crypto.randomUUID()}`,
    type: "session.update",
    session: {
      modalities: media.receiveAudio ? ["text", "audio"] : ["text"],
      input_audio_format: "pcm",
      output_audio_format: "pcm",
      turn_detection: { type: "server_vad", threshold: 0.5,
                        silence_duration_ms: 800 },
    },
  });
  updateSent = true;
}

async function handleModelEvent(event, channel) {
  if (stopped) return;
  if (event.type === "session.created") {
    sessionCreated = true;
    eventChannel = channel; // 沿收到会话事件的通道发送配置。
    tryInitializeModel();
  } else if (event.type === "session.updated" && updateSent) {
    modelReady = true;
    await startSendingMedia();
  } else if (event.type === "error") {
    console.error("模型错误:", event.error);
    endSession();
  } else {
    // 按所选模型定义展示文本、转录或响应状态;音频从媒体轨道接收。
    console.log("模型事件:", event);
  }
}

開始媒體收發

滿足模型就緒條件後,掛載需要上行的音頻、視頻軌道。通過遠端軌道回調處理模型輸出,並持續解析 DataChannel 返回的事件。
  • 音頻通過 RTP 媒體軌道傳輸,無需發送 input_audio_buffer.append。
  • 畫面通過視頻軌道傳輸;當前 WebRTC 接入不支持 input_image_buffer.append。
  • 文本、狀態、控制和錯誤通過 DataChannel 處理。
async function startSendingMedia() {
  if (stopped || !modelReady || mediaStarted) return;
  mediaStarted = true;

  for (const track of localStream?.getTracks() ?? []) {

    if (stopped) return;
    const sender = senders.get(track.kind);
    if (sender) await sender.replaceTrack(track);
  }
}

// 在 startSession() 前注册。无 streams 的轨道也能正确接收。
const remoteAudioStream = new MediaStream();
const remoteVideo = media.receiveVideo ? document.createElement("video") : null;
if (remoteVideo) {
  remoteVideo.autoplay = true;
  remoteVideo.playsInline = true;
  remoteVideo.controls = true;
  document.body.appendChild(remoteVideo);
}
pc.ontrack = ({ track }) => {
  if (stopped) return;
  if (track.kind === "audio" && media.receiveAudio) {
    remoteAudioStream.addTrack(track);
    remoteAudio.srcObject = remoteAudioStream;
    remoteAudio.play().catch(() => {
      console.info("自动播放受限,请点击音频控件播放");
    });
  }
  if (track.kind === "video" && remoteVideo) {
    remoteVideo.srcObject = new MediaStream([track]);
    remoteVideo.play().catch(() => console.info("请点击视频控件播放"));
  }
};

6. 結束交互並釋放資源

不再使用連接時,由客戶端業務層停止應用持有的本地採集軌道,關閉 DataChannel 和 RTCPeerConnection,釋放播放器等資源並清理業務狀態。 連接失敗或異常斷開時,撤銷模型就緒狀態。重新建立連接後按目標模型協議初始化,避免沿用上一條連接的會話狀態。
function endSession() {
  if (stopped) return;
  stopped = true;
  modelReady = false;
  sessionCreated = false;
  updateSent = false;
  mediaStarted = false;
  requestController.abort(); // 取消未完成的 SDP 请求/ICE 等待。
  localStream?.getTracks().forEach(track => track.stop());
  localStream = null;
  channels.forEach(channel => channel.close());
  channels.clear();
  eventChannel = null;
  pc.close();
  senders.clear();
  remoteAudioStream.getTracks().forEach(track => track.stop());
  remoteAudio.pause();
  remoteAudio.srcObject = null;
  remoteAudio.remove();
  if (remoteVideo) {
    remoteVideo.srcObject?.getTracks().forEach(track => track.stop());
    remoteVideo.pause();
    remoteVideo.srcObject = null;
    remoteVideo.remove();
  }
}
在用戶點擊結束按鈕或頁面退出時調用 endSession()。如果業務額外啓用了最佳實踐中的 Canvas 降幀或錄制,還需取消動畫循環、停止 Canvas 媒體軌道及 MediaRecorder。

Data 通道發送和接收事件

客戶端通過 WebRTC DataChannel 發送和接收模型事件。發送前確認通道處於 open 狀態,使用 send 發送序列化後的模型事件;接收時在對應通道的 message 回調中解析消息,並處理服務端創建的事件通道。 發送事件的名稱、字段、參數和調用時機遵循目標模型的客戶端事件定義;收到消息後的解析方式、狀態變更、結果和錯誤處理遵循該模型的服務端事件定義。初始化事件根據模型要求按需發送,後續文本和控制事件按照業務需要發送。

Data 通道代碼示例

同一個綁定函數處理客戶端創建和服務端創建的通道;它只負責收發與 JSON 解析,模型語義交給第 5 節的 handleModelEvent。sendModelEvent 可用於發送模型定義的初始化、文本或控制事件,但應在對應模型允許的時機調用。
function sendModelEvent(event, channel = eventChannel) {
  if (stopped || pc.connectionState !== "connected" ||
      channel?.readyState !== "open") {
    throw new Error("Data 通道尚不可用");
  }
  channel.send(JSON.stringify(event));
}

function bindDataChannel(channel) {
  channels.add(channel);
  channel.onopen = () => {
    if (stopped) return;
    console.log("DataChannel 已打开:", channel.label);
    tryInitializeModel();
  };
  channel.onmessage = ({ data }) => {
    if (stopped) return;
    let event;
    try { event = JSON.parse(data); }
    catch (error) {
      console.warn("无法解析模型事件:", error);
      return;
    }
    if (!event || typeof event !== "object") return;
    handleModelEvent(event, channel).catch(error => {
      if (!stopped) console.error("模型事件处理失败:", error);
      endSession();
    });
  };
  channel.onerror = error => console.error("DataChannel 错误:", error);
  channel.onclose = () => {
    channels.delete(channel);
    if (channel === eventChannel) endSession();
  };
  if (channel.readyState === "open") channel.onopen();
}
將上述片段放在同一腳本中,完成全部聲明和回調註冊後,在業務的開始按鈕回調中調用 startSession(),結束按鈕回調中調用 endSession()。這兩個函數為本文示例函數,不是 WebRTC 內置 API。

模型事件定義

模型/應用客戶端事件定義(發送)服務端事件定義(接收)
Qwen-Omni-Realtime客戶端事件服務端事件
Qwen-Audio-Realtime客戶端事件服務端事件
Qwen-LiveTranslate-Realtime客戶端事件服務端事件
multimodal-dialog交互協議中的 Input Message交互協議中的 Output Message
多模態交互套件的雙向事件定義在同一篇交互協議中。請按實際接入的模型版本選擇事件定義,不同模型之間不共用一套固定事件。 模型事件的發送與接收還應遵循以下規則:
  • 按模型定義識別事件類型和關聯標識。例如,Realtime 模型事件通常使用 type;多模態交互套件的客戶端事件使用 header.action,服務端事件使用 header.event,並通過 task_id 關聯任務。
  • 模型要求的初始化確認、輸入提交、響應取消和任務結束等操作,均在 Data 通道中按對應協議處理;它們屬於模型交互語義,與斷開傳輸連接是不同操作。
  • 音視頻數據通過媒體軌道傳輸。引用模型事件文檔時,媒體封裝和可用交互模式仍遵循本文的接入方式,不直接套用 WebSocket 的媒體發送方式。

最佳實踐