🤖 给 AI 的参考
查看 .md 原文
这是 Hearo(听见)的《WebRTC 接入(实验)》文档,可作为开发参考。文档链接:https://hearo.apps.aisp24.com/docs/webrtc.md

WebRTC 接入(实验)

实验性的浏览器 Opus 接入,含 ICE/TURN 与控制数据通道;默认生产路径为 WSS + PCM

⚠️ 状态:实验:WebRTC(实验)尚不是默认生产接入方式。优先使用 WebSocket 的 WSS + 16 kHz 单声道 PCM 链路。

WebRTC(实验)面向浏览器:浏览器可使用回声消除、降噪、抖动缓冲和拥塞控制,音频走 Opus(48 kHz,服务端统一转成 16 kHz PCM 进房间)。信令是一次性的(非 trickle):客户端把 SDP offer POST 给 gateway,拿回 answer,无需额外的 WebSocket 信令通道。

ℹ️ 本部署的网关地址https://hearo-gw.apps.aisp24.com(下方示例已代入,可直接复制使用)。

1. 获取 ICE 服务器

GET https://hearo-gw.apps.aisp24.com/v1/ice

返回 { "iceServers": [...] }(标准 RTCIceServer 数组):STUN 加上(若平台已配置)TURN 中继凭据。配了 TURN 时 gateway 自己会强制 relay 策略以穿透对称型 NAT。

javascript
const { iceServers } = await fetch("https://hearo-gw.apps.aisp24.com/v1/ice")
  .then((r) => r.json());
const pc = new RTCPeerConnection({ iceServers });

ℹ️ 自部署调优:STUN 服务器用 HEARO_STUN_URL 配置(默认 Google STUN;国内网络访问不到它只会拖慢 ICE 收集,建议换成可达的 STUN 或设 off 关闭);TURN 用 HEARO_TURN_URL/HEARO_TURN_USERNAME/HEARO_TURN_CREDENTIAL,或 Cloudflare Realtime(HEARO_CF_TURN_KEY_ID/HEARO_CF_TURN_API_TOKEN,由 gateway 代签短期凭据)。

2. 建立 PeerConnection 并交换 SDP

POST https://hearo-gw.apps.aisp24.com/v1/webrtc/offer?name=<昵称>&lang=<语言>

说明
Authorization请求头,必填:Bearer <CLIENT_TOKEN>
name / lang查询参数,可选。房间昵称 / 你说的语言(供字幕)
请求体JSON:{"type":"offer","sdp":"<你的 offer SDP>"}
返回JSON:{"type":"answer","sdp":"<answer SDP>"},ICE candidate 已内联(非 trickle)

offer 里需要包含一条音频轨(sendrecv)一个名为 control 的数据通道(名字必须是 control,其他名字会被忽略)。

javascript
const stream = await navigator.mediaDevices.getUserMedia({
  audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
});
stream.getTracks().forEach((t) => pc.addTrack(t, stream));

// 控制通道:与 WebSocket 文本控制平面等价(speech_start / interrupt / roster ...)
const dc = pc.createDataChannel("control");
dc.onmessage = (e) => handleEvent(JSON.parse(e.data));

// 播放下行音频
pc.ontrack = (e) => { audioEl.srcObject = e.streams[0]; };

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 非 trickle:给 ICE 收集留一小段时间(无需等到 complete——
// STUN 不可达时 complete 永远不来;host candidate 是即时的)
await new Promise((r) => {
  pc.onicegatheringstatechange = () => pc.iceGatheringState === "complete" && r();
  setTimeout(r, 2000);
});

const offerUrl = new URL("https://hearo-gw.apps.aisp24.com/v1/webrtc/offer");
offerUrl.searchParams.set("name", "浏览器访客");
offerUrl.searchParams.set("lang", "zh-CN");

const resp = await fetch(offerUrl, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ type: "offer", sdp: pc.localDescription.sdp }),
});
const answer = await resp.json();
await pc.setRemoteDescription({ type: "answer", sdp: answer.sdp });

服务端同样以“有界等待”收集自己的 ICE candidate 后返回 answer。

⚠️ WebRTC(实验)的客户端 Token 只能放在 Authorization: Bearer 请求头中。任何 token query 参数都会被拒绝,即使请求同时携带了有效的 Bearer 头。

3. 控制与事件:走数据通道

WebRTC(实验)路径下,control 数据通道镜像了 WebSocket 的文本控制平面:同样的 speech_start/interrupt/config/caption_start 控制消息和 roster/transcript/turn_*/caption 事件(完整列表见 WebSocket 第 3、4 节)。音频走媒体轨(Opus),无需手动分帧,也没有 audio 头加二进制帧的配对。

进房时房间广播的初始 roster 快照会由 gateway 缓存,待数据通道打开后立即补发——所以通道一打开你就能拿到当前的完整成员列表。

ℹ️ WebRTC(实验)与 WebSocket 的对应关系:把「二进制音频帧」换成「媒体轨」,把「文本帧」换成「数据通道消息」,其余协议(控制/事件、交互模式、邀请 AI)与 WebSocket 一致。