@gogoqiu/tencent-http-server 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/gogoqiu-node-http-service +3 -0
  2. package/dist/build.d.ts +3 -0
  3. package/dist/build.d.ts.map +1 -0
  4. package/dist/build.js +3 -0
  5. package/dist/build.js.map +1 -0
  6. package/dist/routes/index.d.ts +4 -0
  7. package/dist/routes/index.d.ts.map +1 -0
  8. package/dist/routes/index.js +106 -0
  9. package/dist/routes/index.js.map +1 -0
  10. package/dist/server.d.ts +3 -0
  11. package/dist/server.d.ts.map +1 -0
  12. package/dist/server.js +318 -0
  13. package/dist/server.js.map +1 -0
  14. package/dist/serverCmd.d.ts +8 -0
  15. package/dist/serverCmd.d.ts.map +1 -0
  16. package/dist/serverCmd.js +44 -0
  17. package/dist/serverCmd.js.map +1 -0
  18. package/dist/shell/install.d.ts +5 -0
  19. package/dist/shell/install.d.ts.map +1 -0
  20. package/dist/shell/install.js +221 -0
  21. package/dist/shell/install.js.map +1 -0
  22. package/dist/shell/postinst.d.ts +2 -0
  23. package/dist/shell/postinst.d.ts.map +1 -0
  24. package/dist/shell/postinst.js +4 -0
  25. package/dist/shell/postinst.js.map +1 -0
  26. package/dist/shell.d.ts +2 -0
  27. package/dist/shell.d.ts.map +1 -0
  28. package/dist/shell.js +249 -0
  29. package/dist/shell.js.map +1 -0
  30. package/package.json +82 -0
  31. package/public/captures1//346/265/213/350/257/225/347/224/250captures +0 -0
  32. package/public/chat/offer.html +796 -0
  33. package/public/chat/options.html +58 -0
  34. package/public/chat/server-info.html +32 -0
  35. package/public/chat2.html +272 -0
  36. package/public/chat3.html +246 -0
  37. package/public/chat4.html +302 -0
  38. package/public/chat5.html +41 -0
  39. package/public/formdata.html +41 -0
  40. package/public/hls-player.htm +41 -0
  41. package/public/img/back.svg +1 -0
  42. package/public/img/offline.svg +1 -0
  43. package/public/img/online.svg +1 -0
  44. package/public/ip-record.html +28 -0
  45. package/public/js/encrypt.js +36 -0
  46. package/public/js/socket.io.min.js +7 -0
  47. package/public/js/socket.io.min.js.map +1 -0
  48. package/public/myhost/hostReg.html +35 -0
  49. package/public/mylog-chat.htm +260 -0
  50. package/public/mylog3.html +245 -0
  51. package/public/navbar.css +17 -0
  52. package/public/readme.txt +3 -0
  53. package/public/scroll.htm +139 -0
  54. package/public/ssh-client.html +0 -0
  55. package/public/upload-file.html +226 -0
  56. package/public/upload.html +23 -0
  57. package/public/uploads1/files-1757866537383-447469495.jpg +0 -0
  58. package/public/uploads1/files-1757867389485-764531720.jpg +0 -0
  59. package/public/uploads1/files-1757867518311-278635302.jpg +0 -0
  60. package/public/uploads1/files-1757867629687-688924576.jpg +0 -0
  61. package/public/uploads1/files-1757868630683-52261917.jpg +0 -0
  62. package/public/uploads1/files-1757869187061-619427683.jpg +0 -0
  63. package/public/uploads1/small_files-1757869187061-619427683.jpg +0 -0
  64. package/public/uploads1//346/265/213/350/257/225/347/224/250upload +0 -0
  65. package/public/utils.html +57 -0
  66. package/public/utils.js +161 -0
  67. package/public/webrtc/rtc-client.html +238 -0
  68. package/public/webrtc/rtc-file-transfer-client.html +238 -0
  69. package/public/webrtc/rtc-file-transfer-server.html +453 -0
  70. package/public/webrtc/rtc-server.html +453 -0
  71. package/public/webrtc/video-client-input.html +264 -0
  72. package/public/webrtc/video-server-input.html +312 -0
  73. package/public/webrtc/webrtc-chat-with-files.html +581 -0
  74. package/public/webrtc/webrtc-chat.html +367 -0
  75. package/public/webrtc/webrtc-file-offer.html +88 -0
  76. package/public/webrtc/webrtc1.html +186 -0
  77. package/readme.txt +71 -0
  78. package/views/chat.ejs +53 -0
  79. package/views/index.ejs +125 -0
  80. package/views/special-message.ejs +8 -0
  81. package/views/ssh.ejs +142 -0
  82. package/views/utils.ejs +0 -0
  83. package/views/webrtc/client.ejs +203 -0
  84. package/views/webrtc/server.ejs +365 -0
