webrtc 1对1房间交流

97 阅读4分钟

HTML

页面元素

  • 用户列表展示
  • 本地video
  • 要使用的脚本script
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebRTC Example</title>
    <style>
.sent{
  color:aqua
}
.received{
  color:blueviolet
}

    </style>
</head>
<body>
    <h1>WebRTC Example</h1>
    <div class="user-list">
        <h3>用户列表</h3>
        <ul id="userList"></ul>
      </div> <input type="text" id="messageInput" placeholder="发送消息"> <button onclick="sendMessage()">发送</button>
      <p>远端消息: <span id="messageResponse"></span></p>
      <div style="display: flex; justify-content: space-between;">
        <video id="localVideo" autoplay muted playsinline></video>
        <video id="remoteVideo" autoplay playsinline></video>
      </div>
    <script src="app.js"></script>
</body>
</html>

主要逻辑代码

  • 首先要使用websocket双方互相交换sdf和ice
const socket = new WebSocket('ws://localhost:8080');
let clientId = Date.now().toString(); // 生成一个唯一的 ID

let roomName = 'default-room'; // 默认房间名称

注意:创建房间为了做进一步的隔离

  • 与心灵服务器建立链接的主要事件
socket.onopen = () => {

console.log('Connected to signaling server');

// 注册本地客户端并加入房间

socket.send(JSON.stringify({

type: 'join-room',

id: clientId,

roomName: roomName

}));

};

socket.onmessage = event => {
const data = JSON.parse(event.data);
//如果成功加入房间
if (data.type === 'join-success') {
    ...
}

//如果加入失败
if (data.type === 'join-failed') {...}

//如果是接收信令服务器的offer
if (data.type === 'offer') {...}

//如果是信令的回答
if (data.type === 'answer') {...}


//如果是信令的ice消息
if (data.type === 'candidate') {...}

}


大体的流程是

  1. 各自收集音视频流;

//获取本地媒体流并添加到连接

function addLocalStream(peerId,) {

navigator.mediaDevices.getUserMedia({ video: true, audio: true })

.then(stream => {

localStream=stream

localVideo.srcObject = stream;

})

.catch(error => console.error('Error accessing media devices.', error));

}
  1. 发起方发起offer;
const conn = new RTCPeerConnection({iceServers});
await conn.createOffer()//创建offer
await conn.setLocalDescription(offer)//设置本地描述
await socket.send(JSON.stringify({//向接收方发送自己offer

type: 'offer',

offer:offer_,

toId: peerId,

roomName: roomName

}));

  1. 接收方,接收发送方的offer
const conn = new RTCPeerConnection({iceServers});
await conn.setRemoteDescription(offer)//设置远程描述
await let answer =conn.createAnswer()//创建回复
await conn.setLocalDescription(answer)//创建本地描述
await socket.send(JSON.stringify({//将本地回复发送给发起方

type: 'answer',//给发起方

answer: answer,

toId: peerId,

roomName: roomName

}));
  1. 发起方设置远程描述
conn.setRemoteDescription(data.answer);

当然最重要的ice创建就在发起方和接收方创建各自的new RTCPeerConnection时处理


//将track添加到本地localStream
localStream.getTracks().forEach(track => {

console.log("track", track);

// console.log("stream", stream);

conn.addTrack(track, localStream);

});


// 处理 ICE 候选者

conn.onicecandidate = event => {

console.log("生成ice")

if (event.candidate) {

console.log(`ICE candidate for ${peerId}:`, event.candidate);

socket.send(JSON.stringify({

type: 'candidate',

candidate: event.candidate,

toId: peerId,

roomName: roomName

}));

}

};
//处理接收到的远程流

conn.ontrack = (event) => {

console.log("处理接收远处的流", event);

  


// 使用 event.streams[0] 来获取完整的流

const stream = event.streams[0];

// 如果已经存在视频元素,直接更新流

if (remoteVideos[peerId]) {

const videoElement = remoteVideos[peerId];

videoElement.srcObject = stream;

// Optional: 更新其他属性,比如设置为 Playing

} else {

// 根据规范,可以直接访问 event.track,但 event.streams 是 best practice

const videoElement = document.createElement('video');

videoElement.id = `remoteVideo-${peerId}`;

videoElement.autoplay = true;

videoElement.playsInline = true;

videoElement.srcObject = stream; // 绑定流到 video 元素

// 将视频元素添加到页面中

document.body.appendChild(videoElement);

// 存储远程视频元素,方便后续操作

remoteVideos[peerId] = videoElement;

// 监听媒体流结束

stream.onended = () => {

// 当媒体流结束时,移除对应的视频元素

videoElement.remove();

delete remoteVideos[peerId];

};

}



};


  1. 各自交换ice
