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

WebRTC切换Socket.io信令后仅重启服务器首次可用问题排查

1对1 WebRTC视频连接切换Socket.io信令后异常问题

搭建1对1 Peer-to-Peer(P2P)视频连接,双方可互看本地流(localStream)与远程流(remoteStream)。原使用AgoraRTM作为信令服务时运行正常,切换为Socket.io后出现异常:仅在重启WebSocket服务器后第一次运行正常,后续无法建立连接。已排查房间内用户数量、WebSocket连接ID、SDP Offer/Answer收发(确认正常),需定位问题根源并解决。

代码示例

main.js(WebRTC与客户端Socket.io逻辑)

let token = null;
let uid = String(Math.floor(Math.random() * 10000));

let queryString = window.location.search;
const urlSearch = new URLSearchParams(queryString);
const room = urlSearch.get("room");

if (!room) {
  window.location = "lobby.html";
}

let client;
let channel;
let socket;

const constraints = {
  video: {
    width: { min: 640, ideal: 1920, max: 1920 },
    height: { min: 480, ideal: 1080, max: 1920 },
    aspectRatio: 1.777777778,
  },
  audio: false,
};

const servers = {
  iceServers: [
    {
      urls: [
        "stun:stun.l.google.com:19302",
        "stun:stun1.l.google.com:19302",
        "stun:stun2.l.google.com:19302",
        "stun:stun3.l.google.com:19302",
        "stun:stun4.l.google.com:19302",
      ],
    },
  ],
};

const localVideoRef = document.getElementById("localVideo");
const remoteVideoRef = document.getElementById("remoteVideo");

let localStream;
let remoteStream;

let peerConnection;

const configureSignaling = async () => {
  socket = await io.connect("http://localhost:4000");

  socket.emit("join", { room, uid });
  socket.on("MemberJoined", handleMemberJoined);
  socket.on("MessageFromPeer", handleMessageFromPeer);

  socket.on("MemberLeft", handleMemberLeft)
};

const handleMemberLeft = async () => {
  remoteVideoRef.style.display = "none";
};

const handleMessageFromPeer = (m, uid) => {
  const message = JSON.parse(m.text);
  
  if(message.type !== "candidate") {
    console.log('handleMessageFromPeer: ', message, uid)
  }

  if (message.type === "offer") {
    createAnswer(uid, message.offer);
  }

  if (message.type === "answer") {
    addAnswer(message.answer);
  }

  if (message.type === "candidate") {
    if (peerConnection && peerConnection.currentRemoteDescription) {
      peerConnection.addIceCandidate(message.candidate);
    }
  }
};

const createLocalStream = async () => {
  localStream = await navigator.mediaDevices.getUserMedia(constraints);
  localVideoRef.srcObject = localStream;
};

const init = async () => {
  await configureSignaling();
  await createLocalStream();
};

const handleMemberJoined = async (uid) => {
  createOffer(uid);
};

let createOffer = async (uid) => {
  await createPeerConnection(uid);

  let offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);

  socket.emit(
    "sendMessageToPeer",
    { text: JSON.stringify({ type: "offer", offer: offer }) },
    uid
  );
};

let createPeerConnection = async (uid) => {
  peerConnection = new RTCPeerConnection(servers);

  remoteStream = new MediaStream();
  remoteVideoRef.srcObject = remoteStream;
  remoteVideoRef.style.display = "block";
  remoteVideoRef.classList.add("remoteFrame");

  if (!localStream) {
    await createLocalStream();
  }

  localStream.getTracks().forEach((track) => {
    peerConnection.addTrack(track, localStream);
  });

  peerConnection.ontrack = (event) => {
    event.streams[0].getTracks().forEach((track) => {
      remoteStream.addTrack(track);
    });
  };

  peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      socket.emit(
        "sendMessageToPeer",
        {
          text: JSON.stringify({
            type: "candidate",
            candidate: event.candidate,
          }),
        },
        uid
      );
    }
  };
};

let createAnswer = async (uid, offer) => {
  await createPeerConnection(uid);
  await peerConnection.setRemoteDescription(offer);
  
  const answer = await peerConnection.createAnswer();
  await peerConnection.setLocalDescription(answer);
  
  socket.emit(
    "sendMessageToPeer",
    { text: JSON.stringify({ type: "answer", answer: answer }) },
    uid
  );
};

let addAnswer = async (answer) => {
  if (!peerConnection.currentRemoteDescription) {
    peerConnection.setRemoteDescription(answer);
  }
};

let onLogout = async () => {
  remoteVideoRef.classList.remove("remoteFrame");
  await socket.emit('onLeaveRoom', room)
};

let onToggleCamera = async () => {
  const videoTrack = localStream
    .getTracks()
    .find((track) => track.kind === "video");
  if (videoTrack.enabled) {
    videoTrack.enabled = false;
    document.getElementById("camera-btn").style.backgroundColor =
      "rgb(255, 80, 80)";
  } else {
    videoTrack.enabled = true;
    document.getElementById("camera-btn").style.backgroundColor =
      "rgb(179, 102, 249, .9)";
  }
};

