开发步骤
1.客户端显示界面
2.打开摄像头并显示到页面
3.websocket连接
4.join、new-peer、resp-join信令实现
5.leave、peer-leave信令实现
6.offer、answer、candidate信令实现
7.综合调试和完善
1.客户端显示界面
步骤:创建html页面
主要是input、button、video控件的布局。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>webrtc-demo</title><style>body {font-family: Arial, sans-serif;background-color: #f0f0f0;margin: 0;padding: 20px;text-align: center;}h1 {color: #333;}#buttons {margin-bottom: 20px;}input[type="text"] {padding: 10px;width: 250px;border: 1px solid #ccc;border-radius: 5px;margin-right: 10px;}button {padding: 10px 15px;border: none;border-radius: 5px;background-color: #4CAF50;color: white;cursor: pointer;transition: background-color 0.3s;}button:hover {background-color: #45a049;}#videos {display: flex;justify-content: center;margin-top: 20px;}video {border: 2px solid #ccc;border-radius: 5px;margin: 0 10px;width: 600px; /* 增加视频宽度 */height: 400px; /* 设置视频高度 */}</style>
</head>
<body><h1>webrtc</h1><div id="buttons"><input id="roomid" type="text" placeholder="请输入房间id" maxlength="50"/><button id="joinBtn" type="button">加入房间</button><button id="leaveBtn" type="button">离开房间</button></div><div id="videos"><video id="localVideo" autoplay playsinline muted controls>本地视频窗口</video><video id="remoteVideo" autoplay playsinline muted controls>远程视频窗口</video></div><script type="module" src="js/main.js"></script><script type="module" src="js/adapter-latest.js"></script>
</body>
</html>
2.打开摄像头并显示到页面
function openLocalStream() {//初始化本地码流navigator.mediaDevices.getUserMedia({ //初始化本地码流信息audio: true,video: { width: 640, height: 480 }}).then(function (stream) { // 打开本地码流console.log('Open local stream');localVideo.srcObject = stream;localStream = stream;}).catch(function (e) { //错误cbalert("getUserMedia() error: " + e.name);});
}
document.getElementById('joinBtn').onclick = function () {console.log("加入按钮被点击");openLocalStream();
};function closeLocalStream() {if (localStream) {localStream.getTracks().forEach(track => {track.stop();});localStream = null;localVideo.srcObject = null;}
}
document.getElementById('leaveBtn').onclick = function () {console.log("离开按钮被点击");closeLocalStream();
};
3.websocket连接
封装websocket
class RTCEngine {constructor(wsUrl) {this.wsUrl = wsUrl;this.signaling = null;this.createWebSocket();}createWebSocket() {this.signaling = new WebSocket(this.wsUrl);this.signaling.onopen = () => this.onOpen();this.signaling.onmessage = (ev) => this.onMessage(ev);this.signaling.onerror = (ev) => this.onError(ev);this.signaling.onclose = (ev) => this.onClose(ev);}onOpen() {console.log("WebSocket opened.");//开启心跳包this.timerId = setInterval(() => {var jsonMsg = {'cmd': SIGNAL_TYPE_HEARTBEAT,'type': 'active'};this.sendMessage(JSON.stringify(jsonMsg));}, HEARTBEAT_INTERVAL);}onError(event) {console.log("onError: " + event.data);}onClose(event) {console.log("onClose -> code: " + event.code + ", reason: " + event.reason);//关闭心跳包clearInterval(conn.timerId);}sendMessage(message) {this.signaling.send(message);}hanleSendHeartBeat(message) {var jsonMsg = {'cmd': SIGNAL_TYPE_HEARTBEAT,'type':'ok'};this.sendMessage(JSON.stringify(jsonMsg));}onMessage(event) {console.log("onMessage: " + event.data);let jsonMsg;try {jsonMsg = JSON.parse(event.data);} catch (e) {console.warn("onMessage parse JSON failed: " + e);return;}// 处理从服务器接收到的消息switch (jsonMsg.cmd) {case SIGNAL_TYPE_NEW_PEER:handleRemoteNewPeer(jsonMsg); //doOfferbreak;case SIGNAL_TYPE_RESP_JOIN:handleResponseJoin(jsonMsg);break;case SIGNAL_TYPE_PEER_LEAVE:handleRemotePeerLeave(jsonMsg);break;case SIGNAL_TYPE_OFFER:handleRemoteOffer(jsonMsg); //doAnswerbreak;case SIGNAL_TYPE_ANSWER:handleRemoteAnswer(jsonMsg);break;case SIGNAL_TYPE_CANDIDATE:handleRemoteCandidate(jsonMsg);breakcase SIGNAL_TYPE_OVERLOAD:alert("房间号人数已满,请稍后再试");break;case SIGNAL_TYPE_HEARTBEAT:if(jsonMsg.type == 'active')this.hanleSendHeartBeat(jsonMsg);//console.log("收到心跳包:"+jsonMsg.type);break;default:console.warn("unknown cmd: " + jsonMsg.cmd);break;}}
}// 实例化 RTCEngine
const rtcEngine = new RTCEngine(SERVERADDR);
4.join、new-peer、resp-join overload信令设计
信令设计:
RTCEngineWS.js websocket封装类负责接收,发送消息以及连接状态管理。
signal.js 信令的设计集合
main.js 主逻辑
思路:(1)点击加入开妞;(2)响应加入按钮事件;(3)将join发送给服务器;(4)服务器 根据当前房间的人数
做处理,如果房间已经有人则通知房间里面的人有新人加入(newpeer),并通知自己房间里面是什么人(resp-join)。如果房间存在2个 则返回房间号人数已满
var jsonMsg = {'cmd': 'join','roomId': roomId,'uid': localUserId,
};
var jsonMsg = {'cmd': 'resp‐join','remoteUid': remoteUid
};
var jsonMsg = {'cmd': 'new‐peer','remoteUid': uid
};
var jsonMsg = {'cmd': 'overload',
};
5.leave、peer-leave 信令实现
思路:(1)点击离开按钮;(2)响应离开按钮事件;(3)将leave发送给服务器;(4)服
务器处理leave,将发送者删除并通知房间(peer leave)的其他人;(5)房间的其他人在客户
端响应peer leave事件。
var jsonMsg = {'cmd': 'leave','roomId': roomId,'uid': localUserId,
};
var jsonMsg = {'cmd': 'peer‐leave','remoteUid': uid
};
var jsonMsg = {'cmd': 'peer‐leave','remoteUid': uid
};
6.offer、answer、candidate信令实现
思路:
(1C)收到newpeer (handleRemoteNewPeer处理),作为发起者创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(2C)创建offer sdp,设置本地sdp,并将offer sdp发送到服务器;
(3SSS)服务器收到offer sdp 转发给指定的remoteClient;
(4C)接收者收到offer,也创建RTCPeerConnection,绑定事件响应函数,加入本地流;
(5C)接收者设置远程sdp,并创建answer sdp,然后设置本地sdp并将answer sdp发送到服务器;
(6SSS)服务器收到answer sdp 转发给指定的remoteClient;
(7C)发起者收到answer sdp,则设置远程sdp;
(8C)发起者和接收者都收到ontrack回调事件,获取到对方码流的对象句柄;(9)发起者和接收者都开始请求打洞,通过onIceCandidate获取到打洞信息(candidate)并发送给对方
(10)如果P2P能成功则进行P2P通话,如果P2P不成功则进行中继转发通话。
offer、answer、candidate分别如下
var jsonMsg = {'cmd': SIGNAL_TYPE_OFFER,'roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)
};
var jsonMsg = {'cmd': SIGNAL_TYPE_ANSWER,'roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)
};
var jsonMsg = {'cmd': SIGNAL_TYPE_CANDIDATE,'roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(event.candidate)
};
7.综合调试和完善
正常退出和关闭工作
思路:
(1)点击离开时,要将本地摄像头和麦克风关闭; 要将RTCPeerConnection关闭(close);
(2)检测到客户端退出时,服务器再次检测该客户端是否已经退出房间。
总结
信令服务器设计,主要是转发客户端发来的消息到对端,根据类型进行转发。
项目链接
jbj62/webrtc-demo - 码云 - 开源中国
学习资料分享
0voice · GitHub