Parcourir la source

音视频通话支持分布式场景

wangqi49 il y a 5 mois
Parent
commit
338979c5df

+ 1 - 1
src/main/java/com/webchat/common/enums/RedisMessageChannelTopicEnum.java

@@ -5,7 +5,7 @@ import lombok.Getter;
 @Getter
 public enum RedisMessageChannelTopicEnum {
 
-    CHAT, PUSH_ARTICLE, APPLY;
+    CHAT, PUSH_ARTICLE, APPLY, VIDEO_OFFER_SEND, VIDEO_OFFER_RECEIVER;
 
     public String getChannel() {
         return this.name();

+ 8 - 0
src/main/java/com/webchat/config/configuration/RedisConfig.java

@@ -4,6 +4,8 @@ import com.webchat.common.enums.RedisMessageChannelTopicEnum;
 import com.webchat.service.listener.RedisApplyMessageListener;
 import com.webchat.service.listener.RedisChatMessageListener;
 import com.webchat.service.listener.RedisPushMessageListener;
+import com.webchat.service.listener.RedisVideoOfferReceiveMessageListener;
+import com.webchat.service.listener.RedisVideoOfferSendMessageListener;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -21,6 +23,10 @@ public class RedisConfig {
     private RedisPushMessageListener pushMessageListener;
     @Resource
     private RedisApplyMessageListener redisApplyMessageListener;
+    @Resource
+    private RedisVideoOfferSendMessageListener videoOfferSendMessageListener;
+    @Resource
+    private RedisVideoOfferReceiveMessageListener videoOfferReceiveMessageListener;
 
     @Bean
     public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory) {
@@ -31,6 +37,8 @@ public class RedisConfig {
         container.addMessageListener(chatMessageListener, new ChannelTopic(RedisMessageChannelTopicEnum.CHAT.getChannel()));
         container.addMessageListener(pushMessageListener, new ChannelTopic(RedisMessageChannelTopicEnum.PUSH_ARTICLE.getChannel()));
         container.addMessageListener(redisApplyMessageListener, new ChannelTopic(RedisMessageChannelTopicEnum.APPLY.getChannel()));
+        container.addMessageListener(videoOfferSendMessageListener, new ChannelTopic(RedisMessageChannelTopicEnum.VIDEO_OFFER_SEND.getChannel()));
+        container.addMessageListener(videoOfferReceiveMessageListener, new ChannelTopic(RedisMessageChannelTopicEnum.VIDEO_OFFER_RECEIVER.getChannel()));
         return container;
     }
 

+ 75 - 0
src/main/java/com/webchat/controller/client/VideoChatWebSocket.java

@@ -0,0 +1,75 @@
+package com.webchat.controller.client;
+
+import com.webchat.common.enums.RedisMessageChannelTopicEnum;
+import com.webchat.common.util.JsonUtil;
+import com.webchat.common.util.SpringContextUtil;
+import com.webchat.domain.vo.request.mess.VideoChatMessageRequestVO;
+import com.webchat.service.listener.RedisMessageSender;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import javax.websocket.OnClose;
+import javax.websocket.OnError;
+import javax.websocket.OnMessage;
+import javax.websocket.OnOpen;
+import javax.websocket.Session;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.websocket.*;
+
+@Data
+@Slf4j
+@Component
+@ServerEndpoint(value = "/ws/video-chat/{userId}")
+public class VideoChatWebSocket {
+
+    //存储客户端的连接对象,每个客户端连接都会产生一个连接对象
+    public static ConcurrentHashMap<String, VideoChatWebSocket> clients = new ConcurrentHashMap();
+
+    //每个连接都会有自己的会话
+    private Session session;
+
+    private String userId;
+
+    @OnOpen
+    public void open(@PathParam("userId") String userId, Session session, EndpointConfig config){
+
+        log.info("音视频通话用户{}的客户端连接成功!", userId);
+        clients.put(userId, this);
+        this.session = session;
+        this.userId = userId;
+    }
+
+    @OnClose
+    public void close(){
+        clients.remove(userId);
+    }
+
+    @OnError
+    public void error(Throwable error){
+        error.printStackTrace();
+    }
+
+    @OnMessage
+    public void onMessage(String message) {
+        log.info("收到来自用户{}的消息:{}", userId, message);
+        VideoChatMessageRequestVO chatMessage = JsonUtil.fromJson(message, VideoChatMessageRequestVO.class);
+        if (chatMessage == null) {
+            return;
+        }
+        RedisMessageSender redisMessageSender = SpringContextUtil.getBean(RedisMessageSender.class);
+        if (chatMessage.getType().equals("offer")) {
+            // 发送通过邀请提醒消息
+            // 支持分布式场景,走广播模式
+            redisMessageSender.sendMessage(RedisMessageChannelTopicEnum.VIDEO_OFFER_SEND.getChannel(), message);
+            // 发送对话数据
+            redisMessageSender.sendMessage(RedisMessageChannelTopicEnum.VIDEO_OFFER_RECEIVER.getChannel(), message);
+            return;
+        }
+        // 发送对话数据
+        redisMessageSender.sendMessage(RedisMessageChannelTopicEnum.VIDEO_OFFER_RECEIVER.getChannel(), message);
+    }
+}
+

+ 0 - 127
src/main/java/com/webchat/controller/client/VideoWebSocket.java

@@ -1,127 +0,0 @@
-package com.webchat.controller.client;
-
-import com.google.common.collect.Sets;
-import com.webchat.common.util.JsonUtil;
-import com.webchat.common.util.SpringContextUtil;
-import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
-import com.webchat.domain.vo.request.mess.VideoChatMessageRequestVO;
-import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
-import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
-import com.webchat.service.UserService;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections.CollectionUtils;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-import javax.websocket.OnClose;
-import javax.websocket.OnError;
-import javax.websocket.OnMessage;
-import javax.websocket.OnOpen;
-import javax.websocket.Session;
-import javax.websocket.server.PathParam;
-import javax.websocket.server.ServerEndpoint;
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.websocket.*;
-
-@Slf4j
-@Component
-@ServerEndpoint(value = "/ws/video-chat/{userId}")
-public class VideoWebSocket {
-
-    //存储客户端的连接对象,每个客户端连接都会产生一个连接对象
-    private static ConcurrentHashMap<String, VideoWebSocket> map = new ConcurrentHashMap();
-
-    //每个连接都会有自己的会话
-    private Session session;
-
-    private String userId;
-
-    @OnOpen
-    public void open(@PathParam("userId") String userId, Session session, EndpointConfig config){
-
-        log.info("音视频通话用户{}的客户端连接成功!", userId);
-        map.put(userId, this);
-        this.session = session;
-        this.userId = userId;
-    }
-
-    @OnClose
-    public void close(){
-        map.remove(userId);
-    }
-
-    @OnError
-    public void error(Throwable error){
-        error.printStackTrace();
-    }
-
-    @OnMessage
-    public void onMessage(String message) throws IOException {
-
-        log.info("收到来自用户{}的消息:{}", userId, message);
-
-        VideoChatMessageRequestVO chatMessage = JsonUtil.fromJson(message, VideoChatMessageRequestVO.class);
-        if (chatMessage == null) {
-            return;
-        }
-        UserService userService = SpringContextUtil.getBean(UserService.class);
-        String sendUserId = chatMessage.getUserId();
-        String receiverId = chatMessage.getTargetUserId();
-        UserBaseResponseInfoVO sender = userService.getUserInfoByUserId(sendUserId);
-        UserBaseResponseInfoVO receiver = userService.getUserInfoByUserId(receiverId);
-        if (sender.getRoleCode() > 2 || receiver.getRoleCode() > 2) {
-            // 音视频当前仅支持普通用户之间对话
-            return;
-        }
-
-        if (chatMessage.getType().equals("offer")) {
-            // 视频对话发送offer,这里走的是普通聊天,推送音视频请求给接收人
-            ChatMessageResponseVO chatMessageResponseVO = new ChatMessageResponseVO();
-            chatMessageResponseVO.setVideoOffer(true);
-            chatMessageResponseVO.setSenderId(sendUserId);
-            chatMessageResponseVO.setReceiverId(receiverId);
-            chatMessageResponseVO.setSenderName(sender.getUserName());
-            String offerMessage = JsonUtil.toJsonString(chatMessageResponseVO);
-            // 发送通过邀请提醒消息
-            Session receiverSession = ChatWebSocket.clients.get(receiverId).getSession();
-            receiverSession.getAsyncRemote().sendText(offerMessage);
-            // offer
-            Set<String> receiverUserIds = new HashSet<>();
-            receiverUserIds.add(receiverId);
-            sendMessageTo(message, receiverUserIds);
-            return;
-        }
-
-        Set<String> receiverUserIds = new HashSet<>();
-        receiverUserIds.add(receiverId);
-        sendMessageTo(message, receiverUserIds);
-    }
-
-    public void sendMessageTo(String message, Set<String> receiver) throws IOException {
-        if (CollectionUtils.isEmpty(receiver)) {
-            return;
-        }
-        Set<Map.Entry<String, VideoWebSocket>> entries = map.entrySet();
-        for (Map.Entry<String, VideoWebSocket> entry : entries) {
-            if(receiver.contains(entry.getKey())){
-                //将消息转发到其他非自身客户端
-                entry.getValue().send(message);
-            }
-        }
-    }
-
-    public void send(String message) throws IOException {
-        if(session.isOpen()){
-            session.getBasicRemote().sendText(message);
-        }
-    }
-
-    public int  getConnetNum(){
-        return map.size();
-    }
-}
-

+ 14 - 0
src/main/java/com/webchat/service/RedisService.java

@@ -48,6 +48,10 @@ public class RedisService {
     /***
      * 批量Get
      * @param keys
+     *
+     * [1,2,3]
+     * [u1,null,u3]
+     *
      * @return
      */
     public List<String> mget(final List<String> keys) {
@@ -89,6 +93,16 @@ public class RedisService {
         return setNxPx(key, requestId, liveTime);
     }
 
+    /***
+     * @param key
+     * @param value
+     * @param exptime
+     * @return
+     *
+     * 1 ---> setnx(key, none , 2000) return true;
+     * 2 ---> setnx return false;
+     */
+
     public boolean setNxPx(final String key, final String value, final long exptime) {
         Boolean result = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
             RedisSerializer valueSerializer = redisTemplate.getValueSerializer();

+ 1 - 1
src/main/java/com/webchat/service/UserService.java

@@ -455,7 +455,7 @@ public class UserService {
         userEntity.setPassword(md5Pwd(groupId));
         userEntity.setRoleCode(RoleCodeEnum.GROUP.getCode());
         userEntity.setCreateBy(createUserId);
-        // 注册用户
+        // 创建群组
         userDAO.save(userEntity);
         // 注册成功,事务结束后刷新用户缓存信息
         TransactionSyncManagerUtil.registerSynchronization(() -> {

+ 3 - 3
src/main/java/com/webchat/service/listener/RedisChatMessageListener.java

@@ -130,13 +130,13 @@ public class RedisChatMessageListener implements MessageListener {
         }
     }
 
-    public void sendMessageTo(String message, Set<String> receiver) throws IOException {
-        if (CollectionUtils.isEmpty(receiver)) {
+    public void sendMessageTo(String message, Set<String> receivers) throws IOException {
+        if (CollectionUtils.isEmpty(receivers)) {
             return;
         }
         for (ChatWebSocket chat : ChatWebSocket.clients.values()) {
             // 支持点对点,群聊模式
-            if (receiver.contains(chat.getUserId())) {
+            if (receivers.contains(chat.getUserId())) {
                 Session session = chat.getSession();
                 if (session != null && session.isOpen()) {
                     session.getAsyncRemote().sendText(message);

+ 64 - 0
src/main/java/com/webchat/service/listener/RedisVideoOfferReceiveMessageListener.java

@@ -0,0 +1,64 @@
+package com.webchat.service.listener;
+
+import com.webchat.common.util.JsonUtil;
+import com.webchat.common.util.SpringContextUtil;
+import com.webchat.controller.client.ChatWebSocket;
+import com.webchat.controller.client.VideoChatWebSocket;
+import com.webchat.domain.vo.request.mess.VideoChatMessageRequestVO;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
+import com.webchat.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+import java.io.IOException;
+
+@Slf4j
+@Component
+public class RedisVideoOfferReceiveMessageListener implements MessageListener {
+
+    private final RedisTemplate redisTemplate;
+
+    public RedisVideoOfferReceiveMessageListener(@Qualifier("redisTemplate") RedisTemplate redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Override
+    public void onMessage(Message message, byte[] bytes) {
+        String channel = (String) redisTemplate.getStringSerializer().deserialize(message.getChannel());
+        String messageStr = (String) redisTemplate.getValueSerializer().deserialize(message.getBody());
+        log.info("redis message listener ======> channel:{}, message:{}", channel, messageStr);
+        this.handleVideoChattingMessage(messageStr);
+    }
+    /**
+     * 发送音视频通话邀请
+     *
+     * @param message
+     */
+    private void handleVideoChattingMessage(String message) {
+        VideoChatMessageRequestVO chatMessage = JsonUtil.fromJson(message, VideoChatMessageRequestVO.class);
+        if (chatMessage == null) {
+            return;
+        }
+        String receiverId = chatMessage.getTargetUserId();
+        // 校验用户ws 连接session是否在当前机器Hash表
+        VideoChatWebSocket ws = VideoChatWebSocket.clients.get(receiverId);
+        if (ws == null) {
+            return;
+        }
+        Session session = ws.getSession();
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        try {
+            session.getBasicRemote().sendText(message);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}

+ 76 - 0
src/main/java/com/webchat/service/listener/RedisVideoOfferSendMessageListener.java

@@ -0,0 +1,76 @@
+package com.webchat.service.listener;
+
+import com.webchat.common.util.JsonUtil;
+import com.webchat.common.util.SpringContextUtil;
+import com.webchat.controller.client.ChatWebSocket;
+import com.webchat.domain.vo.request.mess.VideoChatMessageRequestVO;
+import com.webchat.domain.vo.response.UserBaseResponseInfoVO;
+import com.webchat.domain.vo.response.mess.ChatMessageResponseVO;
+import com.webchat.service.UserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.websocket.Session;
+
+@Slf4j
+@Component
+public class RedisVideoOfferSendMessageListener implements MessageListener {
+
+    private final RedisTemplate redisTemplate;
+
+    public RedisVideoOfferSendMessageListener(@Qualifier("redisTemplate") RedisTemplate redisTemplate) {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Override
+    public void onMessage(Message message, byte[] bytes) {
+        String channel = (String) redisTemplate.getStringSerializer().deserialize(message.getChannel());
+        String messageStr = (String) redisTemplate.getValueSerializer().deserialize(message.getBody());
+        log.info("redis message listener ======> channel:{}, message:{}", channel, messageStr);
+        this.handleVideoChattingMessage(messageStr);
+    }
+
+    /**
+     * 发送音视频通话邀请
+     *
+     * @param message
+     */
+    private void handleVideoChattingMessage(String message) {
+        VideoChatMessageRequestVO chatMessage = JsonUtil.fromJson(message, VideoChatMessageRequestVO.class);
+        if (chatMessage == null) {
+            return;
+        }
+        String sendUserId = chatMessage.getUserId();
+        String receiverId = chatMessage.getTargetUserId();
+        // 校验用户ws 连接session是否在当前机器Hash表
+        ChatWebSocket ws = ChatWebSocket.clients.get(receiverId);
+        if (ws == null) {
+            return;
+        }
+        Session session = ws.getSession();
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        // 校验用户身份信息
+        UserService userService = SpringContextUtil.getBean(UserService.class);
+        UserBaseResponseInfoVO sender = userService.getUserInfoByUserId(sendUserId);
+        UserBaseResponseInfoVO receiver = userService.getUserInfoByUserId(receiverId);
+        if (sender.getRoleCode() > 2 || receiver.getRoleCode() > 2) {
+            // 音视频当前仅支持普通用户之间对话
+            return;
+        }
+        // 视频对话发送offer,这里走的是普通聊天,推送音视频请求给接收人
+        ChatMessageResponseVO chatMessageResponseVO = new ChatMessageResponseVO();
+        chatMessageResponseVO.setVideoOffer(true);
+        chatMessageResponseVO.setSenderId(sendUserId);
+        chatMessageResponseVO.setReceiverId(receiverId);
+        chatMessageResponseVO.setSenderName(sender.getUserName());
+        String offerMessage = JsonUtil.toJsonString(chatMessageResponseVO);
+        // 发送给接收人音视频通话申请实时通知
+        session.getAsyncRemote().sendText(offerMessage);
+    }
+}

+ 47 - 55
src/main/resources/templates/client/video-chat.html

@@ -70,61 +70,72 @@
         return result ? decodeURIComponent(result[2]) : "";
     }
 
+    // 从URL参数中获取当前用户ID和目标用户ID
     var userId = getUserParamByName("userId");
     var targetUserId = getUserParamByName("targetUserId");
-
-    //initiating a call
-    //our username
     var connectedUser = targetUserId;
-    //connecting to our signaling server
+
+    // 初始化WebSocket连接对象,用于信令服务器通信
     var conn = new WebSocket("ws://"+wsHost+":"+wsPort+"/ws/video-chat/"+userId);
     conn.onopen = function () {
         console.log("Connected to the signaling server");
     };
 
-    //when we got a message from a signaling server
+    // 处理从信令服务器接收到的消息
+    // 当WebSocket接收到信令服务器发送的消息时,触发此事件处理函数
     conn.onmessage = function (msg) {
+        console.log("Got message", msg.data); // 打印接收到的消息内容到控制台,用于调试
 
-        console.log("Got message", msg.data);
-
+        // 解析从服务器接收到的JSON格式的消息数据
         var data = JSON.parse(msg.data);
+        // 根据消息类型进行不同的处理
         switch(data.type) {
+            // 当消息类型为"offer"时,表示远端用户希望建立连接,并发送了他们的SDP(会话描述协议)信息
             case "offer":
+                // 调用handleOffer函数处理offer,设置远端描述,并创建answer响应
                 handleOffer(data.offer);
                 break;
+            // 当消息类型为"answer"时,表示远端用户对我们之前发送的offer做出了响应,并发送了他们的SDP信息
             case "answer":
+                // 调用handleAnswer函数处理answer,设置远端描述
                 handleAnswer(data.answer);
                 break;
-            //when a remote peer sends an ice candidate to us
+            // 当消息类型为"candidate"时,表示远端用户发送了ICE候选信息,用于NAT穿透和连接建立
             case "candidate":
+                // 调用handleCandidate函数处理ICE候选信息,添加到RTCPeerConnection中
                 handleCandidate(data.candidate);
                 break;
+            // 当消息类型为"leave"时,表示远端用户希望结束通话
             case "leave":
+                // 调用handleLeave函数处理离开事件,关闭连接并清理资源
                 handleLeave();
                 break;
+            // 默认情况,不进行任何操作
             default:
                 break;
         }
     };
 
+    // 处理WebSocket连接错误
     conn.onerror = function (err) {
         console.log("Got error", err);
     };
 
-    //alias for sending JSON encoded messages
+    // 发送消息到信令服务器
     function send(message) {
-        //attach the other peer username to our messages
         conn.send(JSON.stringify(message));
     }
 
+    // 获取页面上的按钮和视频元素
     var hangUpBtn = document.querySelector("#hangUpBtn");
     var localVideo = document.querySelector("#localVideo");
     var remoteVideo = document.querySelector("#remoteVideo");
 
-    var yourConn;
+    // 声明RTCPeerConnection对象和流对象
+    var peerConnection;
     var stream;
 
-    // callPage.style.display = "none";
+    // 检测浏览器是否支持WebRTC API
     var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined);
     var RTCSessionDescription = (window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription || undefined);
     navigator.getUserMedia = (navigator.getUserMedia ||
@@ -132,13 +143,10 @@
         navigator.mozGetUserMedia ||
         navigator.msGetUserMedia);
 
-    //getting local video stream
+    // 请求用户的音视频流
     navigator.getUserMedia({ video: true, audio: true }, function (myStream) {
         stream = myStream;
-        // displaying local video stream on the page
-        localVideo.srcObject = stream;
-
-        // using Google public stun server
+        localVideo.srcObject = stream; // 将本地流绑定到本地视频元素
         var configuration = {
             "iceServers": [
                 {
@@ -146,19 +154,12 @@
                 }
             ]
         };
-
-        yourConn = new PeerConnection(configuration);
-
-        // setup stream listening
-        yourConn.addStream(stream);
-
-        //when a remote user adds stream to the peer connection, we display it
-        yourConn.onaddstream = function (e) {
-            remoteVideo.srcObject = e.stream;
+        peerConnection = new PeerConnection(configuration); // 创建RTCPeerConnection对象
+        peerConnection.addStream(stream); // 将本地流添加到连接中
+        peerConnection.onaddstream = function (e) {
+            remoteVideo.srcObject = e.stream; // 将远程流绑定到远程视频元素
         };
-
-        // Setup ice handling
-        yourConn.onicecandidate = function (event) {
+        peerConnection.onicecandidate = function (event) {
             if (event.candidate) {
                 send({
                     type: "candidate",
@@ -168,31 +169,27 @@
                 });
             }
         };
-        // create an offer
-        yourConn.createOffer(function (offer) {
+        // 创建一个offer描述
+        peerConnection.createOffer(function (offer) {
             send({
                 type: "offer",
                 offer: offer,
                 userId: userId,
                 targetUserId: targetUserId
             });
-            yourConn.setLocalDescription(offer);
+            peerConnection.setLocalDescription(offer);
         }, function (error) {
             alert("Error when creating an offer");
         });
-
-
     }, function (error) {
         console.log(error);
     });
 
-    //when somebody sends us an offer
+    // 处理收到的offer
     function handleOffer(offer) {
-        yourConn.setRemoteDescription(new RTCSessionDescription(offer));
-
-        //create an answer to an offer
-        yourConn.createAnswer(function (answer) {
-            yourConn.setLocalDescription(answer);
+        peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
+        peerConnection.createAnswer(function (answer) {
+            peerConnection.setLocalDescription(answer);
             send({
                 type: "answer",
                 answer: answer,
@@ -204,39 +201,34 @@
         });
     }
 
-    //when we got an answer from a remote user
+    // 处理收到的answer
     function handleAnswer(answer) {
-        yourConn.setRemoteDescription(new RTCSessionDescription(answer));
+        peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
     }
 
-    //when we got an ice candidate from a remote user
+    // 处理收到的ICE候选
     function handleCandidate(candidate) {
-        yourConn.addIceCandidate(new RTCIceCandidate(candidate));
+        peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
     }
 
-    //hang up
+    // 挂断按钮事件监听器
     hangUpBtn.addEventListener("click", function () {
-
         send({
             type: "leave",
             userId: userId,
             targetUserId: targetUserId
         });
-
         handleLeave();
     });
 
+    // 处理离开事件
     function handleLeave() {
         connectedUser = null;
         remoteVideo.src = null;
-
-        yourConn.close();
-        yourConn.onicecandidate = null;
-        yourConn.onaddstream = null;
-        layer.closeAll();
+        peerConnection.close(); // 关闭RTCPeerConnection
+        peerConnection.onicecandidate = null;
+        peerConnection.onaddstream = null;
+        layer.closeAll(); // 关闭所有层(可能是第三方库函数)
     }
-
-
-
 </script>
 </html>