@@ -0,0 +1,581 @@
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>WebRTC 聊天应用 - 带文件传输</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: '#3B82F6',
15
+ secondary: '#10B981',
16
+ danger: '#EF4444',
17
+ dark: '#1E293B',
18
+ light: '#F8FAFC'
19
+ },
20
+ fontFamily: {
21
+ sans: ['Inter', 'system-ui', 'sans-serif'],
22
+ },
23
+ }
24
+ }
25
+ }
26
+ </script>
27
+ <style type="text/tailwindcss">
28
+ @layer utilities {
29
+ .content-auto {
30
+ content-visibility: auto;
31
+ }
32
+ .scrollbar-hide {
33
+ scrollbar-width: none;
34
+ -ms-overflow-style: none;
35
+ }
36
+ .scrollbar-hide::-webkit-scrollbar {
37
+ display: none;
38
+ }
39
+ .progress-bar {
40
+ height: 4px;
41
+ background-color: #e2e8f0;
42
+ border-radius: 2px;
43
+ overflow: hidden;
44
+ }
45
+ .progress-value {
46
+ height: 100%;
47
+ background-color: #3B82F6;
48
+ width: 0%;
49
+ transition: width 0.3s ease;
50
+ }
51
+ }
52
+ </style>
53
+ </head>
54
+ <body class="bg-gray-100 min-h-screen font-sans">
55
+ <div class="container mx-auto px-4 py-8 max-w-4xl">
56
+ <header class="text-center mb-8">
57
+ <h1 class="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-dark mb-2">WebRTC 聊天应用</h1>
58
+ <p class="text-gray-600">实时音频通话、文本聊天与文件传输</p>
59
+ </header>
60
+
61
+ <div class="bg-white rounded-xl shadow-lg overflow-hidden">
62
+ <!-- 连接状态区域 -->
63
+ <div class="bg-primary/10 p-4 border-b flex justify-between items-center">
64
+ <div>
65
+ <h2 class="font-semibold text-dark">连接状态</h2>
66
+ <p id="connectionStatus" class="text-sm text-gray-600">未连接</p>
67
+ </div>
68
+ <div class="flex gap-2">
69
+ <button id="startButton" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition flex items-center gap-1">
70
+ <i class="fa fa-play"></i> 开始
71
+ </button>
72
+ <button id="callButton" class="bg-secondary hover:bg-secondary/90 text-white px-4 py-2 rounded-lg transition flex items-center gap-1" disabled>
73
+ <i class="fa fa-phone"></i> 通话
74
+ </button>
75
+ <button id="hangupButton" class="bg-danger hover:bg-danger/90 text-white px-4 py-2 rounded-lg transition flex items-center gap-1" disabled>
76
+ <i class="fa fa-phone-slash"></i> 挂断
77
+ </button>
78
+ </div>
79
+ </div>
80
+
81
+ <!-- 媒体区域 -->
82
+ <div class="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
83
+ <div class="bg-gray-100 rounded-lg p-2">
84
+ <h3 class="text-sm font-medium text-gray-600 mb-2">本地视频</h3>
85
+ <video id="localVideo" autoplay muted class="w-full aspect-video object-cover rounded bg-gray-200"></video>
86
+ </div>
87
+ <div class="bg-gray-100 rounded-lg p-2">
88
+ <h3 class="text-sm font-medium text-gray-600 mb-2">远程视频</h3>
89
+ <video id="remoteVideo" autoplay class="w-full aspect-video object-cover rounded bg-gray-200"></video>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- 消息和文件传输区域 -->
94
+ <div class="border-t p-4">
95
+ <h3 class="text-lg font-semibold text-dark mb-3">聊天消息与文件</h3>
96
+
97
+ <!-- 文件传输区域 -->
98
+ <div class="mb-4">
99
+ <div class="flex gap-2 mb-3">
100
+ <label for="fileInput" class="bg-gray-200 hover:bg-gray-300 text-dark px-4 py-2 rounded-lg transition flex items-center gap-1 cursor-pointer">
101
+ <i class="fa fa-file"></i> 选择文件
102
+ </label>
103
+ <input type="file" id="fileInput" class="hidden" />
104
+ <button id="sendFileButton" class="bg-secondary hover:bg-secondary/90 text-white px-4 py-2 rounded-lg transition flex items-center gap-1" disabled>
105
+ <i class="fa fa-upload"></i> 发送文件
106
+ </button>
107
+ </div>
108
+
109
+ <!-- 传输进度区域 -->
110
+ <div id="transferProgress" class="hidden mb-4 p-3 bg-gray-50 rounded-lg">
111
+ <div class="flex justify-between items-center mb-1">
112
+ <span id="fileName" class="text-sm font-medium"></span>
113
+ <span id="progressPercent" class="text-sm text-gray-600">0%</span>
114
+ </div>
115
+ <div class="progress-bar">
116
+ <div id="progressBar" class="progress-value"></div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <!-- 消息区域 -->
122
+ <div id="messages" class="h-64 overflow-y-auto mb-4 p-3 bg-gray-50 rounded-lg scrollbar-hide"></div>
123
+ <div class="flex gap-2">
124
+ <input type="text" id="messageInput" placeholder="输入消息..." class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50">
125
+ <button id="sendButton" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-lg transition">
126
+ <i class="fa fa-paper-plane"></i> 发送
127
+ </button>
128
+ </div>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- 信令信息区域 -->
133
+ <div class="mt-6 bg-white rounded-xl shadow-lg p-4">
134
+ <h3 class="text-lg font-semibold text-dark mb-3">信令信息 (用于两个标签页之间测试)</h3>
135
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
136
+ <div>
137
+ <label class="block text-sm font-medium text-gray-600 mb-1">本地描述</label>
138
+ <textarea id="localDescription" class="w-full h-24 p-2 border border-gray-300 rounded-lg text-sm overflow-auto scrollbar-hide" readonly></textarea>
139
+ <button id="copyLocalDesc" class="mt-1 text-sm text-primary hover:text-primary/80">复制</button>
140
+ </div>
141
+ <div>
142
+ <label class="block text-sm font-medium text-gray-600 mb-1">远程描述</label>
143
+ <textarea id="remoteDescription" class="w-full h-24 p-2 border border-gray-300 rounded-lg text-sm overflow-auto scrollbar-hide"></textarea>
144
+ <button id="setRemoteDesc" class="mt-1 text-sm text-primary hover:text-primary/80">应用远程描述</button>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <script>
151
+ // DOM 元素
152
+ const startButton = document.getElementById('startButton');
153
+ const callButton = document.getElementById('callButton');
154
+ const hangupButton = document.getElementById('hangupButton');
155
+ const localVideo = document.getElementById('localVideo');
156
+ const remoteVideo = document.getElementById('remoteVideo');
157
+ const connectionStatus = document.getElementById('connectionStatus');
158
+ const messages = document.getElementById('messages');
159
+ const messageInput = document.getElementById('messageInput');
160
+ const sendButton = document.getElementById('sendButton');
161
+ const localDescription = document.getElementById('localDescription');
162
+ const remoteDescription = document.getElementById('remoteDescription');
163
+ const copyLocalDesc = document.getElementById('copyLocalDesc');
164
+ const setRemoteDesc = document.getElementById('setRemoteDesc');
165
+ const fileInput = document.getElementById('fileInput');
166
+ const sendFileButton = document.getElementById('sendFileButton');
167
+ const transferProgress = document.getElementById('transferProgress');
168
+ const fileName = document.getElementById('fileName');
169
+ const progressBar = document.getElementById('progressBar');
170
+ const progressPercent = document.getElementById('progressPercent');
171
+
172
+ // WebRTC 相关变量
173
+ let localStream;
174
+ let remoteStream;
175
+ let peerConnection;
176
+ let dataChannel;
177
+ let fileReader;
178
+ let currentFile;
179
+ let receivedChunks = [];
180
+ let expectedFileSize = 0;
181
+ let receivedSize = 0;
182
+
183
+ // 配置 STUN 服务器
184
+ const configuration = {
185
+ iceServers: [
186
+ { urls: 'stun:stun.l.google.com:19302' },
187
+ { urls: 'stun:stun1.l.google.com:19302' }
188
+ ]
189
+ };
190
+
191
+ // 分片大小 (16KB)
192
+ const CHUNK_SIZE = 16384;
193
+
194
+ // 状态更新函数
195
+ function updateStatus(text, isError = false) {
196
+ connectionStatus.textContent = text;
197
+ connectionStatus.className = isError ? 'text-sm text-danger' : 'text-sm text-gray-600';
198
+ }
199
+
200
+ // 添加消息到聊天区域
201
+ function addMessage(text, isLocal = false, isFile = false, fileUrl = null) {
202
+ const messageElement = document.createElement('div');
203
+ messageElement.className = `mb-2 p-3 rounded-lg max-w-[80%] ${isLocal ? 'bg-primary/10 text-dark ml-auto' : 'bg-gray-200 text-dark'}`;
204
+
205
+ if (isFile && fileUrl) {
206
+ messageElement.innerHTML = `
207
+ <div class="flex items-center gap-2">
208
+ <i class="fa fa-file-o text-primary"></i>
209
+ <a href="${fileUrl}" download="${text}" class="hover:text-primary">${text}</a>
210
+ </div>
211
+ `;
212
+ } else {
213
+ messageElement.textContent = text;
214
+ }
215
+
216
+ messages.appendChild(messageElement);
217
+ messages.scrollTop = messages.scrollHeight;
218
+ }
219
+
220
+ // 开始按钮 - 获取媒体流
221
+ // initPC
222
+ startButton.addEventListener('click', async () => {
223
+ try {
224
+ // 获取用户媒体流 (音频和视频)
225
+ const stream = await navigator.mediaDevices.getUserMedia({
226
+ audio: true,
227
+ video: { width: 640, height: 480 }
228
+ });
229
+
230
+ localStream = stream;
231
+ localVideo.srcObject = stream;
232
+
233
+ // 创建 RTCPeerConnection 实例
234
+ peerConnection = new RTCPeerConnection(configuration);
235
+
236
+ // 创建数据通道用于文本聊天和文件传输
237
+ dataChannel = peerConnection.createDataChannel('chatAndFile', {
238
+ ordered: true, // 保证数据顺序
239
+ maxRetransmits: 3 // 最大重传次数
240
+ });
241
+ setupDataChannel(dataChannel);
242
+
243
+ // 监听远程数据通道
244
+ peerConnection.ondatachannel = (event) => {
245
+ dataChannel = event.channel;
246
+ setupDataChannel(dataChannel);
247
+ };
248
+
249
+ // 添加本地流到连接
250
+ localStream.getTracks().forEach(track => {
251
+ peerConnection.addTrack(track, localStream);
252
+ });
253
+
254
+ // 监听远程流
255
+ peerConnection.ontrack = (event) => {
256
+ if (!remoteStream) {
257
+ remoteStream = new MediaStream();
258
+ remoteVideo.srcObject = remoteStream;
259
+ }
260
+ event.streams[0].getTracks().forEach(track => {
261
+ remoteStream.addTrack(track);
262
+ });
263
+ };
264
+
265
+ // 监听 ICE 候选
266
+ peerConnection.onicecandidate = (event) => {
267
+ if (event.candidate) {
268
+ console.log('ICE 候选:', event.candidate);
269
+ // 保存到本地存储,用于同一设备测试
270
+ localStorage.setItem('lastIceCandidate', JSON.stringify(event.candidate));
271
+ }
272
+ };
273
+
274
+ // 监听连接状态变化
275
+ peerConnection.oniceconnectionstatechange = () => {
276
+ updateStatus(`ICE 连接状态: ${peerConnection.iceConnectionState}`);
277
+
278
+ if (peerConnection.iceConnectionState === 'connected') {
279
+ addMessage('已连接,开始聊天吧!');
280
+ sendFileButton.disabled = false;
281
+ } else if (peerConnection.iceConnectionState === 'failed') {
282
+ updateStatus('连接失败,请尝试重新连接', true);
283
+ sendFileButton.disabled = true;
284
+ } else if (peerConnection.iceConnectionState === 'disconnected') {
285
+ updateStatus('连接已断开', true);
286
+ sendFileButton.disabled = true;
287
+ }
288
+ };
289
+
290
+ startButton.disabled = true;
291
+ callButton.disabled = false;
292
+ updateStatus('已准备好,等待建立连接');
293
+ } catch (error) {
294
+ console.error('获取媒体流失败:', error);
295
+ updateStatus(`获取媒体失败: ${error.message}`, true);
296
+ }
297
+ });
298
+
299
+ // 配置数据通道
300
+ function setupDataChannel(channel) {
301
+ channel.onopen = () => {
302
+ updateStatus('数据通道已打开,可以发送消息和文件');
303
+ sendButton.disabled = false;
304
+ if (peerConnection.iceConnectionState === 'connected') {
305
+ sendFileButton.disabled = false;
306
+ }
307
+ };
308
+
309
+ channel.onmessage = (event) => {
310
+ handleIncomingData(event.data);
311
+ };
312
+
313
+ channel.onclose = () => {
314
+ updateStatus('数据通道已关闭');
315
+ sendButton.disabled = true;
316
+ sendFileButton.disabled = true;
317
+ };
318
+
319
+ channel.onerror = (error) => {
320
+ console.error('数据通道错误:', error);
321
+ updateStatus(`消息发送失败: ${error}`, true);
322
+ };
323
+ }
324
+
325
+ // 处理接收到的数据
326
+ function handleIncomingData(data) {
327
+ // 如果是文件元数据
328
+ if (typeof data === 'string' && data.startsWith('FILE_METADATA:')) {
329
+ const metadata = JSON.parse(data.replace('FILE_METADATA:', ''));
330
+ expectedFileSize = metadata.size;
331
+ receivedSize = 0;
332
+ receivedChunks = [];
333
+
334
+ addMessage(`正在接收文件: ${metadata.name} (${formatFileSize(metadata.size)})`);
335
+ showProgress(metadata.name, 0);
336
+ return;
337
+ }
338
+
339
+ // 如果是文件结束标记
340
+ if (typeof data === 'string' && data === 'FILE_COMPLETE') {
341
+ const blob = new Blob(receivedChunks);
342
+ const fileUrl = URL.createObjectURL(blob);
343
+ addMessage(currentFileName, false, true, fileUrl);
344
+
345
+ // 清理
346
+ receivedChunks = [];
347
+ expectedFileSize = 0;
348
+ receivedSize = 0;
349
+ hideProgress();
350
+ return;
351
+ }
352
+
353
+ // 如果是文件分片
354
+ if (data instanceof Blob) {
355
+ data.arrayBuffer().then(buffer => {
356
+ receivedChunks.push(buffer);
357
+ receivedSize += buffer.byteLength;
358
+
359
+ const percent = Math.round((receivedSize / expectedFileSize) * 100);
360
+ updateProgress(percent);
361
+ });
362
+ return;
363
+ }
364
+
365
+ // 否则是普通文本消息
366
+ addMessage(`对方: ${data}`, false);
367
+ }
368
+
369
+ // 通话按钮 - 创建offer
370
+ callButton.addEventListener('click', async () => {
371
+ try {
372
+ // 创建 offer
373
+ const offer = await peerConnection.createOffer();
374
+ await peerConnection.setLocalDescription(offer);
375
+
376
+ // 显示本地描述
377
+ localDescription.value = JSON.stringify(offer);
378
+
379
+ callButton.disabled = true;
380
+ hangupButton.disabled = false;
381
+ updateStatus('正在等待对方接受连接...');
382
+ } catch (error) {
383
+ console.error('创建 offer 失败:', error);
384
+ updateStatus(`创建连接失败: ${error.message}`, true);
385
+ }
386
+ });
387
+
388
+ // 挂断按钮
389
+ hangupButton.addEventListener('click', () => {
390
+ // 关闭所有轨道
391
+ if (localStream) {
392
+ localStream.getTracks().forEach(track => track.stop());
393
+ }
394
+
395
+ if (remoteStream) {
396
+ remoteStream.getTracks().forEach(track => track.stop());
397
+ }
398
+
399
+ // 关闭连接
400
+ if (peerConnection) {
401
+ peerConnection.close();
402
+ peerConnection = null;
403
+ }
404
+
405
+ // 关闭数据通道
406
+ if (dataChannel) {
407
+ dataChannel.close();
408
+ dataChannel = null;
409
+ }
410
+
411
+ // 重置UI
412
+ localVideo.srcObject = null;
413
+ remoteVideo.srcObject = null;
414
+ localDescription.value = '';
415
+ remoteDescription.value = '';
416
+ startButton.disabled = false;
417
+ callButton.disabled = true;
418
+ hangupButton.disabled = true;
419
+ sendButton.disabled = true;
420
+ sendFileButton.disabled = true;
421
+ updateStatus('已挂断连接');
422
+ addMessage('通话已结束');
423
+ hideProgress();
424
+ });
425
+
426
+ // 发送消息
427
+ sendButton.addEventListener('click', () => {
428
+ const message = messageInput.value.trim();
429
+ if (message && dataChannel && dataChannel.readyState === 'open') {
430
+ dataChannel.send(message);
431
+ addMessage(`我: ${message}`, true);
432
+ messageInput.value = '';
433
+ }
434
+ });
435
+
436
+ // 按Enter发送消息
437
+ messageInput.addEventListener('keypress', (e) => {
438
+ if (e.key === 'Enter') {
439
+ sendButton.click();
440
+ }
441
+ });
442
+
443
+ // 文件选择处理
444
+ fileInput.addEventListener('change', (e) => {
445
+ if (e.target.files.length > 0) {
446
+ currentFile = e.target.files[0];
447
+ sendFileButton.disabled = !(dataChannel && dataChannel.readyState === 'open');
448
+
449
+ if (currentFile.size > 100 * 1024 * 1024) { // 限制100MB
450
+ alert('文件过大,请选择小于100MB的文件');
451
+ currentFile = null;
452
+ sendFileButton.disabled = true;
453
+ }
454
+ }
455
+ });
456
+
457
+ // 发送文件
458
+ sendFileButton.addEventListener('click', () => {
459
+ if (!currentFile || !dataChannel || dataChannel.readyState !== 'open') return;
460
+
461
+ // 发送文件元数据
462
+ const metadata = {
463
+ name: currentFile.name,
464
+ size: currentFile.size,
465
+ type: currentFile.type
466
+ };
467
+
468
+ dataChannel.send(`FILE_METADATA:${JSON.stringify(metadata)}`);
469
+ addMessage(`正在发送文件: ${currentFile.name} (${formatFileSize(currentFile.size)})`, true);
470
+
471
+ // 显示进度条
472
+ showProgress(currentFile.name, 0);
473
+
474
+ // 分片读取并发送文件
475
+ let offset = 0;
476
+ fileReader = new FileReader();
477
+
478
+ fileReader.onload = (e) => {
479
+ if (e.target.readyState === FileReader.DONE) {
480
+ // 发送分片
481
+ dataChannel.send(e.target.result);
482
+
483
+ // 更新进度
484
+ offset += e.target.result.byteLength;
485
+ const percent = Math.round((offset / currentFile.size) * 100);
486
+ updateProgress(percent);
487
+
488
+ // 继续读取下一分片
489
+ if (offset < currentFile.size) {
490
+ readNextChunk();
491
+ } else {
492
+ // 发送完成标记
493
+ dataChannel.send('FILE_COMPLETE');
494
+ addMessage(`文件发送完成: ${currentFile.name}`, true);
495
+ }
496
+ }
497
+ };
498
+
499
+ // 开始读取第一分片
500
+ readNextChunk();
501
+
502
+ function readNextChunk() {
503
+ const fileSlice = currentFile.slice(offset, offset + CHUNK_SIZE);
504
+ fileReader.readAsArrayBuffer(fileSlice);
505
+ }
506
+ });
507
+
508
+ // 显示进度条
509
+ function showProgress(name, percent) {
510
+ fileName.textContent = name;
511
+ progressBar.style.width = `${percent}%`;
512
+ progressPercent.textContent = `${percent}%`;
513
+ transferProgress.classList.remove('hidden');
514
+ }
515
+
516
+ // 更新进度
517
+ function updateProgress(percent) {
518
+ progressBar.style.width = `${percent}%`;
519
+ progressPercent.textContent = `${percent}%`;
520
+ }
521
+
522
+ // 隐藏进度条
523
+ function hideProgress() {
524
+ transferProgress.classList.add('hidden');
525
+ fileName.textContent = '';
526
+ progressBar.style.width = '0%';
527
+ progressPercent.textContent = '0%';
528
+ }
529
+
530
+ // 格式化文件大小
531
+ function formatFileSize(bytes) {
532
+ if (bytes < 1024) return bytes + ' B';
533
+ else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
534
+ else return (bytes / 1048576).toFixed(1) + ' MB';
535
+ }
536
+
537
+ // 复制本地描述
538
+ copyLocalDesc.addEventListener('click', () => {
539
+ localDescription.select();
540
+ document.execCommand('copy');
541
+ alert('本地描述已复制');
542
+ });
543
+
544
+ // 应用远程描述
545
+ setRemoteDesc.addEventListener('click', async () => {
546
+ try {
547
+ const remoteDesc = JSON.parse(remoteDescription.value);
548
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(remoteDesc));
549
+
550
+ // 如果是offer,创建answer
551
+ if (remoteDesc.type === 'offer') {
552
+ const answer = await peerConnection.createAnswer();
553
+ await peerConnection.setLocalDescription(answer);
554
+ localDescription.value = JSON.stringify(answer);
555
+ }
556
+
557
+ updateStatus('已应用远程描述,正在建立连接...');
558
+
559
+ // 检查是否有ICE候选需要添加
560
+ const lastCandidate = localStorage.getItem('lastIceCandidate');
561
+ if (lastCandidate) {
562
+ try {
563
+ await peerConnection.addIceCandidate(JSON.parse(lastCandidate));
564
+ console.log('已添加远程ICE候选');
565
+ } catch (error) {
566
+ console.error('添加ICE候选失败:', error);
567
+ }
568
+ }
569
+ } catch (error) {
570
+ console.error('设置远程描述失败:', error);
571
+ updateStatus(`设置远程描述失败: ${error.message}`, true);
572
+ }
573
+ });
574
+
575
+ // 初始化状态
576
+ updateStatus('请点击"开始"按钮初始化');
577
+ sendButton.disabled = true;
578
+ sendFileButton.disabled = true;
579
+ </script>
580
+ </body>
581
+ </html>