if (data.type === 'candidate') {

const conn = localPc

console.log("是否有远端的conn",conn)

console.log("ice",data.candidate)

if (conn) {

console.log("交换开始")

conn.addIceCandidate(data.candidate

);

}
}

6.发起发要创建信息通道用于文字,文件,图片等的交流,也是在创建new RTCPeerConnection时处理

// 发起方创建数据通道并存储

const localChannel= conn.createDataChannel('faqiChannel', {

ordered: true,

protocol: "json",

});

setupChannelEvents(localChannel, peerId);

dataChannelMap.set(peerId,localChannel) // 存储到对应 peerId 的通道

接收方可以在设置远程描述时找到发起方创建的通道相关信息,在ice交换完可利用ondatachannel监听通道信息

//  接收方通过事件获取对方创建的通道

localPc.ondatachannel = (event) => {

const receiveChannel = event.channel;

setupChannelEvents(receiveChannel, peerId);

dataChannelMap.set(peerId,receiveChannel) // 存储到对应 peerId 的通道

} ;

信令服务端代码如下:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

  


console.log('WebSocket server started on port 8080');

  


// 房间管理,结构为 Map<roomName, Map<peerId, WebSocket>>

const rooms = new Map();

  


wss.on('connection', ws => {

let currentId = Date.now().toString(); // 临时 ID

let currentRoom = null;

  


ws.on('message', message => {

console.log(`Received message from ${currentId}: ${message}`);

try {

const data = JSON.parse(message);

if (data.type === 'join-room') {

// 客户端加入房间

const roomName = data.roomName;

const peerId = data.id;

  


// 创建房间(如果不存在)

if (!rooms.has(roomName)) {

rooms.set(roomName, new Map());

}

  


// 加入房间

const room = rooms.get(roomName);

if (room.has(peerId)) {

ws.send(JSON.stringify({

type: 'join-failed',

message: 'Peer ID already exists in the room',

roomName

}));

return;

}

  


// 更新客户端 ID 和房间

room.set(peerId, ws);

currentId = peerId;

currentRoom = roomName;

  


// // 告知其他成员新成员加入

const peers = Array.from(room.keys()).filter(key => key !== peerId);

// room.forEach((clientWs, clientPeerId) => {

// if (clientPeerId !== peerId) {

// clientWs.send(JSON.stringify({

// type: 'new-peer',

// peerId: peerId,

// roomName

// }));

// }

// });

  


// 发送成功消息

ws.send(JSON.stringify({

type: 'join-success',

id: peerId,

peers: peers,

roomName

}));

  


console.log(`Client ${peerId} joined room ${roomName}`);

} else if (data.type === 'offer') {

  


console.log("转发Offer")

console.log("当前currentId",currentId)

console.log(data.offer)

// 转发 Offer

const toId = data.toId;

const room = rooms.get(currentRoom);

const peers = Array.from(room.keys()).filter(key => key !== toId);

console.log("来自toId",toId)

if (room) {

const toWs = room.get(toId);

if (toWs) {

toWs.send(JSON.stringify({

type: 'offer',

offer: data.offer,

fromId: currentId,

roomName: currentRoom,

peers:peers

}));

} else {

console.error(`Remote peer ${toId} not found in room ${currentRoom}`);

}

}

} else if (data.type === 'answer') {

console.log("转发Answer")

// 转发 Answer

const toId = data.toId;

const room = rooms.get(currentRoom);

if (room) {

const toWs = room.get(toId);

if (toWs) {

toWs.send(JSON.stringify({

type: 'answer',

answer: data.answer,

fromId: currentId,

roomName: currentRoom

}));

} else {

console.error(`Remote peer ${toId} not found in room ${currentRoom}`);

}

}

} else if (data.type === 'candidate') {

console.log("转发Candidate")

// 转发 ICE Candidate

const toId = data.toId;

const room = rooms.get(currentRoom);

if (room) {

const toWs = room.get(toId);

if (toWs) {

toWs.send(JSON.stringify({

type: 'candidate',

candidate: data.candidate,

fromId: currentId,

roomName: currentRoom

}));

} else {

console.error(`Remote peer ${toId} not found in room ${currentRoom}`);

}

}

}

} catch (error) {

console.error('Error processing message:', error);

}

});

  


ws.on('close', () => {

if (currentRoom) {

const room = rooms.get(currentRoom);

if (room) {

room.delete(currentId);

console.log(`Client ${currentId} left room ${currentRoom}`);

// 通知其他成员该用户已离开

room.forEach((clientWs) => {

clientWs.send(JSON.stringify({

type: 'peer-disconnected',

peerId: currentId,

roomName: currentRoom,

}));

});

}

}

});

});

现在就实现了一对一的视频交流,如果同学们想要深入了解原理,可以去看看SCTP协议

总结,websocket就是个转发的服务器,一旦webrtc ice交换完成通道建立及可基于webrtc信息交换;