WebRTC实现视频通话
WebRTC实现内网下电脑和手机视频通话1.信令与信令服务器信令Signaling是 WebRTC 中用于建立、维护和终止连接的所有控制信息的统称。它本身不传输音视频数据只负责“协调双方如何开始通话”。主要涉及到的信令内容SDP协商offer/answer内容音视频格式、分辨率、编解码器、传输协议等“通话规格”。目的让双方确认“我们要用同一种语言讲话”。ICE候选candidate内容IP 地址、端口、传输协议UDP/TCP等“网络地址”。目的让双方知道“数据该往哪个物理地址发”。信令服务器在WebRTC中两个设备没有对方的IP地址也没法直接建立连接。信令服务器作为双方都能访问的公共节点让它们能互相找到彼此。2.视频通话实现思路核心逻辑2.1手机端和电脑端前端代码实现逻辑一致实现视频通话之前首先连接信令服务器这是后续两台设备传递信令的基础 前端连接信令服务器逻辑// ---------- 连接信令服务器 ----------functionconnectSignaling(){wsnewWebSocket(SIGNALING_SERVER_URL);ws.onopen(){setStatus(✅ 已连接到信令服务器等待对方...,#4CAF50);// 启用发起和接听按钮接听需要等对方发起才真正可用这里仅让界面可操作startBtn.disabledfalse;answerBtn.disabledfalse;// 但此时接听按钮按下其实还没用因为没有 offer后续逻辑会处理};......} 信令服务器代码逻辑importWebSocket,{WebSocketServer}fromws// import WebSocket from ws//创建websocket服务器监听端口constwssnewWebSocketServer({port:8090})//用于存储所有连接的客户端constclientsnewSet()console.log(信令服务器已启动监听端口: 8090);wss.on(connection,(ws){console.log(一个新客户端连接)clients.add(ws)//收到消息时转发给所有其他客户端ws.on(message,(message){console.log(收到消息并转发)//广播所有消息给其他客户端clients.forEach((client){if(client!wsclient.readyStateWebSocket.OPEN){client.send(message)}})})//客户端断开连接时清理ws.on(close,(){console.log(客户端断开了)clients.delete(ws)})})连接完成信令服务器后开始进行通话2.2首先在手机端点击通话按钮核心逻辑获取本地流显示手机端的视频流在本地//1.获取本地视频流if(!localStream){awaitgetLocalStream();}asyncfunctiongetLocalStream(){try{conststreamawaitnavigator.mediaDevices.getUserMedia({video:true,audio:true});localVideo.srcObjectstream;localStreamstream;returnstream;}catch(e){alert(无法获取摄像头/麦克风权限: e.message);setStatus(❌ 获取媒体设备失败,#f44336);throwe;}}创建点对点连接绑定onicecandidate事件当本地产生ICE候选时发送给远端// 2. 创建连接pccreatePeerConnection();isCallertrue;// ---------- 创建 PeerConnection ----------functioncreatePeerConnection(){constpcnewRTCPeerConnection(rtcConfig);// 当本地产生ICE候选时发送给远端pc.onicecandidate(event){if(event.candidate){sendMessage({type:candidate,candidate:event.candidate});}};// 当收到远端流时显示在远程视频标签pc.ontrack(event){if(event.streamsevent.streams[0]){remoteVideo.srcObjectevent.streams[0];}};// 将本地流添加到连接中if(localStream){localStream.getTracks().forEach(track{pc.addTrack(track,localStream);});}returnpc;}创建SDP协商offeroffer创建完后先存储到本地**setLocalDescription**函数调用完成后**本地产生候选**执行 onicecandidate事件发送候选给远端// 3. 创建 Offer (SDP)constofferawaitpc.createOffer();awaitpc.setLocalDescription(offer);发送offer到信令服务器然后发送给信令服务器将offer 的sdp属性// 4. 发送 Offer 给远端sendMessage({type:offer,sdp:offer.sdp,// 可以附带一些元信息用 type 区分即可});2.3信令服务器中候选转发到远端电脑端收到ICE候选将其添加到连接中functionconnectSignaling(){......ws.onmessageasync(event){constdataJSON.parse(awaitevent.data.text());console.log(收到信令:,data);// 根据消息类型处理switch(data.type){......casecandidate:// 收到ICE候选 → 添加到连接中if(pc){try{awaitpc.addIceCandidate(newRTCIceCandidate(data.candidate));}catch(e){console.warn(添加ICE候选失败:,e);}}break;default:break;}};......}2.4信令服务器中offer转发到远端电脑端 **远端收到后开始接听**改变状态提示提示对方发起通话请点击接听functionconnectSignaling(){......ws.onmessageasync(event){constdataJSON.parse(awaitevent.data.text());console.log(收到信令:,data);// 根据消息类型处理switch(data.type){caseoffer:// 收到呼叫邀请 → 自动弹出接听提示或直接调用 handleOffer// 这里我们让用户点击「接听」按钮来触发setStatus( 对方发起通话请点击「接听」,#2196F3);// 把收到的 offer 存到全局供点击接听时使用window.pendingOfferdata;// 启用接听按钮answerBtn.disabledfalse;break;......}};......} **获取本地流**显示电脑端的视频流在本地// 1. 获取本地流if(!localStream){awaitgetLocalStream();}创建点对点连接绑定onicecandidate事件当本地产生ICE候选时发送给远端// 2. 创建连接pccreatePeerConnection();isCallerfalse;// ---------- 创建 PeerConnection ----------functioncreatePeerConnection(){constpcnewRTCPeerConnection(rtcConfig);// 当本地产生ICE候选时发送给远端pc.onicecandidate(event){if(event.candidate){sendMessage({type:candidate,candidate:event.candidate});}};// 当收到远端流时显示在远程视频标签pc.ontrack(event){if(event.streamsevent.streams[0]){remoteVideo.srcObjectevent.streams[0];}};// 将本地流添加到连接中if(localStream){localStream.getTracks().forEach(track{pc.addTrack(track,localStream);});}returnpc;}设置远程描述将对方的offer手机端设置到远程描述// 3. 设置远程描述 (对方的 Offer)constofferwindow.pendingOffer;awaitpc.setRemoteDescription(newRTCSessionDescription(offer));创建SDP协商answeranswer创建完后先存储到本地setLocalDescription**函数调用完成后**本地产生候选执行onicecandidate事件发送候选给对方手机端// 4. 创建 Answerconstanswerawaitpc.createAnswer();awaitpc.setLocalDescription(answer);发送answer到信令服务器然后将answer 的sdp属性发送给信令服务器// 5. 发送 Answer 给远端sendMessage({type:answer,sdp:answer.sdp,});2.5信令服务器中候选转发到对方手机端收到ICE候选将其添加到连接中functionconnectSignaling(){......ws.onmessageasync(event){constdataJSON.parse(awaitevent.data.text());console.log(收到信令:,data);// 根据消息类型处理switch(data.type){......casecandidate:// 收到ICE候选 → 添加到连接中if(pc){try{awaitpc.addIceCandidate(newRTCIceCandidate(data.candidate));}catch(e){console.warn(添加ICE候选失败:,e);}}break;default:break;}};......}2.6信令服务器中answer转发到对方手机端手机端收到后 设置answer到远程描述通话建立functionconnectSignaling(){wsnewWebSocket(SIGNALING_SERVER_URL);ws.onopen(){setStatus(✅ 已连接到信令服务器等待对方...,#4CAF50);// 启用发起和接听按钮接听需要等对方发起才真正可用这里仅让界面可操作startBtn.disabledfalse;answerBtn.disabledfalse;// 但此时接听按钮按下其实还没用因为没有 offer后续逻辑会处理};ws.onmessageasync(event){constdataJSON.parse(awaitevent.data.text());console.log(收到信令:,data);// 根据消息类型处理switch(data.type){......caseanswer:// 收到应答 → 设置远程描述if(pc){awaitpc.setRemoteDescription(newRTCSessionDescription(data));setStatus( 通话已建立,#4CAF50);hangupBtn.disabledfalse;startBtn.disabledtrue;answerBtn.disabledtrue;}break;......}};......}2.7只有双方最终都完成了setRemoteDescription和addIceCandidate通话才最终建立