You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebRTC场景WebSocket调用send方法报CONNECTING状态报错排查

问题诊断

控制台抛出的Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state错误触发逻辑非常明确:
WebSocket实例初始化后,需要和服务端完成TCP握手、TLS校验、WebSocket协议升级全流程,这个过程中连接处于CONNECTING状态,只有连接正式建立、触发onopen事件后,连接状态切换为OPEN,才能正常调用send()方法传输数据。
现有代码在页面加载时就初始化WebSocket,但用户点击按钮触发sendUsername发送信令时,WebSocket连接还没完成建立,直接调用send就会抛出上述错误,导致后续房间创建、信令交互全流程中断,和端口、地址配置没有关系。
另外代码里还存在几处WebRTC旧API兼容问题、信令发送顺序问题,会在后续运行中触发其他异常。

修复步骤
  • 为WebSocket增加待发送消息队列,连接未就绪时先把消息存入队列,等连接成功后批量发送,从逻辑上杜绝CONNECTING状态下调用send的问题
  • 补全WebSocket的onopen/onerror/onclose事件监听,方便排查连接异常
  • 修正WebRTC旧API写法,调整SDP offer发送顺序,适配现代浏览器标准
  • 可在交互层增加控制:WebSocket连接就绪前禁用发送、呼叫按钮,避免用户操作过早触发异常
修复后完整代码
// 初始化WebSocket实例与待发消息队列
const webSocket = new WebSocket("wss://domain:443")
const messageQueue = []
let username
let localStream
let peerConn
let isVideo = true

// WebSocket连接建立成功后,清空积压的待发消息
webSocket.onopen = () => {
  while (messageQueue.length) {
    const pendingData = messageQueue.shift()
    webSocket.send(JSON.stringify(pendingData))
  }
  // 连接成功后再放开用户名提交、呼叫按钮的可点击状态
  document.getElementById("join-btn").disabled = false
}

webSocket.onerror = (err) => {
  console.error("WebSocket连接异常:", err)
}

webSocket.onclose = () => {
  console.log("WebSocket连接已断开")
}

webSocket.onmessage = (event) => {
  handleSignallingData(JSON.parse(event.data))
}

function handleSignallingData(data) {
  switch (data.type) {
    case "answer":
      peerConn.setRemoteDescription(new RTCSessionDescription(data.answer))
      break
    case "candidate":
      peerConn.addIceCandidate(new RTCIceCandidate(data.candidate))
  }
}

function sendUsername() {
  username = document.getElementById("username-input").value
  sendData({
    type: "store_user"
  })
}

function sendData(data) {
  data.username = username
  // 连接已就绪直接发送,未就绪存入队列
  if (webSocket.readyState === WebSocket.OPEN) {
    webSocket.send(JSON.stringify(data))
  } else {
    messageQueue.push(data)
  }
}

async function startCall() {
  document.getElementById("video-call-div").style.display = "inline"

  try {
    // 替换为标准Promise版本的媒体采集API
    localStream = await navigator.mediaDevices.getUserMedia({
      video: {
        frameRate: 24,
        width: {
          min: 480, ideal: 720, max: 1280
        },
        aspectRatio: 1.33333
      },
      audio: true
    })
    document.getElementById("local-video").srcObject = localStream

    const configuration = {
      iceServers: [
        {
          urls: [
            "stun:stun.l.google.com:19302",
            "stun:stun1.l.google.com:19302",
            "stun:stun2.l.google.com:19302"
          ]
        }
      ]
    }

    peerConn = new RTCPeerConnection(configuration)
    // 替换已废弃的addStream方法,用addTrack添加媒体轨
    localStream.getTracks().forEach(track => {
      peerConn.addTrack(track, localStream)
    })

    // 替换已废弃的onaddstream事件,用ontrack监听远端媒体流
    peerConn.ontrack = (e) => {
      document.getElementById("remote-video").srcObject = e.streams[0]
    }

    peerConn.onicecandidate = (e) => {
      if (!e.candidate) return
      sendData({
        type: "store_candidate",
        candidate: e.candidate
      })
    }

    await createAndSendOffer()
  } catch (error) {
    console.log("媒体采集/呼叫流程异常:", error)
  }
}

async function createAndSendOffer() {
  try {
    const offer = await peerConn.createOffer()
    // 先设置本地SDP,再发送offer信令,修正原有顺序问题
    await peerConn.setLocalDescription(offer)
    sendData({
      type: "store_offer",
      offer: offer
    })
  } catch (error) {
    console.log("创建offer异常:", error)
  }
}

function muteVideo() {
  isVideo = !isVideo
  localStream.getVideoTracks()[0].enabled = isVideo
}
额外说明
  • 上述代码默认你在HTML里给加入房间/提交用户名的按钮加了id="join-btn"、初始状态加了disabled属性,不需要的话可以删掉对应控制逻辑
  • 如果要做断线重连,只需要在onclose回调里加重新初始化WebSocket的逻辑即可,消息队列的逻辑可以直接复用
  • 信令服务端如果有心跳要求,直接在onopen回调里启动定时心跳发送即可,不会出现状态错误

内容的提问来源于stack exchange,提问作者MrCipher

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:51:21