let onToggleMic = async () => {
  const audioTrack = localStream
    .getTracks()
    .find((track) => track.kind === "audio");
  if (audioTrack.enabled) {
    audioTrack.enabled = false;
    document.getElementById("mic-btn").style.backgroundColor =
      "rgb(255, 80, 80)";
  } else {
    audioTrack.enabled = true;
    document.getElementById("mic-btn").style.backgroundColor =
      "rgb(179, 102, 249, .9)";
  }
};

window.addEventListener("beforeunload", onLogout);

init();

index.js(WebSocket服务端)

const express = require("express");
const app = express();
const PORT = 4000;

const http = require("http").Server(app);
const cors = require("cors");

app.use(cors());

const socketIO = require("socket.io")(http, {
  cors: {
    origin: "http://127.0.0.1:5501",
  },
});

//Add this before the app.get() block
socketIO.on("connection", (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);
  console.log('CONNECTED')

  socket.on('join', async ({room, uid}) => {
    await socket.join(room);

    const connectedUsers = await socket.in(room).fetchSockets()

    console.log('room:', connectedUsers.length);

    if(connectedUsers.length) {
      console.log('PUMBA')
      socket.broadcast.emit('MemberJoined', uid)
    }
  })

  socket.on("onLeaveRoom", async (room) => {
    await socket.leave(room)
    socket.broadcast.emit('MemberLeft')
  })

  socket.on("disconnect", async (room) => {
    socket.broadcast.emit('MemberLeft')
  })

  socket.on('sendMessageToPeer', (data, uid) => {
    console.log('sendMessageTOPEER', uid)
    socket.broadcast.emit('MessageFromPeer', data, uid ) 
  });
});

app.get("/api", (req, res) => {
  res.json({
    message: "Hello world",
  });
});

http.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});

问题根源分析

  1. Socket.io消息广播范围错误:使用socket.broadcast.emit会向所有连接的客户端广播消息,而非仅目标房间内的用户。后续连接时,无关客户端会收到错误的信令消息,打乱WebRTC协商流程。
  2. UID与Socket连接标识混淆:客户端自定义的uid与Socket.io的socket.id(唯一连接标识)混用,导致信令消息无法精准发送到目标客户端。
  3. 未清理旧PeerConnection实例:用户离开或重新连接时,未关闭并销毁旧的peerConnection和remoteStream,新连接复用旧实例引发协商冲突。

解决方案与代码修改

1. 服务端(index.js)修改

核心修改点:

  • 维护房间用户映射表,关联socket.id与客户端uid
  • 所有房间内消息使用socket.to(room).emit或socketIO.to(targetSocketId).emit精准发送
  • 修正成员加入/离开的通知范围

修改后的服务端代码:

const express = require("express");
const app = express();
const PORT = 4000;

const http = require("http").Server(app);
const cors = require("cors");

app.use(cors());

const socketIO = require("socket.io")(http, {
  cors: {
    origin: "http://127.0.0.1:5501",
  },
});

// 维护房间用户映射:roomId -> Map(socketId => uid)
const roomUsers = new Map();

socketIO.on("connection", (socket) => {
  console.log(`⚡: ${socket.id} user just connected!`);

  socket.on('join', async ({room, uid}) => {
    await socket.join(room);
    
    // 初始化房间用户表
    if (!roomUsers.has(room)) {
      roomUsers.set(room, new Map());
    }
    roomUsers.get(room).set(socket.id, uid);

    const connectedUsers = await socket.in(room).fetchSockets();
    // 房间内已有其他用户时,通知旧用户新成员加入,同时通知新用户旧成员存在
    if (connectedUsers.length > 1) {
      // 向房间内除自己外的用户发送新成员信息
      socket.to(room).emit('MemberJoined', uid, socket.id);
      // 向新用户发送房间内已存在的用户信息
      const existingUsers = Array.from(roomUsers.get(room).entries())
        .filter(([sid]) => sid !== socket.id)
        .map(([sid, userUid]) => ({uid: userUid, socketId: sid}));
      socket.emit('ExistingMembers', existingUsers);
    }
  })

  socket.on("onLeaveRoom", async (room) => {
    await socket.leave(room);
    roomUsers.get(room)?.delete(socket.id);
    // 仅通知房间内用户
    socket.to(room).emit('MemberLeft');
  })

  socket.on("disconnect", async () => {
    // 遍历所有房间,移除当前socket的记录
    for (const [room, users] of roomUsers.entries()) {
      if (users.has(socket.id)) {
        users.delete(socket.id);
        socket.to(room).emit('MemberLeft');
        // 房间空了就删除
        if (users.size === 0) {
          roomUsers.delete(room);
        }
        break;
      }
    }
  })

  socket.on('sendMessageToPeer', (data, targetSocketId) => {
    // 精准发送给目标socket
    socketIO.to(targetSocketId).emit('MessageFromPeer', data);
  });
});

