group-video-chat.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  6. <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
  7. <title>webchat即时在线聊天室,视频通话</title>
  8. <link rel="stylesheet" href="/css/common/common.css">
  9. <link rel="stylesheet" href="/css/client/chat.css">
  10. <link href="/ref/layui-v2.6.8/layui/css/layui.css" rel="stylesheet" type="text/css" />
  11. <script src="/ref/jquery/jquery-3.4.1.js" type="text/javascript"></script>
  12. <script src="/ref/layui-v2.6.8/layui/layui.js" type="text/javascript"></script>
  13. <style>
  14. #hangUpBtn {
  15. position: absolute;
  16. bottom: 80px;
  17. width: 50px;
  18. height: 50px;
  19. border: none;
  20. border-radius: 100px;
  21. background-color: #fb5c5c;
  22. box-shadow: 0px 0px 10px #fb8d8d;
  23. }
  24. #videos {
  25. position: absolute;
  26. left: 0px;
  27. top: 0px;
  28. width: 100%;
  29. height: 100%;
  30. background-color: whitesmoke;
  31. display: grid;
  32. grid-template-columns: repeat(auto-fill, minmax(30%, 1fr));
  33. grid-gap: 1px; /* 根据需要调整网格间距 */
  34. }
  35. #videos div {
  36. position: relative;
  37. width: 299px; /* 视频宽度自适应 */
  38. height: 224px; /* 保持视频宽高比 */
  39. background-color: black;
  40. border-radius: 1px;
  41. float: left;
  42. border: none;
  43. background-color: black;
  44. }
  45. #videos div video {
  46. position: relative;
  47. width: 299px; /* 视频宽度自适应 */
  48. height: 224px; /* 保持视频宽高比 */
  49. background-color: black;
  50. border-radius: 1px;
  51. float: left;
  52. border: none;
  53. }
  54. #videos div span {
  55. position: absolute;
  56. top: 15px;
  57. left: 15px;
  58. height: 20px;
  59. line-height: 20px;
  60. background-color: rgba(0, 149, 255, 0.7);
  61. padding: 3px 10px;
  62. border-radius: 100px;
  63. color: white;
  64. font-size: 13px;
  65. }
  66. #videos div span.other {
  67. background-color: rgba(0, 0, 0, 0.7);
  68. }
  69. </style>
  70. </head>
  71. <body>
  72. <center>
  73. <div id = "videos"></div>
  74. <div class = "row text-center">
  75. <div class = "col-md-12">
  76. <button id = "hangUpBtn" class="btn-danger btn">
  77. <svg t="1730296252524" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4292" width="30" height="30"><path d="M115.2 115.2c19.2-32 44.8-64 76.8-83.2 32-19.2 70.4-32 115.2-32H320c70.4 0 134.4 76.8 153.6 198.4 12.8 57.6 6.4 121.6-19.2 160-12.8 32-38.4 51.2-70.4 51.2l-12.8 12.8v12.8c19.2 38.4 38.4 76.8 64 108.8 19.2 32 44.8 64 70.4 96 6.4 6.4 12.8 6.4 19.2 6.4h6.4c12.8-19.2 32-38.4 57.6-44.8 44.8-6.4 115.2 12.8 179.2 57.6 96 70.4 140.8 166.4 108.8 230.4l-6.4 6.4c-19.2 38.4-44.8 70.4-83.2 89.6-160 96-409.6-38.4-576-307.2-128-198.4-166.4-428.8-96-563.2z m288 217.6c12.8-32 19.2-76.8 12.8-128-12.8-83.2-64-147.2-96-147.2h-12.8c-32 0-64 6.4-89.6 25.6-19.2 12.8-38.4 32-51.2 57.6-64 115.2-25.6 326.4 89.6 512 147.2 230.4 371.2 364.8 499.2 288 25.6-12.8 44.8-38.4 57.6-70.4 0 0 0-6.4 6.4-6.4v-6.4c19.2-32-19.2-102.4-89.6-153.6-44.8-32-102.4-51.2-134.4-44.8-12.8 6.4-19.2 12.8-19.2 19.2 0 0-12.8 25.6-44.8 32-19.2 6.4-44.8-6.4-64-25.6l-6.4-6.4c-25.6-32-51.2-64-76.8-102.4-25.6-38.4-44.8-83.2-64-121.6V448c-6.4-19.2-6.4-38.4 0-57.6 12.8-25.6 44.8-38.4 44.8-38.4h6.4c19.2 0 25.6-12.8 32-19.2z m217.6 12.8c6.4-12.8 19.2-19.2 32-12.8 32 6.4 51.2 25.6 64 51.2 12.8 25.6 12.8 51.2 6.4 76.8-6.4 12.8-19.2 25.6-38.4 19.2-12.8-6.4-25.6-19.2-19.2-38.4 6.4-12.8 0-25.6 0-32-6.4-12.8-19.2-19.2-25.6-25.6-19.2 0-25.6-19.2-19.2-38.4zM672 256c6.4-12.8 19.2-19.2 32-12.8 96 32 147.2 134.4 115.2 230.4-6.4 12.8-19.2 25.6-38.4 19.2-12.8-6.4-25.6-19.2-19.2-38.4 19.2-64-12.8-134.4-76.8-153.6-12.8-6.4-19.2-25.6-12.8-44.8 0 6.4 0 0 0 0z m25.6-115.2c6.4-12.8 19.2-19.2 32-12.8 76.8 25.6 140.8 83.2 179.2 153.6 38.4 76.8 44.8 160 19.2 236.8-6.4 12.8-19.2 25.6-38.4 19.2-12.8-6.4-25.6-19.2-19.2-38.4 19.2-64 19.2-134.4-12.8-192-32-57.6-83.2-102.4-147.2-128-12.8 0-19.2-19.2-12.8-38.4 0 6.4 0 0 0 0z m0 0" p-id="4293" fill="#ffffff"></path></svg>
  78. </button>
  79. </div>
  80. </div>
  81. </center>
  82. </body>
  83. <script src="/js/client/video.chat.js" type="text/javascript"></script>
  84. <script type="text/javascript">
  85. var wsHost = document.domain;
  86. var wsPort = 8101;
  87. function getUserParamByName(key) {
  88. var url = window.location.search;
  89. var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)");
  90. var result = url.substr(1).match(reg);
  91. return result ? decodeURIComponent(result[2]) : "";
  92. }
  93. var userId = getUserParamByName("userId");
  94. var userName = getUserParamByName("userName");
  95. var targetUserId = getUserParamByName("targetUserId");
  96. var connectedUserIds = new Array();
  97. var onlineUserMap;
  98. var videos = document.querySelector("#videos");
  99. // RTCPeerConnection管理
  100. const peerConnections = {};
  101. var configuration = {
  102. "iceServers": [
  103. {
  104. "urls": "stun:stun.l.google.com:19302"
  105. }
  106. ]
  107. };
  108. var PeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection || undefined);
  109. var RTCSessionDescription = (window.webkitRTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription || undefined);
  110. navigator.getUserMedia = (navigator.getUserMedia ||
  111. navigator.webkitGetUserMedia ||
  112. navigator.mozGetUserMedia ||
  113. navigator.msGetUserMedia);
  114. // 初始化WebSocket连接对象,用于信令服务器通信
  115. let localStream;
  116. var conn;
  117. function initSignalServerConnection() {
  118. conn = new WebSocket("ws://" + wsHost + ":" + wsPort + "/ws/group-video-chat/" + targetUserId + "/" + userId);
  119. conn.onopen = function () {
  120. console.log("Connected to the signaling server");
  121. send({
  122. type: "call",
  123. userId: userId,
  124. targetUserId: targetUserId,
  125. groupId: targetUserId
  126. })
  127. };
  128. conn.onmessage = function (msg) {
  129. var data = JSON.parse(msg.data);
  130. console.log("Got message =====> ", data);
  131. switch (data.type) {
  132. case "online":
  133. handleOnline(data);
  134. break;
  135. case "offline":
  136. handleOffline(data);
  137. break;
  138. case "offer":
  139. handleOffer(data.offer, data.userId);
  140. break;
  141. case "answer":
  142. handleAnswer(data.answer, data.userId);
  143. break;
  144. case "candidate":
  145. handleCandidate(data.candidate, data.userId);
  146. break;
  147. case "leave":
  148. handleLeave(data.userId);
  149. break;
  150. default:
  151. break;
  152. }
  153. };
  154. conn.onerror = function (err) {
  155. console.log("Got error", err);
  156. initSignalServerConnection();
  157. };
  158. conn.onclose = function (err) {
  159. initSignalServerConnection();
  160. }
  161. }
  162. startMedia();
  163. function startMedia() {
  164. // 初始化本地视频流,并绑定到页面当前客户端视频标签上
  165. navigator.getUserMedia({ video: true, audio: true }, function (stream) {
  166. localStream = stream;
  167. // 创建视频元素并绑定到页面上
  168. var videoContainer = document.createElement('div');
  169. var span = document.createElement('span');
  170. var video = document.createElement('video');
  171. video.autoplay = true;
  172. video.muted = true;
  173. // 将本地流绑定到本地视频元素
  174. video.srcObject = localStream;
  175. span.innerText = userName;
  176. videoContainer.id = "video-" + userId;
  177. videoContainer.appendChild(video);
  178. videoContainer.appendChild(span);
  179. videos.appendChild(videoContainer);
  180. initSignalServerConnection();
  181. }, function (error) {
  182. console.log(error);
  183. });
  184. }
  185. function send(message) {
  186. conn.send(JSON.stringify(message));
  187. }
  188. function getPeerId(uid) {
  189. let peerKeyArr = [userId, uid];
  190. return peerKeyArr.sort().join('-');
  191. }
  192. function getPeerConnection(uid) {
  193. let peerKey = getPeerId(uid);
  194. if (peerConnections[peerKey]) {
  195. return peerConnections[peerKey];
  196. }
  197. var peerConnection = new PeerConnection(configuration);
  198. peerConnections[peerKey] = peerConnection;
  199. return peerConnection;
  200. }
  201. function handleOnline(message) {
  202. onlineUserMap = new Map(Object.entries(message.onlineUserMap));
  203. for (let i = 0; i < message.userIds.length; i++) {
  204. var uid = message.userIds[i];
  205. if (connectedUserIds.includes(uid)) {
  206. continue;
  207. }
  208. connectedUserIds.push(uid);
  209. if (uid == userId) {
  210. continue;
  211. }
  212. var peerConnection = getPeerConnection(uid);
  213. var videoContainer = document.createElement('div');
  214. var span = document.createElement('span');
  215. var video = document.createElement('video');
  216. video.autoplay = true;
  217. video.muted = true;
  218. video.controls = false;
  219. span.innerText = onlineUserMap.get(uid).userName;
  220. span.className = "other";
  221. videoContainer.id = "video-" + uid;
  222. // 将视频元素添加到页面上
  223. videoContainer.appendChild(video);
  224. videoContainer.appendChild(span);
  225. videos.appendChild(videoContainer);
  226. peerConnection.addStream(localStream);
  227. peerConnection.onaddstream = function(event){
  228. video.srcObject = event.stream;
  229. };
  230. peerConnection.onicecandidate = function (event) {
  231. if (event.candidate) {
  232. send({
  233. type: "candidate",
  234. candidate: event.candidate,
  235. userId: userId,
  236. targetUserId: uid,
  237. groupId: targetUserId
  238. });
  239. }
  240. };
  241. // 创建一个offer描述
  242. peerConnection.createOffer(function (offer) {
  243. send({
  244. type: "offer",
  245. offer: offer,
  246. userId: userId,
  247. targetUserId: uid,
  248. groupId: targetUserId
  249. });
  250. peerConnection.setLocalDescription(offer);
  251. }, function (error) {
  252. alert("Error when creating an offer");
  253. });
  254. }
  255. }
  256. function handleOffline(message) {
  257. // 获取下线用户
  258. var offlineUserId = message.offlineUserId;
  259. var offlineUserName = onlineUserMap.get(offlineUserId).userName;
  260. var offlineVideo = document.getElementById("video-"+offlineUserId);
  261. if (offlineVideo) {
  262. offlineVideo.remove();
  263. layer.msg(offlineUserName + "退出会议!");
  264. }
  265. getPeerConnection(offlineUserId).close();
  266. peerConnections[getPeerId(offlineUserId)] = null;
  267. connectedUserIds = connectedUserIds.filter(function (uid) { return uid !== offlineUserId; });
  268. }
  269. function handleOffer(offer, from) {
  270. var peerConnection = getPeerConnection(from);
  271. peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
  272. peerConnection.createAnswer(function (answer) {
  273. peerConnection.setLocalDescription(answer);
  274. send({
  275. type: "answer",
  276. answer: answer,
  277. userId: userId,
  278. targetUserId: from,
  279. groupId: targetUserId
  280. });
  281. }, function (error) {
  282. alert("Error when creating an answer");
  283. });
  284. }
  285. function handleAnswer(answer, from) {
  286. var peerConnection = getPeerConnection(from);
  287. peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
  288. }
  289. // 处理收到的ICE候选
  290. function handleCandidate(candidate, from) {
  291. var peerConnection = getPeerConnection(from);
  292. peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
  293. }
  294. function handleLeave(from) {
  295. var offlineUserName = onlineUserMap.get(from).userName;
  296. layer.msg(offlineUserName + "退出会议!");
  297. document.getElementById("video-" + from).remove();
  298. getPeerConnection(from).close();
  299. peerConnections[getPeerId(from)] = null;
  300. connectedUserIds = connectedUserIds.filter(function (uid) { return uid !== from; });
  301. }
  302. hangUpBtn.addEventListener("click", function () {
  303. connectedUserIds.forEach(function (user) {
  304. send({
  305. type: "leave",
  306. userId: userId,
  307. targetUserId: targetUserId,
  308. groupId: targetUserId
  309. });
  310. handleLeave(user);
  311. // 在iframe页面中执行以下代码
  312. var index = parent.layer.getFrameIndex(window.name); // 获取当前iframe层的索引
  313. parent.layer.close(index); // 关闭父级弹窗
  314. });
  315. });
  316. </script>
  317. </html>