mistpe commited on
Commit
1756749
·
verified ·
1 Parent(s): 6305afd

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +971 -19
index.html CHANGED
@@ -1,19 +1,971 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>视频会议应用</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.1/socket.io.min.js"></script>
8
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/8.1.1/adapter.min.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ font-family: 'Arial', sans-serif;
15
+ }
16
+
17
+ body {
18
+ background-color: #f0f2f5;
19
+ color: #333;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1200px;
24
+ margin: 0 auto;
25
+ padding: 20px;
26
+ }
27
+
28
+ header {
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+ padding: 15px 0;
33
+ border-bottom: 1px solid #ddd;
34
+ margin-bottom: 20px;
35
+ }
36
+
37
+ .logo {
38
+ font-size: 24px;
39
+ font-weight: bold;
40
+ color: #0066cc;
41
+ }
42
+
43
+ .controls {
44
+ display: flex;
45
+ gap: 10px;
46
+ }
47
+
48
+ button {
49
+ padding: 8px 16px;
50
+ background-color: #0066cc;
51
+ color: white;
52
+ border: none;
53
+ border-radius: 4px;
54
+ cursor: pointer;
55
+ font-size: 14px;
56
+ transition: background-color 0.3s;
57
+ }
58
+
59
+ button:hover {
60
+ background-color: #0055aa;
61
+ }
62
+
63
+ button.red {
64
+ background-color: #cc0000;
65
+ }
66
+
67
+ button.red:hover {
68
+ background-color: #aa0000;
69
+ }
70
+
71
+ .disabled {
72
+ background-color: #888;
73
+ cursor: not-allowed;
74
+ }
75
+
76
+ .disabled:hover {
77
+ background-color: #888;
78
+ }
79
+
80
+ .meeting-container {
81
+ display: grid;
82
+ grid-template-columns: 1fr 300px;
83
+ gap: 20px;
84
+ height: calc(100vh - 150px);
85
+ }
86
+
87
+ .video-grid {
88
+ display: grid;
89
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
90
+ grid-auto-rows: 1fr;
91
+ gap: 15px;
92
+ height: 100%;
93
+ overflow: auto;
94
+ }
95
+
96
+ .video-container {
97
+ background-color: #222;
98
+ border-radius: 8px;
99
+ overflow: hidden;
100
+ position: relative;
101
+ }
102
+
103
+ .video-container video {
104
+ width: 100%;
105
+ height: 100%;
106
+ object-fit: cover;
107
+ }
108
+
109
+ .user-name {
110
+ position: absolute;
111
+ bottom: 10px;
112
+ left: 10px;
113
+ color: white;
114
+ background-color: rgba(0, 0, 0, 0.5);
115
+ padding: 4px 8px;
116
+ border-radius: 4px;
117
+ font-size: 14px;
118
+ }
119
+
120
+ .mic-status {
121
+ position: absolute;
122
+ top: 10px;
123
+ right: 10px;
124
+ color: white;
125
+ padding: 4px;
126
+ border-radius: 50%;
127
+ }
128
+
129
+ .sidebar {
130
+ background-color: white;
131
+ border-radius: 8px;
132
+ display: flex;
133
+ flex-direction: column;
134
+ height: 100%;
135
+ }
136
+
137
+ .tabs {
138
+ display: flex;
139
+ border-bottom: 1px solid #ddd;
140
+ }
141
+
142
+ .tab {
143
+ padding: 10px 15px;
144
+ cursor: pointer;
145
+ }
146
+
147
+ .tab.active {
148
+ color: #0066cc;
149
+ border-bottom: 2px solid #0066cc;
150
+ }
151
+
152
+ .tab-content {
153
+ flex-grow: 1;
154
+ overflow: auto;
155
+ padding: 15px;
156
+ }
157
+
158
+ .participants-list {
159
+ list-style: none;
160
+ }
161
+
162
+ .participant {
163
+ display: flex;
164
+ align-items: center;
165
+ padding: 8px 0;
166
+ border-bottom: 1px solid #eee;
167
+ }
168
+
169
+ .participant-info {
170
+ flex-grow: 1;
171
+ }
172
+
173
+ .chat-container {
174
+ display: flex;
175
+ flex-direction: column;
176
+ height: 100%;
177
+ }
178
+
179
+ .messages {
180
+ flex-grow: 1;
181
+ overflow: auto;
182
+ padding: 10px;
183
+ }
184
+
185
+ .message {
186
+ margin-bottom: 10px;
187
+ padding: 8px 12px;
188
+ border-radius: 6px;
189
+ max-width: 80%;
190
+ }
191
+
192
+ .message.received {
193
+ background-color: #f1f1f1;
194
+ align-self: flex-start;
195
+ }
196
+
197
+ .message.sent {
198
+ background-color: #dcf8c6;
199
+ align-self: flex-end;
200
+ }
201
+
202
+ .sender {
203
+ font-size: 12px;
204
+ color: #555;
205
+ margin-bottom: 2px;
206
+ }
207
+
208
+ .chat-input {
209
+ display: flex;
210
+ padding: 10px;
211
+ border-top: 1px solid #ddd;
212
+ }
213
+
214
+ .chat-input input {
215
+ flex-grow: 1;
216
+ padding: 8px 12px;
217
+ border: 1px solid #ddd;
218
+ border-radius: 4px;
219
+ margin-right: 10px;
220
+ outline: none;
221
+ }
222
+
223
+ .join-form {
224
+ max-width: 400px;
225
+ margin: 100px auto;
226
+ background-color: white;
227
+ padding: 30px;
228
+ border-radius: 8px;
229
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
230
+ }
231
+
232
+ .form-group {
233
+ margin-bottom: 20px;
234
+ }
235
+
236
+ label {
237
+ display: block;
238
+ margin-bottom: 8px;
239
+ font-weight: bold;
240
+ }
241
+
242
+ input[type="text"],
243
+ input[type="password"] {
244
+ width: 100%;
245
+ padding: 10px;
246
+ border: 1px solid #ddd;
247
+ border-radius: 4px;
248
+ font-size: 16px;
249
+ }
250
+
251
+ .button-group {
252
+ display: flex;
253
+ justify-content: space-between;
254
+ }
255
+
256
+ .hidden {
257
+ display: none;
258
+ }
259
+
260
+ /* 屏幕共享布局 */
261
+ .screen-share {
262
+ grid-column: span 2;
263
+ height: 100%;
264
+ background-color: #222;
265
+ border-radius: 8px;
266
+ overflow: hidden;
267
+ position: relative;
268
+ }
269
+
270
+ .screen-share video {
271
+ width: 100%;
272
+ height: 100%;
273
+ object-fit: contain;
274
+ }
275
+
276
+ .screen-share-label {
277
+ position: absolute;
278
+ top: 10px;
279
+ left: 10px;
280
+ color: white;
281
+ background-color: rgba(0, 0, 0, 0.5);
282
+ padding: 4px 8px;
283
+ border-radius: 4px;
284
+ font-size: 14px;
285
+ }
286
+
287
+ /* 响应式设计 */
288
+ @media (max-width: 768px) {
289
+ .meeting-container {
290
+ grid-template-columns: 1fr;
291
+ }
292
+
293
+ .sidebar {
294
+ margin-top: 20px;
295
+ height: 300px;
296
+ }
297
+ }
298
+ </style>
299
+ </head>
300
+ <body>
301
+ <!-- 登录表单 -->
302
+ <div class="join-form" id="joinForm">
303
+ <h2 style="text-align: center; margin-bottom: 20px;">加入会议</h2>
304
+ <div class="form-group">
305
+ <label for="displayName">您的姓名</label>
306
+ <input type="text" id="displayName" placeholder="请输入您的姓名">
307
+ </div>
308
+ <div class="form-group">
309
+ <label for="roomId">会议 ID</label>
310
+ <input type="text" id="roomId" placeholder="请输入会议 ID 或创建新会议">
311
+ </div>
312
+ <div class="button-group">
313
+ <button id="createMeeting">创建会议</button>
314
+ <button id="joinMeeting">加入会议</button>
315
+ </div>
316
+ </div>
317
+
318
+ <!-- 会议主界面 (默认隐藏) -->
319
+ <div class="container hidden" id="meetingContainer">
320
+ <header>
321
+ <div class="logo">视频会议</div>
322
+ <div class="meeting-info">
323
+ 会议 ID: <span id="currentRoomId"></span>
324
+ <button id="copyRoomId">复制</button>
325
+ </div>
326
+ <div class="controls">
327
+ <button id="toggleAudio"><i class="fas fa-microphone"></i> 麦克风</button>
328
+ <button id="toggleVideo"><i class="fas fa-video"></i> 摄像头</button>
329
+ <button id="toggleScreen"><i class="fas fa-desktop"></i> 屏幕共享</button>
330
+ <button id="leaveCall" class="red"><i class="fas fa-phone-slash"></i> 离开会议</button>
331
+ </div>
332
+ </header>
333
+
334
+ <div class="meeting-container">
335
+ <div class="video-grid" id="videoGrid">
336
+ <!-- 视频将在这里动态添加 -->
337
+ </div>
338
+
339
+ <div class="sidebar">
340
+ <div class="tabs">
341
+ <div class="tab active" data-tab="participants">参会者</div>
342
+ <div class="tab" data-tab="chat">聊天</div>
343
+ </div>
344
+
345
+ <div class="tab-content" id="participantsTab">
346
+ <ul class="participants-list" id="participantsList">
347
+ <!-- 参会���将在这里动态添加 -->
348
+ </ul>
349
+ </div>
350
+
351
+ <div class="tab-content hidden" id="chatTab">
352
+ <div class="chat-container">
353
+ <div class="messages" id="messages">
354
+ <!-- 消息将在这里动态添加 -->
355
+ </div>
356
+ <div class="chat-input">
357
+ <input type="text" id="messageInput" placeholder="输入消息...">
358
+ <button id="sendMessage">发送</button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <script>
367
+ // DOM 元素
368
+ const joinForm = document.getElementById('joinForm');
369
+ const meetingContainer = document.getElementById('meetingContainer');
370
+ const displayNameInput = document.getElementById('displayName');
371
+ const roomIdInput = document.getElementById('roomId');
372
+ const createMeetingBtn = document.getElementById('createMeeting');
373
+ const joinMeetingBtn = document.getElementById('joinMeeting');
374
+ const currentRoomIdSpan = document.getElementById('currentRoomId');
375
+ const copyRoomIdBtn = document.getElementById('copyRoomId');
376
+ const toggleAudioBtn = document.getElementById('toggleAudio');
377
+ const toggleVideoBtn = document.getElementById('toggleVideo');
378
+ const toggleScreenBtn = document.getElementById('toggleScreen');
379
+ const leaveCallBtn = document.getElementById('leaveCall');
380
+ const videoGrid = document.getElementById('videoGrid');
381
+ const tabs = document.querySelectorAll('.tab');
382
+ const tabContents = document.querySelectorAll('.tab-content');
383
+ const participantsList = document.getElementById('participantsList');
384
+ const messages = document.getElementById('messages');
385
+ const messageInput = document.getElementById('messageInput');
386
+ const sendMessageBtn = document.getElementById('sendMessage');
387
+
388
+ // 状态变量
389
+ let currentRoom = null;
390
+ let localStream = null;
391
+ let screenStream = null;
392
+ let peerConnections = {};
393
+ let dataChannels = {};
394
+ let localAudioEnabled = true;
395
+ let localVideoEnabled = true;
396
+
397
+ // 模拟服务器功能 (实际应用中应该使用真实的信令服务器)
398
+ const mockServer = {
399
+ connections: {},
400
+
401
+ connect(roomId, userId) {
402
+ if (!this.connections[roomId]) {
403
+ this.connections[roomId] = {};
404
+ }
405
+ this.connections[roomId][userId] = {
406
+ onOffer: null,
407
+ onAnswer: null,
408
+ onIceCandidate: null,
409
+ sendOffer: (targetId, offer) => {
410
+ const target = this.connections[roomId][targetId];
411
+ if (target && target.onOffer) {
412
+ setTimeout(() => {
413
+ target.onOffer(userId, offer);
414
+ }, 100);
415
+ }
416
+ },
417
+ sendAnswer: (targetId, answer) => {
418
+ const target = this.connections[roomId][targetId];
419
+ if (target && target.onAnswer) {
420
+ setTimeout(() => {
421
+ target.onAnswer(userId, answer);
422
+ }, 100);
423
+ }
424
+ },
425
+ sendIceCandidate: (targetId, candidate) => {
426
+ const target = this.connections[roomId][targetId];
427
+ if (target && target.onIceCandidate) {
428
+ setTimeout(() => {
429
+ target.onIceCandidate(userId, candidate);
430
+ }, 100);
431
+ }
432
+ },
433
+ sendMessage: (targetId, message) => {
434
+ const target = this.connections[roomId][targetId];
435
+ if (target && target.onMessage) {
436
+ setTimeout(() => {
437
+ target.onMessage(userId, message);
438
+ }, 100);
439
+ }
440
+ },
441
+ onMessage: null
442
+ };
443
+
444
+ return {
445
+ userId,
446
+ peers: Object.keys(this.connections[roomId]).filter(id => id !== userId)
447
+ };
448
+ },
449
+
450
+ disconnect(roomId, userId) {
451
+ if (this.connections[roomId]) {
452
+ delete this.connections[roomId][userId];
453
+ if (Object.keys(this.connections[roomId]).length === 0) {
454
+ delete this.connections[roomId];
455
+ }
456
+ }
457
+ }
458
+ };
459
+
460
+ // 生成随机 ID
461
+ function generateId() {
462
+ return Math.random().toString(36).substr(2, 9);
463
+ }
464
+
465
+ // 初始化媒体流
466
+ async function initLocalStream() {
467
+ try {
468
+ localStream = await navigator.mediaDevices.getUserMedia({
469
+ audio: true,
470
+ video: true
471
+ });
472
+
473
+ // 创建本地视频元素
474
+ const videoContainer = document.createElement('div');
475
+ videoContainer.className = 'video-container';
476
+ videoContainer.id = 'local-video-container';
477
+
478
+ const video = document.createElement('video');
479
+ video.srcObject = localStream;
480
+ video.autoplay = true;
481
+ video.muted = true; // 避免回声
482
+ video.playsInline = true;
483
+
484
+ const userName = document.createElement('div');
485
+ userName.className = 'user-name';
486
+ userName.textContent = displayNameInput.value + ' (你)';
487
+
488
+ videoContainer.appendChild(video);
489
+ videoContainer.appendChild(userName);
490
+
491
+ videoGrid.appendChild(videoContainer);
492
+
493
+ return true;
494
+ } catch (error) {
495
+ console.error('无法获取媒体流', error);
496
+ alert('无法访问摄像头或麦克风,请确保它们已连接并授予访问权限。');
497
+ return false;
498
+ }
499
+ }
500
+
501
+ // 加入会议
502
+ async function joinMeeting() {
503
+ const displayName = displayNameInput.value.trim();
504
+ let roomId = roomIdInput.value.trim();
505
+
506
+ if (!displayName) {
507
+ alert('请输入您的姓名');
508
+ return;
509
+ }
510
+
511
+ if (!roomId) {
512
+ alert('请输入会议 ID');
513
+ return;
514
+ }
515
+
516
+ // 初始化本地媒体流
517
+ const success = await initLocalStream();
518
+ if (!success) return;
519
+
520
+ // 连接到"服务器"
521
+ const userId = generateId();
522
+ const { peers } = mockServer.connect(roomId, userId);
523
+
524
+ currentRoom = {
525
+ id: roomId,
526
+ userId,
527
+ displayName
528
+ };
529
+
530
+ // 显示会议 ID
531
+ currentRoomIdSpan.textContent = roomId;
532
+
533
+ // 为每个已存在的对等端创建连接
534
+ for (const peerId of peers) {
535
+ createPeerConnection(peerId);
536
+ }
537
+
538
+ // 设置信令回调
539
+ const connection = mockServer.connections[roomId][userId];
540
+
541
+ connection.onOffer = async (peerId, offer) => {
542
+ const pc = peerConnections[peerId] || createPeerConnection(peerId);
543
+
544
+ try {
545
+ await pc.setRemoteDescription(new RTCSessionDescription(offer));
546
+
547
+ const answer = await pc.createAnswer();
548
+ await pc.setLocalDescription(answer);
549
+
550
+ connection.sendAnswer(peerId, answer);
551
+ } catch (error) {
552
+ console.error('处理提议时出错', error);
553
+ }
554
+ };
555
+
556
+ connection.onAnswer = async (peerId, answer) => {
557
+ const pc = peerConnections[peerId];
558
+ if (pc) {
559
+ try {
560
+ await pc.setRemoteDescription(new RTCSessionDescription(answer));
561
+ } catch (error) {
562
+ console.error('处理应答时出错', error);
563
+ }
564
+ }
565
+ };
566
+
567
+ connection.onIceCandidate = (peerId, candidate) => {
568
+ const pc = peerConnections[peerId];
569
+ if (pc) {
570
+ try {
571
+ pc.addIceCandidate(new RTCIceCandidate(candidate));
572
+ } catch (error) {
573
+ console.error('添加 ICE 候选时出错', error);
574
+ }
575
+ }
576
+ };
577
+
578
+ connection.onMessage = (peerId, message) => {
579
+ displayMessage(peerId, message);
580
+ };
581
+
582
+ // 切换到会议界面
583
+ joinForm.classList.add('hidden');
584
+ meetingContainer.classList.remove('hidden');
585
+ }
586
+
587
+ // 创建 WebRTC 对等连接
588
+ function createPeerConnection(peerId) {
589
+ const pc = new RTCPeerConnection({
590
+ iceServers: [
591
+ { urls: 'stun:stun.l.google.com:19302' },
592
+ { urls: 'stun:stun1.l.google.com:19302' }
593
+ ]
594
+ });
595
+
596
+ peerConnections[peerId] = pc;
597
+
598
+ // 添加本地流到连接
599
+ localStream.getTracks().forEach(track => {
600
+ pc.addTrack(track, localStream);
601
+ });
602
+
603
+ // 创建数据通道
604
+ const dataChannel = pc.createDataChannel(`chat-${peerId}`);
605
+
606
+ dataChannel.onopen = () => {
607
+ console.log(`与 ${peerId} 的数据通道已打开`);
608
+ dataChannels[peerId] = dataChannel;
609
+ };
610
+
611
+ dataChannel.onclose = () => {
612
+ console.log(`与 ${peerId} 的数据通道已关闭`);
613
+ delete dataChannels[peerId];
614
+ };
615
+
616
+ // 处理 ICE 候选
617
+ pc.onicecandidate = (event) => {
618
+ if (event.candidate) {
619
+ const connection = mockServer.connections[currentRoom.id][currentRoom.userId];
620
+ connection.sendIceCandidate(peerId, event.candidate);
621
+ }
622
+ };
623
+
624
+ // 处理远程流
625
+ pc.ontrack = (event) => {
626
+ const stream = event.streams[0];
627
+
628
+ // 检查是否已经存在该对等方的视频元素
629
+ const existingContainer = document.getElementById(`peer-${peerId}-container`);
630
+
631
+ if (!existingContainer) {
632
+ // 创建新的视频容器
633
+ const videoContainer = document.createElement('div');
634
+ videoContainer.className = 'video-container';
635
+ videoContainer.id = `peer-${peerId}-container`;
636
+
637
+ const video = document.createElement('video');
638
+ video.srcObject = stream;
639
+ video.autoplay = true;
640
+ video.playsInline = true;
641
+
642
+ const userName = document.createElement('div');
643
+ userName.className = 'user-name';
644
+ userName.textContent = '参会者'; // 实际应用中应该从信令服务器获取名称
645
+
646
+ videoContainer.appendChild(video);
647
+ videoContainer.appendChild(userName);
648
+
649
+ videoGrid.appendChild(videoContainer);
650
+
651
+ // 添加到参会者列表
652
+ const participant = document.createElement('li');
653
+ participant.className = 'participant';
654
+ participant.id = `participant-${peerId}`;
655
+
656
+ const participantInfo = document.createElement('div');
657
+ participantInfo.className = 'participant-info';
658
+ participantInfo.textContent = '参会者';
659
+
660
+ participant.appendChild(participantInfo);
661
+ participantsList.appendChild(participant);
662
+ } else {
663
+ // 更新现有视频元素
664
+ const video = existingContainer.querySelector('video');
665
+ if (video.srcObject !== stream) {
666
+ video.srcObject = stream;
667
+ }
668
+ }
669
+ };
670
+
671
+ // 处理数据通道的接收
672
+ pc.ondatachannel = (event) => {
673
+ const receiveChannel = event.channel;
674
+
675
+ receiveChannel.onmessage = (messageEvent) => {
676
+ displayMessage(peerId, JSON.parse(messageEvent.data));
677
+ };
678
+
679
+ receiveChannel.onopen = () => {
680
+ console.log(`接收到来自 ${peerId} 的数据通道`);
681
+ dataChannels[peerId] = receiveChannel;
682
+ };
683
+
684
+ receiveChannel.onclose = () => {
685
+ console.log(`来自 ${peerId} 的数据通道已关闭`);
686
+ delete dataChannels[peerId];
687
+ };
688
+ };
689
+
690
+ // 创建并发送提议
691
+ pc.createOffer()
692
+ .then(offer => pc.setLocalDescription(offer))
693
+ .then(() => {
694
+ const connection = mockServer.connections[currentRoom.id][currentRoom.userId];
695
+ connection.sendOffer(peerId, pc.localDescription);
696
+ })
697
+ .catch(error => {
698
+ console.error('创建提议时出错', error);
699
+ });
700
+
701
+ return pc;
702
+ }
703
+
704
+ // 切换麦克风
705
+ function toggleAudio() {
706
+ const audioTracks = localStream.getAudioTracks();
707
+
708
+ if (audioTracks.length > 0) {
709
+ const track = audioTracks[0];
710
+ track.enabled = !track.enabled;
711
+ localAudioEnabled = track.enabled;
712
+
713
+ toggleAudioBtn.innerHTML = localAudioEnabled ?
714
+ '<i class="fas fa-microphone"></i> 麦克风' :
715
+ '<i class="fas fa-microphone-slash"></i> 麦克风 (已关闭)';
716
+
717
+ toggleAudioBtn.classList.toggle('disabled', !localAudioEnabled);
718
+ }
719
+ }
720
+
721
+ // 切换摄像头
722
+ function toggleVideo() {
723
+ const videoTracks = localStream.getVideoTracks();
724
+
725
+ if (videoTracks.length > 0) {
726
+ const track = videoTracks[0];
727
+ track.enabled = !track.enabled;
728
+ localVideoEnabled = track.enabled;
729
+
730
+ toggleVideoBtn.innerHTML = localVideoEnabled ?
731
+ '<i class="fas fa-video"></i> 摄像头' :
732
+ '<i class="fas fa-video-slash"></i> 摄像头 (已关闭)';
733
+
734
+ toggleVideoBtn.classList.toggle('disabled', !localVideoEnabled);
735
+
736
+ // 更新本地视频容器的样式
737
+ const localVideoContainer = document.getElementById('local-video-container');
738
+ if (localVideoContainer) {
739
+ localVideoContainer.style.backgroundColor = localVideoEnabled ? '#222' : '#444';
740
+ }
741
+ }
742
+ }
743
+
744
+ // 切换屏幕共享
745
+ async function toggleScreen() {
746
+ if (!screenStream) {
747
+ try {
748
+ screenStream = await navigator.mediaDevices.getDisplayMedia({
749
+ video: true
750
+ });
751
+
752
+ // 替换所有连接中的视频轨道
753
+ const videoTrack = screenStream.getVideoTracks()[0];
754
+
755
+ Object.values(peerConnections).forEach(pc => {
756
+ const senders = pc.getSenders();
757
+ const sender = senders.find(s => s.track && s.track.kind === 'video');
758
+ if (sender) {
759
+ sender.replaceTrack(videoTrack);
760
+ }
761
+ });
762
+
763
+ // 更新本地视频
764
+ const localVideo = document.querySelector('#local-video-container video');
765
+ if (localVideo) {
766
+ localVideo.srcObject = screenStream;
767
+ }
768
+
769
+ toggleScreenBtn.innerHTML = '<i class="fas fa-desktop"></i> 停止共享';
770
+ toggleScreenBtn.classList.add('red');
771
+
772
+ // 处理屏幕共享结束
773
+ videoTrack.onended = () => {
774
+ stopScreenSharing();
775
+ };
776
+ } catch (error) {
777
+ console.error('无法共享屏幕', error);
778
+ alert('无法共享屏幕,请确保您已授予权限。');
779
+ }
780
+ } else {
781
+ stopScreenSharing();
782
+ }
783
+ }
784
+
785
+ // 停止屏幕共享
786
+ function stopScreenSharing() {
787
+ if (screenStream) {
788
+ screenStream.getTracks().forEach(track => track.stop());
789
+ screenStream = null;
790
+
791
+ // 恢复摄像头视频
792
+ const videoTrack = localStream.getVideoTracks()[0];
793
+ if (videoTrack) {
794
+ Object.values(peerConnections).forEach(pc => {
795
+ const senders = pc.getSenders();
796
+ const sender = senders.find(s => s.track && s.track.kind === 'video');
797
+ if (sender) {
798
+ sender.replaceTrack(videoTrack);
799
+ }
800
+ });
801
+
802
+ // 更新本地视频
803
+ const localVideo = document.querySelector('#local-video-container video');
804
+ if (localVideo) {
805
+ localVideo.srcObject = localStream;
806
+ }
807
+ }
808
+
809
+ toggleScreenBtn.innerHTML = '<i class="fas fa-desktop"></i> 屏幕共享';
810
+ toggleScreenBtn.classList.remove('red');
811
+ }
812
+ }
813
+
814
+ // 离开会议
815
+ function leaveCall() {
816
+ // 关闭所有对等连接
817
+ Object.values(peerConnections).forEach(pc => pc.close());
818
+ peerConnections = {};
819
+
820
+ // 关闭本地媒体流
821
+ if (localStream) {
822
+ localStream.getTracks().forEach(track => track.stop());
823
+ localStream = null;
824
+ }
825
+
826
+ // 关闭屏幕共享流
827
+ if (screenStream) {
828
+ screenStream.getTracks().forEach(track => track.stop());
829
+ screenStream = null;
830
+ }
831
+
832
+ // 从"服务器"断开连接
833
+ if (currentRoom) {
834
+ mockServer.disconnect(currentRoom.id, currentRoom.userId);
835
+ currentRoom = null;
836
+ }
837
+
838
+ // 清空视频网格
839
+ videoGrid.innerHTML = '';
840
+
841
+ // 清空参会者列表
842
+ participantsList.innerHTML = '';
843
+
844
+ // 清空聊天消息
845
+ messages.innerHTML = '';
846
+
847
+ // 切换回登录界面
848
+ meetingContainer.classList.add('hidden');
849
+ joinForm.classList.remove('hidden');
850
+
851
+ // 重置按钮状态
852
+ toggleAudioBtn.innerHTML = '<i class="fas fa-microphone"></i> 麦克风';
853
+ toggleAudioBtn.classList.remove('disabled');
854
+
855
+ toggleVideoBtn.innerHTML = '<i class="fas fa-video"></i> 摄像头';
856
+ toggleVideoBtn.classList.remove('disabled');
857
+
858
+ toggleScreenBtn.innerHTML = '<i class="fas fa-desktop"></i> 屏幕共享';
859
+ toggleScreenBtn.classList.remove('red');
860
+ }
861
+
862
+ // 发送聊天消息
863
+ function sendMessage() {
864
+ const messageText = messageInput.value.trim();
865
+
866
+ if (messageText && currentRoom) {
867
+ const message = {
868
+ text: messageText,
869
+ sender: currentRoom.displayName,
870
+ timestamp: new Date().toISOString()
871
+ };
872
+
873
+ // 显示自己的消息
874
+ displayMessage(currentRoom.userId, message);
875
+
876
+ // 发送消息给所有对等端
877
+ Object.keys(dataChannels).forEach(peerId => {
878
+ const channel = dataChannels[peerId];
879
+ if (channel.readyState === 'open') {
880
+ channel.send(JSON.stringify(message));
881
+ }
882
+ });
883
+
884
+ // 清空输入框
885
+ messageInput.value = '';
886
+ }
887
+ }
888
+
889
+ // 显示聊天消息
890
+ function displayMessage(senderId, message) {
891
+ const messageElement = document.createElement('div');
892
+ messageElement.className = `message ${senderId === currentRoom?.userId ? 'sent' : 'received'}`;
893
+
894
+ const senderElement = document.createElement('div');
895
+ senderElement.className = 'sender';
896
+ senderElement.textContent = message.sender;
897
+
898
+ const textElement = document.createElement('div');
899
+ textElement.textContent = message.text;
900
+
901
+ messageElement.appendChild(senderElement);
902
+ messageElement.appendChild(textElement);
903
+
904
+ messages.appendChild(messageElement);
905
+ messages.scrollTop = messages.scrollHeight;
906
+ }
907
+
908
+ // 复制会议 ID
909
+ function copyRoomId() {
910
+ const roomId = currentRoomIdSpan.textContent;
911
+ navigator.clipboard.writeText(roomId)
912
+ .then(() => {
913
+ alert('会议 ID 已复制到剪贴板');
914
+ })
915
+ .catch(err => {
916
+ console.error('无法复制会议 ID', err);
917
+ alert('无法复制会议 ID,请手动选择并复制');
918
+ });
919
+ }
920
+
921
+ // 切换标签页
922
+ function switchTab(tabName) {
923
+ tabs.forEach(tab => {
924
+ tab.classList.toggle('active', tab.dataset.tab === tabName);
925
+ });
926
+
927
+ tabContents.forEach(content => {
928
+ content.classList.toggle('hidden', content.id !== `${tabName}Tab`);
929
+ });
930
+ }
931
+
932
+ // 事件监听
933
+ createMeetingBtn.addEventListener('click', () => {
934
+ roomIdInput.value = generateId();
935
+ joinMeeting();
936
+ });
937
+
938
+ joinMeetingBtn.addEventListener('click', () => {
939
+ joinMeeting();
940
+ });
941
+
942
+ toggleAudioBtn.addEventListener('click', toggleAudio);
943
+ toggleVideoBtn.addEventListener('click', toggleVideo);
944
+ toggleScreenBtn.addEventListener('click', toggleScreen);
945
+ leaveCallBtn.addEventListener('click', leaveCall);
946
+ copyRoomIdBtn.addEventListener('click', copyRoomId);
947
+
948
+ tabs.forEach(tab => {
949
+ tab.addEventListener('click', () => {
950
+ switchTab(tab.dataset.tab);
951
+ });
952
+ });
953
+
954
+ sendMessageBtn.addEventListener('click', sendMessage);
955
+
956
+ messageInput.addEventListener('keypress', (event) => {
957
+ if (event.key === 'Enter') {
958
+ sendMessage();
959
+ }
960
+ });
961
+
962
+ // 阻止表单提交刷新页面
963
+ document.addEventListener('submit', (event) => {
964
+ event.preventDefault();
965
+ });
966
+ </script>
967
+
968
+ <!-- Font Awesome 图标 -->
969
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script>
970
+ </body>
971
+ </html>