app.get("/api", (req, res) => {
  res.json({
    message: "Hello world",
  });
});

http.listen(PORT, () => {
  console.log(`Server listening on ${PORT}`);
});

2. 客户端(main.js)修改

核心修改点:

  • 添加资源清理函数,关闭旧PeerConnection并释放媒体流
  • 修正信令消息发送目标为socket.id而非自定义uid
  • 处理ExistingMembers事件,支持房间内已有用户时主动发起连接

修改后的客户端代码关键部分:

// 添加资源清理函数
const cleanupPeerConnection = () => {
  if (peerConnection) {
    peerConnection.close();
    peerConnection = null;
  }
  if (remoteStream) {
    remoteStream.getTracks().forEach(track => track.stop());
    remoteStream = null;
    remoteVideoRef.srcObject = null;
    remoteVideoRef.style.display = "none";
    remoteVideoRef.classList.remove("remoteFrame");
  }
};

let remoteSocketId = null; // 存储对端socketId

const configureSignaling = async () => {
  socket = await io.connect("http://localhost:4000");

  socket.emit("join", { room, uid });
  socket.on("MemberJoined", handleMemberJoined);
  socket.on("MessageFromPeer", handleMessageFromPeer);
  socket.on("MemberLeft", handleMemberLeft);
  // 新增:处理房间内已有用户的情况
  socket.on("ExistingMembers", handleExistingMembers);
  // 新增:socket断开时清理资源
  socket.on("disconnect", cleanupPeerConnection);
};

const handleMemberLeft = async () => {
  cleanupPeerConnection();
  remoteSocketId = null;
};

// 新增:处理房间内已有用户
const handleExistingMembers = (members) => {
  if (members.length > 0) {
    // 向第一个已存在用户发起offer
    remoteSocketId = members[0].socketId;
    createOffer(remoteSocketId);
  }
};

const handleMemberJoined = async (uid, socketId) => {
  remoteSocketId = socketId;
  createOffer(socketId);
};

const handleMessageFromPeer = (m) => {
  const message = JSON.parse(m.text);
  
  if(message.type !== "candidate") {
    console.log('handleMessageFromPeer: ', message)
  }

  if (message.type === "offer") {
    createAnswer(message.offer);
  }

  if (message.type === "answer") {
    addAnswer(message.answer);
  }

  if (message.type === "candidate") {
    if (peerConnection && peerConnection.currentRemoteDescription) {
      peerConnection.addIceCandidate(message.candidate);
    }
  }
};

let createOffer = async (targetSocketId) => {
  await createPeerConnection(targetSocketId);

  let offer = await peerConnection.createOffer();
  await peerConnection.setLocalDescription(offer);

  socket.emit(
    "sendMessageToPeer",
    { text: JSON.stringify({ type: "offer", offer: offer }) },
    targetSocketId
  );
};

let createPeerConnection = async (targetSocketId) => {
  // 创建前先清理旧连接
  cleanupPeerConnection();
  
  peerConnection = new RTCPeerConnection(servers);

  remoteStream = new MediaStream();
  remoteVideoRef.srcObject = remoteStream;
  remoteVideoRef.style.display = "block";
  remoteVideoRef.classList.add("remoteFrame");

  if (!localStream) {
    await createLocalStream();
  }

  localStream.getTracks().forEach((track) => {
    peerConnection.addTrack(track, localStream);
  });

  peerConnection.ontrack = (event) => {
    event.streams[0].getTracks().forEach((track) => {
      remoteStream.addTrack(track);
    });
  };

  peerConnection.onicecandidate = (event) => {
    if (event.candidate) {
      socket.emit(
        "sendMessageToPeer",
        {
          text: JSON.stringify({
            type: "candidate",
            candidate: event.candidate,
          }),
        },
        targetSocketId
      );
    }
  };
};

let createAnswer = async (offer) => {
  await createPeerConnection();
  
  await peerConnection.setRemoteDescription(offer);
  const answer = await peerConnection.createAnswer();
  await peerConnection.setLocalDescription(answer);
  
  socket.emit(
    "sendMessageToPeer",
    { text: JSON.stringify({ type: "answer", answer: answer }) },
    remoteSocketId
  );
};

// 修正addAnswer逻辑,去掉不必要的判断
let addAnswer = async (answer) => {
  try {
    await peerConnection.setRemoteDescription(answer);
  } catch (err) {
    console.error('设置远程描述失败:', err);
  }
};

let onLogout = async () => {
  cleanupPeerConnection();
  remoteSocketId = null;
  await socket.emit('onLeaveRoom', room);
};

window.addEventListener("beforeunload", () => {
  cleanupPeerConnection();
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 23:55:18