hu21 1.0.0
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.
- package/index.html +1856 -0
- package/package.json +19 -0
package/index.html
ADDED
|
@@ -0,0 +1,1856 @@
|
|
|
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, user-scalable=no">
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
|
+
<title>投屏播放器</title>
|
|
8
|
+
<style>
|
|
9
|
+
/* 全局样式重置 */
|
|
10
|
+
* {
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
font-family: Arial, sans-serif;
|
|
15
|
+
outline: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
background-color: #1a1a1a;
|
|
20
|
+
color: #fff;
|
|
21
|
+
height: 100vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
font-size: 36px; /* 4K分辨率基础字体 */
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* ========== 核心区域样式(按功能组分隔) ========== */
|
|
28
|
+
/* 1. 标题区域(仅TIZEN-CAST) */
|
|
29
|
+
.title-section {
|
|
30
|
+
padding: 20px;
|
|
31
|
+
background-color: #252525;
|
|
32
|
+
border-bottom: 2px solid #444; /* 下边界线 */
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.history-title {
|
|
40
|
+
font-size: 60px;
|
|
41
|
+
font-weight: 900;
|
|
42
|
+
letter-spacing: 2px;
|
|
43
|
+
background: linear-gradient(90deg, #ff6600, #00cc66, #0078ff, #9900ff);
|
|
44
|
+
-webkit-background-clip: text;
|
|
45
|
+
background-clip: text;
|
|
46
|
+
color: transparent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* 2. 输入控制组(带边界线) */
|
|
50
|
+
.input-section {
|
|
51
|
+
padding: 30px 20px;
|
|
52
|
+
background-color: #252525;
|
|
53
|
+
border-bottom: 2px solid #444; /* 下边界线 */
|
|
54
|
+
flex-shrink: 0;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 40px;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* IP地址激活按钮 */
|
|
62
|
+
.ip-activate-btn {
|
|
63
|
+
background-color: #43a047;
|
|
64
|
+
color: #fff;
|
|
65
|
+
border: none;
|
|
66
|
+
padding: 0 30px;
|
|
67
|
+
border-radius: 10px;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
font-size: 28px;
|
|
70
|
+
height: 80px;
|
|
71
|
+
width: 180px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.protocol-group {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 40px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.protocol-label {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
gap: 15px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
padding: 15px 25px;
|
|
85
|
+
border-radius: 10px;
|
|
86
|
+
font-size: 28px;
|
|
87
|
+
height: 80px;
|
|
88
|
+
align-items: center;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
opacity: 0.5;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.protocol-label.active {
|
|
94
|
+
pointer-events: auto;
|
|
95
|
+
opacity: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.protocol-label input {
|
|
99
|
+
width: 28px;
|
|
100
|
+
height: 28px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.ip-input-group {
|
|
104
|
+
display: flex;
|
|
105
|
+
gap: 20px;
|
|
106
|
+
align-items: center;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* IP输入框固定大小 */
|
|
110
|
+
.ip-segment {
|
|
111
|
+
width: 120px;
|
|
112
|
+
height: 80px;
|
|
113
|
+
padding: 0 15px;
|
|
114
|
+
background-color: #3d3d3d;
|
|
115
|
+
border: 2px solid #555;
|
|
116
|
+
color: #fff;
|
|
117
|
+
text-align: center;
|
|
118
|
+
font-size: 28px;
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
resize: none;
|
|
121
|
+
-webkit-user-modify: read-only;
|
|
122
|
+
user-modify: read-only;
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
opacity: 0.5;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ip-segment.active {
|
|
128
|
+
pointer-events: auto;
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ip-segment.editable {
|
|
133
|
+
-webkit-user-modify: read-write;
|
|
134
|
+
user-modify: read-write;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* 端口输入框固定大小 */
|
|
138
|
+
.port-input {
|
|
139
|
+
width: 150px;
|
|
140
|
+
height: 80px;
|
|
141
|
+
padding: 0 15px;
|
|
142
|
+
background-color: #3d3d3d;
|
|
143
|
+
border: 2px solid #555;
|
|
144
|
+
color: #fff;
|
|
145
|
+
text-align: center;
|
|
146
|
+
font-size: 28px;
|
|
147
|
+
border-radius: 10px;
|
|
148
|
+
resize: none;
|
|
149
|
+
-webkit-user-modify: read-only;
|
|
150
|
+
user-modify: read-only;
|
|
151
|
+
pointer-events: none;
|
|
152
|
+
opacity: 0.5;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.port-input.active {
|
|
156
|
+
pointer-events: auto;
|
|
157
|
+
opacity: 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.port-input.editable {
|
|
161
|
+
-webkit-user-modify: read-write;
|
|
162
|
+
user-modify: read-write;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* IP分隔符加大加粗 */
|
|
166
|
+
.ip-separator {
|
|
167
|
+
font-size: 36px;
|
|
168
|
+
font-weight: 900;
|
|
169
|
+
color: #fff;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.go-btn {
|
|
173
|
+
height: 80px;
|
|
174
|
+
padding: 0 35px;
|
|
175
|
+
background-color: #0078ff;
|
|
176
|
+
color: #fff;
|
|
177
|
+
border: none;
|
|
178
|
+
border-radius: 10px;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
font-size: 28px;
|
|
181
|
+
width: 180px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* 3. 投屏历史组(带边界线) */
|
|
185
|
+
.history-section {
|
|
186
|
+
padding: 30px 20px;
|
|
187
|
+
background-color: #2d2d2d;
|
|
188
|
+
border-bottom: 2px solid #444; /* 下边界线 */
|
|
189
|
+
flex-shrink: 0;
|
|
190
|
+
display: flex;
|
|
191
|
+
flex-direction: column;
|
|
192
|
+
gap: 20px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.history-content {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
width: 100%;
|
|
199
|
+
gap: 20px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.history-list {
|
|
203
|
+
display: flex;
|
|
204
|
+
flex-wrap: nowrap; /* 禁止换行 */
|
|
205
|
+
justify-content: center; /* 水平居中 */
|
|
206
|
+
align-items: center; /* 垂直居中 */
|
|
207
|
+
gap: 15px;
|
|
208
|
+
max-height: none; /* 取消最大高度限制 */
|
|
209
|
+
overflow: hidden; /* 隐藏溢出(但实际最多4条,不会溢出) */
|
|
210
|
+
flex: 1;
|
|
211
|
+
padding: 10px;
|
|
212
|
+
border: 1px solid #555;
|
|
213
|
+
border-radius: 8px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* 简化IP历史记录外边框(单色,调小尺寸) */
|
|
217
|
+
.history-item {
|
|
218
|
+
background-color: #3d3d3d;
|
|
219
|
+
padding: 12px 18px; /* 调小内边距 */
|
|
220
|
+
border-radius: 10px; /* 调小圆角 */
|
|
221
|
+
display: inline-flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
justify-content: center;
|
|
224
|
+
cursor: pointer;
|
|
225
|
+
width: 400px; /* 调小宽度 */
|
|
226
|
+
height: 70px; /* 调小高度 */
|
|
227
|
+
font-size: 26px; /* 调小字体 */
|
|
228
|
+
/* 单色边框核心 */
|
|
229
|
+
border: 2px solid #0078ff; /* 单一蓝色边框,无渐变 */
|
|
230
|
+
position: relative;
|
|
231
|
+
z-index: 1;
|
|
232
|
+
transition: all 0.3s ease;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.history-item:hover {
|
|
236
|
+
background-color: #4a4a4a; /* hover仅变背景色 */
|
|
237
|
+
border-color: #ff6600; /* hover边框变橙色 */
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.history-item:focus {
|
|
241
|
+
outline: none;
|
|
242
|
+
background-color: #4a4a4a;
|
|
243
|
+
border-color: #ff6600;
|
|
244
|
+
box-shadow: 0 0 15px rgba(255, 102, 0, 0.5); /* 聚焦发光 */
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.history-url {
|
|
248
|
+
font-size: 26px; /* 匹配调小的字体 */
|
|
249
|
+
color: #0078ff;
|
|
250
|
+
white-space: nowrap;
|
|
251
|
+
text-align: center;
|
|
252
|
+
width: 100%;
|
|
253
|
+
position: relative;
|
|
254
|
+
z-index: 2;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.clear-history {
|
|
258
|
+
background-color: #6a1b9a;
|
|
259
|
+
color: #fff;
|
|
260
|
+
border: none;
|
|
261
|
+
padding: 15px 30px;
|
|
262
|
+
border-radius: 10px;
|
|
263
|
+
cursor: pointer;
|
|
264
|
+
font-size: 28px;
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
width: 180px;
|
|
267
|
+
height: 80px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.empty-history {
|
|
271
|
+
font-size: 28px;
|
|
272
|
+
color: #999;
|
|
273
|
+
padding: 15px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* 4. 播放器组(带边界线) */
|
|
277
|
+
.play-section {
|
|
278
|
+
display: flex;
|
|
279
|
+
flex: 1;
|
|
280
|
+
border-top: 2px solid #444; /* 上边界线 */
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.play-control-panel {
|
|
284
|
+
width: 600px; /* 固定左侧操作说明宽度 */
|
|
285
|
+
background-color: #222;
|
|
286
|
+
padding: 20px;
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
gap: 30px;
|
|
290
|
+
flex-shrink: 0; /* 禁止左侧缩小 */
|
|
291
|
+
border-right: 2px solid #444; /* 右边界线 */
|
|
292
|
+
justify-content: flex-start;
|
|
293
|
+
text-align: center;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#currentPlaySection {
|
|
297
|
+
width: 100%;
|
|
298
|
+
margin-bottom: 30px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.current-play-title {
|
|
302
|
+
font-size: 40px;
|
|
303
|
+
background: linear-gradient(90deg, #ff6600, #00cc66, #0078ff);
|
|
304
|
+
-webkit-background-clip: text;
|
|
305
|
+
background-clip: text;
|
|
306
|
+
color: transparent;
|
|
307
|
+
line-height: 1.4;
|
|
308
|
+
font-weight: bold;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.operation-title {
|
|
312
|
+
font-size: 40px;
|
|
313
|
+
color: #ccc;
|
|
314
|
+
margin-bottom: 20px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.operation-list {
|
|
318
|
+
list-style: none;
|
|
319
|
+
font-size: 36px;
|
|
320
|
+
color: #eee;
|
|
321
|
+
line-height: 2;
|
|
322
|
+
padding: 0;
|
|
323
|
+
margin: 0 auto;
|
|
324
|
+
text-align: left;
|
|
325
|
+
max-width: 90%;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.operation-list li {
|
|
329
|
+
margin-bottom: 15px;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/* 播放器容器(右侧) */
|
|
333
|
+
.player-panel {
|
|
334
|
+
flex: 1; /* 占满剩余全部空间 */
|
|
335
|
+
display: flex;
|
|
336
|
+
flex-direction: column;
|
|
337
|
+
position: relative;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* 视频容器(包含视频和全屏按钮) */
|
|
341
|
+
.video-container {
|
|
342
|
+
width: 100%;
|
|
343
|
+
height: 100%;
|
|
344
|
+
display: block; /* 始终显示 */
|
|
345
|
+
position: relative;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#video-player {
|
|
349
|
+
width: 100%;
|
|
350
|
+
height: 100%;
|
|
351
|
+
object-fit: contain;
|
|
352
|
+
background-color: #000;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* 始终显示原生控件,全屏时隐藏 */
|
|
356
|
+
video::-webkit-media-controls {
|
|
357
|
+
display: flex !important;
|
|
358
|
+
}
|
|
359
|
+
:fullscreen video::-webkit-media-controls {
|
|
360
|
+
display: none !important;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* 自定义全屏按钮(初始显示在右下角,仅全屏时隐藏) */
|
|
364
|
+
.custom-fullscreen-btn {
|
|
365
|
+
position: absolute;
|
|
366
|
+
bottom: 30px;
|
|
367
|
+
right: 30px;
|
|
368
|
+
width: 80px;
|
|
369
|
+
height: 80px;
|
|
370
|
+
background-color: rgba(0,0,0,0.7);
|
|
371
|
+
color: #fff;
|
|
372
|
+
border: 2px solid #666;
|
|
373
|
+
border-radius: 50%;
|
|
374
|
+
cursor: pointer;
|
|
375
|
+
font-size: 36px;
|
|
376
|
+
display: flex !important; /* 强制初始显示 */
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
z-index: 30; /* 最高层级,确保在所有元素上层 */
|
|
380
|
+
transition: all 0.3s ease;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
:fullscreen .custom-fullscreen-btn {
|
|
384
|
+
display: none !important; /* 仅全屏时隐藏 */
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* 播放器占位符(初始显示) */
|
|
388
|
+
.player-placeholder {
|
|
389
|
+
width: 100%;
|
|
390
|
+
height: 100%;
|
|
391
|
+
display: flex;
|
|
392
|
+
flex-direction: column;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
color: #999;
|
|
396
|
+
font-size: 36px;
|
|
397
|
+
gap: 30px;
|
|
398
|
+
position: absolute; /* 绝对定位,与视频重叠 */
|
|
399
|
+
top: 0;
|
|
400
|
+
left: 0;
|
|
401
|
+
z-index: 10; /* 层级低于全屏按钮 */
|
|
402
|
+
background-color: #111;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.player-icon {
|
|
406
|
+
font-size: 90px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* 弹窗样式 */
|
|
410
|
+
.modal {
|
|
411
|
+
position: fixed;
|
|
412
|
+
top: 0;
|
|
413
|
+
left: 0;
|
|
414
|
+
width: 100%;
|
|
415
|
+
height: 100%;
|
|
416
|
+
background-color: rgba(0,0,0,0.8);
|
|
417
|
+
display: none;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
z-index: 1000;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.modal-content {
|
|
424
|
+
background-color: #333;
|
|
425
|
+
padding: 50px;
|
|
426
|
+
border-radius: 15px;
|
|
427
|
+
min-width: 800px;
|
|
428
|
+
text-align: center;
|
|
429
|
+
border: 2px solid #666;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.modal-message {
|
|
433
|
+
font-size: 36px;
|
|
434
|
+
margin-bottom: 40px;
|
|
435
|
+
color: #eee;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.modal-buttons {
|
|
439
|
+
display: flex;
|
|
440
|
+
gap: 30px;
|
|
441
|
+
justify-content: center;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.modal-btn {
|
|
445
|
+
padding: 20px 40px;
|
|
446
|
+
border: none;
|
|
447
|
+
border-radius: 10px;
|
|
448
|
+
cursor: pointer;
|
|
449
|
+
font-size: 36px;
|
|
450
|
+
color: #fff;
|
|
451
|
+
min-width: 180px;
|
|
452
|
+
height: 80px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.confirm-btn {
|
|
456
|
+
background-color: #0078ff;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.cancel-btn {
|
|
460
|
+
background-color: #666;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.ok-btn {
|
|
464
|
+
background-color: #0078ff;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* 焦点样式(仅针对指定元素) */
|
|
468
|
+
[tabindex="0"]:focus {
|
|
469
|
+
outline: 3px solid #ff6600 !important;
|
|
470
|
+
outline-offset: 2px !important;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
[tabindex="-1"] {
|
|
474
|
+
outline: none !important;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* 焦点丢失提示样式 */
|
|
478
|
+
.focus-lost-indicator {
|
|
479
|
+
position: fixed;
|
|
480
|
+
top: 20px;
|
|
481
|
+
right: 20px;
|
|
482
|
+
background: #ff6600;
|
|
483
|
+
color: #fff;
|
|
484
|
+
padding: 15px 25px;
|
|
485
|
+
border-radius: 10px;
|
|
486
|
+
font-size: 28px;
|
|
487
|
+
z-index: 9999;
|
|
488
|
+
display: none;
|
|
489
|
+
}
|
|
490
|
+
</style>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<!-- 1. 标题区域(仅TIZEN-CAST,第一行居中) -->
|
|
494
|
+
<div class="title-section">
|
|
495
|
+
<div class="history-title">TIZEN-CAST</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<!-- 2. 输入控制组(IP按钮+协议+IP输入+投屏按钮) -->
|
|
499
|
+
<div class="input-section">
|
|
500
|
+
<button id="ipActivateBtn" class="ip-activate-btn" tabindex="0">IP地址</button>
|
|
501
|
+
|
|
502
|
+
<div class="protocol-group">
|
|
503
|
+
<label class="protocol-label" tabindex="-1">
|
|
504
|
+
<input type="radio" name="protocol" value="http" checked> HTTP
|
|
505
|
+
</label>
|
|
506
|
+
<label class="protocol-label" tabindex="-1">
|
|
507
|
+
<input type="radio" name="protocol" value="https"> HTTPS
|
|
508
|
+
</label>
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<div class="ip-input-group">
|
|
512
|
+
<input type="text" class="ip-segment" maxlength="3" placeholder="192" tabindex="-1">
|
|
513
|
+
<span class="ip-separator">.</span>
|
|
514
|
+
<input type="text" class="ip-segment" maxlength="3" placeholder="168" tabindex="-1">
|
|
515
|
+
<span class="ip-separator">.</span>
|
|
516
|
+
<input type="text" class="ip-segment" maxlength="3" placeholder="1" tabindex="-1">
|
|
517
|
+
<span class="ip-separator">.</span>
|
|
518
|
+
<input type="text" class="ip-segment" maxlength="3" placeholder="6" tabindex="-1">
|
|
519
|
+
<span class="ip-separator">:</span>
|
|
520
|
+
<input type="text" class="port-input" maxlength="5" placeholder="52020" tabindex="-1">
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<button id="goBtn" class="go-btn" tabindex="0">投屏</button>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<!-- 3. 投屏历史组(投屏历史+清空按钮) -->
|
|
527
|
+
<div class="history-section">
|
|
528
|
+
<div class="history-content">
|
|
529
|
+
<div id="historyList" class="history-list">
|
|
530
|
+
<div class="empty-history">暂无投屏历史</div>
|
|
531
|
+
</div>
|
|
532
|
+
<button id="clearHistory" class="clear-history" tabindex="0">清空历史</button>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<!-- 4. 播放器组 -->
|
|
537
|
+
<div class="play-section">
|
|
538
|
+
<div class="play-control-panel" tabindex="-1">
|
|
539
|
+
<div id="currentPlaySection" style="display: none;">
|
|
540
|
+
<div id="currentPlayTitle" class="current-play-title">正在播放:未知剧集</div>
|
|
541
|
+
</div>
|
|
542
|
+
<div class="operation-guide">
|
|
543
|
+
<div class="operation-title">操作说明</div>
|
|
544
|
+
<ul class="operation-list">
|
|
545
|
+
<li>↑↓←→:切换焦点/控制视频</li>
|
|
546
|
+
<li>Enter/OK:确认/播放/暂停(全屏)</li>
|
|
547
|
+
<li>Back/ESC:退出全屏/关闭弹窗/重置</li>
|
|
548
|
+
<li>←→:快退/快进10秒</li>
|
|
549
|
+
<li>↑↓:音量增减</li>
|
|
550
|
+
<li>连续按2次Back/ESC:退出程序</li>
|
|
551
|
+
</ul>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
<div class="player-panel">
|
|
556
|
+
<!-- 视频容器(包含视频和全屏按钮) -->
|
|
557
|
+
<div id="video-container" class="video-container">
|
|
558
|
+
<!-- 播放器占位符 -->
|
|
559
|
+
<div id="player-placeholder" class="player-placeholder">
|
|
560
|
+
<div class="player-icon">📺</div>
|
|
561
|
+
<div class="player-tip">输入IP后按Enter投屏</div>
|
|
562
|
+
</div>
|
|
563
|
+
<!-- 视频元素 -->
|
|
564
|
+
<video id="video-player"></video>
|
|
565
|
+
<!-- 全屏按钮(初始显示在右下角) -->
|
|
566
|
+
<button id="customFullscreenBtn" class="custom-fullscreen-btn" tabindex="0">⛶</button>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- 进度提示弹窗 -->
|
|
572
|
+
<div id="progressModal" class="modal">
|
|
573
|
+
<div class="modal-content">
|
|
574
|
+
<div class="modal-message">检测到上次未播放完的视频,是否继续播放?</div>
|
|
575
|
+
<div class="modal-buttons">
|
|
576
|
+
<button id="confirmProgress" class="modal-btn confirm-btn" tabindex="0">确定</button>
|
|
577
|
+
<button id="cancelProgress" class="modal-btn cancel-btn" tabindex="0">取消</button>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<!-- 错误提示弹窗 -->
|
|
583
|
+
<div id="errorModal" class="modal">
|
|
584
|
+
<div class="modal-content">
|
|
585
|
+
<div id="errorMessage" class="modal-message">播放错误</div>
|
|
586
|
+
<div class="modal-buttons">
|
|
587
|
+
<button id="okError" class="modal-btn ok-btn" tabindex="0">确定</button>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- 退出提示弹窗 -->
|
|
593
|
+
<div id="exitModal" class="modal">
|
|
594
|
+
<div class="modal-content">
|
|
595
|
+
<div class="modal-message">确定要退出程序吗?</div>
|
|
596
|
+
<div class="modal-buttons">
|
|
597
|
+
<button id="confirmExit" class="modal-btn confirm-btn" tabindex="0">确定</button>
|
|
598
|
+
<button id="cancelExit" class="modal-btn cancel-btn" tabindex="0">取消</button>
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<!-- 引入依赖 -->
|
|
604
|
+
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
605
|
+
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.12/dist/hls.min.js"></script>
|
|
606
|
+
<script>
|
|
607
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
608
|
+
// 核心变量
|
|
609
|
+
let focusableElements = [];
|
|
610
|
+
let currentFocusIndex = 0;
|
|
611
|
+
let hls = null;
|
|
612
|
+
let targetServerUrl = '';
|
|
613
|
+
let windowHeaders = {};
|
|
614
|
+
let currentEpisodeTitle = '未知剧集';
|
|
615
|
+
let currentVideoUrl = '';
|
|
616
|
+
const SKIP_SECONDS = 10;
|
|
617
|
+
const VOLUME_STEP = 0.1;
|
|
618
|
+
const isTizenEnv = !!window.tizen;
|
|
619
|
+
let isModalActive = false;
|
|
620
|
+
let backPressCount = 0;
|
|
621
|
+
let backPressTimer = null;
|
|
622
|
+
let isInputEditable = false;
|
|
623
|
+
let isIpInputActive = false; // IP输入框激活状态
|
|
624
|
+
let errorModalTimer = null; // 错误弹窗异步定时器
|
|
625
|
+
let isClosingModalWithEnter = false; // 标记关闭弹窗的Enter事件
|
|
626
|
+
let errorModalEventsBound = false; // 错误弹窗事件绑定标记
|
|
627
|
+
|
|
628
|
+
// DOM元素
|
|
629
|
+
const videoContainer = document.getElementById('video-container');
|
|
630
|
+
const videoPlayer = document.getElementById('video-player');
|
|
631
|
+
const currentPlaySection = document.getElementById('currentPlaySection');
|
|
632
|
+
const currentPlayTitle = document.getElementById('currentPlayTitle');
|
|
633
|
+
const goBtn = document.getElementById('goBtn');
|
|
634
|
+
const customFullscreenBtn = document.getElementById('customFullscreenBtn');
|
|
635
|
+
const ipActivateBtn = document.getElementById('ipActivateBtn');
|
|
636
|
+
|
|
637
|
+
// 弹窗元素
|
|
638
|
+
const progressModal = document.getElementById('progressModal');
|
|
639
|
+
const confirmProgressBtn = document.getElementById('confirmProgress');
|
|
640
|
+
const cancelProgressBtn = document.getElementById('cancelProgress');
|
|
641
|
+
const errorModal = document.getElementById('errorModal');
|
|
642
|
+
const errorMessage = document.getElementById('errorMessage');
|
|
643
|
+
const okErrorBtn = document.getElementById('okError');
|
|
644
|
+
const exitModal = document.getElementById('exitModal');
|
|
645
|
+
const confirmExitBtn = document.getElementById('confirmExit');
|
|
646
|
+
const cancelExitBtn = document.getElementById('cancelExit');
|
|
647
|
+
|
|
648
|
+
// 输入框组
|
|
649
|
+
const ipInputs = Array.from(document.querySelectorAll('.ip-segment'));
|
|
650
|
+
const portInput = document.querySelector('.port-input');
|
|
651
|
+
const inputGroup = [...ipInputs, portInput];
|
|
652
|
+
const protocolLabels = Array.from(document.querySelectorAll('.protocol-label'));
|
|
653
|
+
const DEFAULT_IP_SEGMENTS = ['192', '168', '1', '6']; // 默认IP分段:192.168.1.6
|
|
654
|
+
const DEFAULT_PORT = '52020';
|
|
655
|
+
|
|
656
|
+
// ========== 基础工具函数 ==========
|
|
657
|
+
function isFullscreen() {
|
|
658
|
+
return !!document.fullscreenElement || !!document.webkitFullscreenElement;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ========== 1. 弹窗焦点管理(统一重构,修复所有冲突) ==========
|
|
662
|
+
// 弹窗焦点陷阱:统一逻辑,支持所有弹窗
|
|
663
|
+
function trapFocusInModal(modalElements, modalDom, defaultIndex = 0) {
|
|
664
|
+
if (isModalActive) return; // 防止重复激活
|
|
665
|
+
|
|
666
|
+
isModalActive = true; // 标记弹窗激活
|
|
667
|
+
let currentModalIndex = Math.max(0, Math.min(defaultIndex, modalElements.length - 1));
|
|
668
|
+
const savedFocusIndex = currentFocusIndex; // 保存之前的焦点索引
|
|
669
|
+
|
|
670
|
+
// 创建焦点丢失提示框(只创建一次)
|
|
671
|
+
if (!document.getElementById('focusLostIndicator')) {
|
|
672
|
+
const indicatorEl = document.createElement('div');
|
|
673
|
+
indicatorEl.id = 'focusLostIndicator';
|
|
674
|
+
indicatorEl.className = 'focus-lost-indicator';
|
|
675
|
+
indicatorEl.textContent = '⚠️ 焦点丢失,按Enter重置';
|
|
676
|
+
document.body.appendChild(indicatorEl);
|
|
677
|
+
}
|
|
678
|
+
const focusLostIndicator = document.getElementById('focusLostIndicator');
|
|
679
|
+
|
|
680
|
+
// 清空之前的事件监听
|
|
681
|
+
modalElements.forEach(el => {
|
|
682
|
+
el.removeEventListener('keydown', handleModalKeydown);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// 初始聚焦到默认元素(增加异常处理)
|
|
686
|
+
const initFocusElement = modalElements[currentModalIndex];
|
|
687
|
+
if (initFocusElement) {
|
|
688
|
+
initFocusElement.focus({ preventScroll: true });
|
|
689
|
+
initFocusElement.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 焦点守卫:防止焦点移出弹窗
|
|
693
|
+
const focusGuard = function(e) {
|
|
694
|
+
if (!isModalActive) return;
|
|
695
|
+
|
|
696
|
+
if (!modalElements.includes(e.target)) {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
e.stopImmediatePropagation();
|
|
699
|
+
|
|
700
|
+
const targetElement = modalElements[currentModalIndex] || modalElements[0];
|
|
701
|
+
if (targetElement) {
|
|
702
|
+
targetElement.focus({ preventScroll: true });
|
|
703
|
+
modalElements.forEach(el => el.style.boxShadow = '');
|
|
704
|
+
targetElement.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
705
|
+
focusLostIndicator.style.display = 'none';
|
|
706
|
+
} else {
|
|
707
|
+
focusLostIndicator.style.display = 'block';
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
currentModalIndex = modalElements.indexOf(e.target);
|
|
711
|
+
modalElements.forEach(el => el.style.boxShadow = '');
|
|
712
|
+
e.target.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
713
|
+
focusLostIndicator.style.display = 'none';
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
document.addEventListener('focusin', focusGuard, { capture: true, passive: false });
|
|
718
|
+
|
|
719
|
+
// 弹窗按键处理:统一逻辑
|
|
720
|
+
function handleModalKeydown(e) {
|
|
721
|
+
if (!isModalActive) return;
|
|
722
|
+
|
|
723
|
+
// 焦点丢失时按Enter重置
|
|
724
|
+
if (e.key === 'Enter' && focusLostIndicator.style.display === 'block') {
|
|
725
|
+
e.preventDefault();
|
|
726
|
+
const targetElement = modalElements[0];
|
|
727
|
+
targetElement.focus({ preventScroll: true });
|
|
728
|
+
modalElements.forEach(el => el.style.boxShadow = '');
|
|
729
|
+
targetElement.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
730
|
+
focusLostIndicator.style.display = 'none';
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Enter/OK键:触发点击
|
|
735
|
+
if (e.key === 'Enter') {
|
|
736
|
+
e.preventDefault();
|
|
737
|
+
e.stopImmediatePropagation(); // 阻止冒泡到全局
|
|
738
|
+
modalElements[currentModalIndex].click();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// 方向键切换焦点
|
|
743
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
744
|
+
e.preventDefault();
|
|
745
|
+
currentModalIndex = (currentModalIndex + 1) % modalElements.length;
|
|
746
|
+
const targetElement = modalElements[currentModalIndex];
|
|
747
|
+
targetElement.focus({ preventScroll: true });
|
|
748
|
+
modalElements.forEach(el => el.style.boxShadow = '');
|
|
749
|
+
targetElement.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
750
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
751
|
+
e.preventDefault();
|
|
752
|
+
currentModalIndex = (currentModalIndex - 1 + modalElements.length) % modalElements.length;
|
|
753
|
+
const targetElement = modalElements[currentModalIndex];
|
|
754
|
+
targetElement.focus({ preventScroll: true });
|
|
755
|
+
modalElements.forEach(el => el.style.boxShadow = '');
|
|
756
|
+
targetElement.style.boxShadow = '0 0 0 4px #ff6600, 0 0 0 8px rgba(255,102,0,0.3)';
|
|
757
|
+
} else if (e.key === 'Escape' || e.key === 'Back') {
|
|
758
|
+
e.preventDefault();
|
|
759
|
+
e.stopImmediatePropagation();
|
|
760
|
+
closeModal(modalDom, savedFocusIndex);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 绑定弹窗按键事件
|
|
765
|
+
modalElements.forEach(el => {
|
|
766
|
+
el.addEventListener('keydown', handleModalKeydown);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// 返回释放焦点的函数
|
|
770
|
+
return function releaseFocus() {
|
|
771
|
+
isModalActive = false;
|
|
772
|
+
modalElements.forEach(el => {
|
|
773
|
+
el.removeEventListener('keydown', handleModalKeydown);
|
|
774
|
+
el.style.boxShadow = '';
|
|
775
|
+
});
|
|
776
|
+
document.removeEventListener('focusin', focusGuard, { capture: true });
|
|
777
|
+
focusLostIndicator.style.display = 'none';
|
|
778
|
+
setFocus(savedFocusIndex);
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 关闭弹窗:统一逻辑
|
|
783
|
+
function closeModal(modalDom, restoreIndex = 0) {
|
|
784
|
+
modalDom.style.display = 'none';
|
|
785
|
+
isModalActive = false;
|
|
786
|
+
setTimeout(() => {
|
|
787
|
+
setFocus(restoreIndex);
|
|
788
|
+
}, 50); // 延迟恢复焦点,避免Enter连锁触发
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ========== 2. 弹窗逻辑重构(修复所有已知问题) ==========
|
|
792
|
+
// 错误弹窗:修复Enter连锁触发、异步弹窗、重复绑定
|
|
793
|
+
function triggerErrorModal(message, delay = 0) {
|
|
794
|
+
// 取消之前未执行的弹窗
|
|
795
|
+
if (errorModalTimer) {
|
|
796
|
+
clearTimeout(errorModalTimer);
|
|
797
|
+
errorModalTimer = null;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 全屏状态下不弹出
|
|
801
|
+
if (isFullscreen()) {
|
|
802
|
+
console.warn('全屏状态下,取消错误弹窗弹出');
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (delay > 0) {
|
|
807
|
+
errorModalTimer = setTimeout(() => {
|
|
808
|
+
if (!isFullscreen() && message) {
|
|
809
|
+
showErrorModal(message);
|
|
810
|
+
}
|
|
811
|
+
errorModalTimer = null;
|
|
812
|
+
}, delay);
|
|
813
|
+
} else {
|
|
814
|
+
showErrorModal(message);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function showErrorModal(message) {
|
|
819
|
+
if (isFullscreen() || isModalActive || !message) return;
|
|
820
|
+
|
|
821
|
+
isModalActive = true;
|
|
822
|
+
const currentActiveElement = document.activeElement;
|
|
823
|
+
const savedFocusIndex = focusableElements.indexOf(currentActiveElement);
|
|
824
|
+
|
|
825
|
+
errorMessage.textContent = message;
|
|
826
|
+
errorModal.style.display = 'flex';
|
|
827
|
+
|
|
828
|
+
const modalElements = [okErrorBtn];
|
|
829
|
+
const releaseFocus = trapFocusInModal(modalElements, errorModal, 0);
|
|
830
|
+
|
|
831
|
+
// 统一关闭逻辑
|
|
832
|
+
const closeErrorModal = function() {
|
|
833
|
+
isClosingModalWithEnter = false;
|
|
834
|
+
errorModalEventsBound = false;
|
|
835
|
+
closeModal(errorModal, savedFocusIndex);
|
|
836
|
+
releaseFocus();
|
|
837
|
+
okErrorBtn.removeEventListener('click', closeErrorModal);
|
|
838
|
+
okErrorBtn.removeEventListener('keydown', handleEnterKey);
|
|
839
|
+
errorModal.style.display = 'none'; // 兜底隐藏
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
// Enter键处理
|
|
843
|
+
const handleEnterKey = function(e) {
|
|
844
|
+
if (e.key === 'Enter' && e.isTrusted) {
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
e.stopImmediatePropagation();
|
|
847
|
+
isClosingModalWithEnter = true;
|
|
848
|
+
closeErrorModal();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// 防止重复绑定
|
|
853
|
+
if (!errorModalEventsBound) {
|
|
854
|
+
okErrorBtn.addEventListener('click', closeErrorModal);
|
|
855
|
+
okErrorBtn.addEventListener('keydown', handleEnterKey);
|
|
856
|
+
errorModalEventsBound = true;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// 强制聚焦
|
|
860
|
+
setTimeout(() => {
|
|
861
|
+
if (errorModal.style.display === 'flex') {
|
|
862
|
+
okErrorBtn.focus({ preventScroll: true });
|
|
863
|
+
}
|
|
864
|
+
}, 0);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// 进度弹窗:统一逻辑,修复焦点问题
|
|
868
|
+
let callback, lastPos;
|
|
869
|
+
let isProgressModalShow = false;
|
|
870
|
+
function setupModalFocus() {
|
|
871
|
+
if (isModalActive) return;
|
|
872
|
+
|
|
873
|
+
const currentActiveElement = document.activeElement;
|
|
874
|
+
const savedFocusIndex = focusableElements.indexOf(currentActiveElement);
|
|
875
|
+
|
|
876
|
+
const modalElements = [confirmProgressBtn, cancelProgressBtn];
|
|
877
|
+
const releaseFocus = trapFocusInModal(modalElements, progressModal, 0);
|
|
878
|
+
|
|
879
|
+
confirmProgressBtn.onclick = function() {
|
|
880
|
+
closeModal(progressModal, savedFocusIndex);
|
|
881
|
+
releaseFocus();
|
|
882
|
+
isProgressModalShow = false;
|
|
883
|
+
videoPlayer.addEventListener('loadedmetadata', () => {
|
|
884
|
+
videoPlayer.currentTime = lastPos;
|
|
885
|
+
}, { once: true });
|
|
886
|
+
callback();
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
cancelProgressBtn.onclick = function() {
|
|
890
|
+
closeModal(progressModal, savedFocusIndex);
|
|
891
|
+
releaseFocus();
|
|
892
|
+
isProgressModalShow = false;
|
|
893
|
+
callback();
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// 退出弹窗:统一逻辑
|
|
898
|
+
function showExitModal() {
|
|
899
|
+
if (isModalActive) return;
|
|
900
|
+
|
|
901
|
+
exitModal.style.display = 'flex';
|
|
902
|
+
const modalElements = [confirmExitBtn, cancelExitBtn];
|
|
903
|
+
const releaseFocus = trapFocusInModal(modalElements, exitModal, 0);
|
|
904
|
+
|
|
905
|
+
confirmExitBtn.onclick = function() {
|
|
906
|
+
closeModal(exitModal);
|
|
907
|
+
releaseFocus();
|
|
908
|
+
if (isTizenEnv && window.tizen.application.getCurrentApplication) {
|
|
909
|
+
const app = window.tizen.application.getCurrentApplication();
|
|
910
|
+
window.tizen.application.exit(app);
|
|
911
|
+
} else {
|
|
912
|
+
window.close();
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
cancelExitBtn.onclick = function() {
|
|
917
|
+
closeModal(exitModal);
|
|
918
|
+
releaseFocus();
|
|
919
|
+
backPressCount = 0;
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ========== 3. 全屏按钮控制(修复逻辑冲突) ==========
|
|
924
|
+
function initFullscreenButton() {
|
|
925
|
+
// 更新全屏按钮状态
|
|
926
|
+
function updateFullscreenBtn() {
|
|
927
|
+
if (isFullscreen()) {
|
|
928
|
+
customFullscreenBtn.style.display = 'none';
|
|
929
|
+
} else {
|
|
930
|
+
customFullscreenBtn.style.display = 'flex';
|
|
931
|
+
customFullscreenBtn.textContent = '⛶';
|
|
932
|
+
focusFullscreenBtn();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// 绑定自定义全屏按钮事件
|
|
937
|
+
customFullscreenBtn.addEventListener('click', function() {
|
|
938
|
+
toggleFullscreen();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
customFullscreenBtn.addEventListener('keydown', function(e) {
|
|
942
|
+
if ((e.key === 'Enter') && !isModalActive && e.isTrusted && !isFullscreen()) {
|
|
943
|
+
e.preventDefault();
|
|
944
|
+
e.stopPropagation(); // 阻止冒泡
|
|
945
|
+
toggleFullscreen(); // 直接调用,避免this.click()重复触发
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
// 监听全屏变化
|
|
950
|
+
document.addEventListener('fullscreenchange', updateFullscreenBtn);
|
|
951
|
+
document.addEventListener('webkitfullscreenchange', updateFullscreenBtn);
|
|
952
|
+
document.addEventListener('mozfullscreenchange', updateFullscreenBtn);
|
|
953
|
+
document.addEventListener('MSFullscreenChange', updateFullscreenBtn);
|
|
954
|
+
|
|
955
|
+
// 初始状态
|
|
956
|
+
updateFullscreenBtn();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 全屏控制函数:修复逻辑,全屏时允许退出
|
|
960
|
+
function toggleFullscreen() {
|
|
961
|
+
// 进入全屏
|
|
962
|
+
if (!isFullscreen()) {
|
|
963
|
+
// 进入全屏前关闭所有弹窗
|
|
964
|
+
if (isModalActive) {
|
|
965
|
+
if (errorModal.style.display === 'flex') closeModal(errorModal);
|
|
966
|
+
if (progressModal.style.display === 'flex') closeModal(progressModal);
|
|
967
|
+
if (exitModal.style.display === 'flex') closeModal(exitModal);
|
|
968
|
+
}
|
|
969
|
+
// 取消未弹出的错误弹窗
|
|
970
|
+
if (errorModalTimer) clearTimeout(errorModalTimer);
|
|
971
|
+
|
|
972
|
+
if (videoContainer.requestFullscreen) {
|
|
973
|
+
videoContainer.requestFullscreen();
|
|
974
|
+
} else if (videoContainer.webkitRequestFullscreen) {
|
|
975
|
+
videoContainer.webkitRequestFullscreen();
|
|
976
|
+
} else if (videoContainer.mozRequestFullScreen) {
|
|
977
|
+
videoContainer.mozRequestFullScreen();
|
|
978
|
+
} else if (videoContainer.msRequestFullscreen) {
|
|
979
|
+
videoContainer.msRequestFullscreen();
|
|
980
|
+
}
|
|
981
|
+
} else {
|
|
982
|
+
// 退出全屏
|
|
983
|
+
if (document.exitFullscreen) {
|
|
984
|
+
document.exitFullscreen();
|
|
985
|
+
} else if (document.webkitExitFullscreen) {
|
|
986
|
+
document.webkitExitFullscreen();
|
|
987
|
+
} else if (document.mozCancelFullScreen) {
|
|
988
|
+
document.mozCancelFullScreen();
|
|
989
|
+
} else if (document.msExitFullscreen) {
|
|
990
|
+
document.msExitFullscreen();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// ========== 4. 投屏历史渲染(修复按键重复触发) ==========
|
|
996
|
+
function renderHistoryList() {
|
|
997
|
+
let history = JSON.parse(localStorage.getItem('castHistory') || '[]');
|
|
998
|
+
const historyList = document.getElementById('historyList');
|
|
999
|
+
if (history.length === 0) {
|
|
1000
|
+
historyList.innerHTML = '<div class="empty-history">暂无投屏历史</div>';
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
historyList.innerHTML = '';
|
|
1005
|
+
history.forEach(url => {
|
|
1006
|
+
const item = document.createElement('div');
|
|
1007
|
+
item.className = 'history-item';
|
|
1008
|
+
item.tabIndex = 0;
|
|
1009
|
+
// 可选:缩短URL显示(避免超出容器)
|
|
1010
|
+
const shortUrl = url.length > 30 ? url.slice(0, 30) + '...' : url;
|
|
1011
|
+
item.innerHTML = `<div class="history-url">${shortUrl}</div>`;
|
|
1012
|
+
item.addEventListener('click', function() {
|
|
1013
|
+
parseUrlToInput(url);
|
|
1014
|
+
activateIpInput(false);
|
|
1015
|
+
focusFullscreenBtn();
|
|
1016
|
+
getVideoDataAndPlay(url);
|
|
1017
|
+
});
|
|
1018
|
+
item.addEventListener('keydown', function(e) {
|
|
1019
|
+
if ((e.key === 'Enter') && !isModalActive) {
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
this.click();
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
historyList.appendChild(item);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// 若不足4条,补充空占位(保持布局居中)
|
|
1028
|
+
const emptyCount = 4 - history.length;
|
|
1029
|
+
for (let i = 0; i < emptyCount; i++) {
|
|
1030
|
+
const emptyItem = document.createElement('div');
|
|
1031
|
+
emptyItem.style.width = '400px'; // 和history-item宽度一致
|
|
1032
|
+
emptyItem.style.height = '70px'; // 和history-item高度一致
|
|
1033
|
+
historyList.appendChild(emptyItem);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// ========== 5. 双击Back/ESC退出逻辑(修复弹窗冲突) ==========
|
|
1038
|
+
function handleBackPress() {
|
|
1039
|
+
if (isModalActive) return;
|
|
1040
|
+
|
|
1041
|
+
backPressCount++;
|
|
1042
|
+
|
|
1043
|
+
if (backPressCount === 1) {
|
|
1044
|
+
backPressTimer = setTimeout(() => {
|
|
1045
|
+
backPressCount = 0;
|
|
1046
|
+
}, 3000);
|
|
1047
|
+
} else if (backPressCount === 2) {
|
|
1048
|
+
clearTimeout(backPressTimer);
|
|
1049
|
+
backPressCount = 0;
|
|
1050
|
+
showExitModal();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// ========== 6. IP输入框激活/禁用逻辑(无冲突) ==========
|
|
1055
|
+
function activateIpInput(active) {
|
|
1056
|
+
isIpInputActive = active;
|
|
1057
|
+
|
|
1058
|
+
// 协议框激活/禁用
|
|
1059
|
+
protocolLabels.forEach(label => {
|
|
1060
|
+
if (active) {
|
|
1061
|
+
label.classList.add('active');
|
|
1062
|
+
label.tabIndex = 0;
|
|
1063
|
+
} else {
|
|
1064
|
+
label.classList.remove('active');
|
|
1065
|
+
label.tabIndex = -1;
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// IP输入框激活/禁用
|
|
1070
|
+
inputGroup.forEach(input => {
|
|
1071
|
+
if (active) {
|
|
1072
|
+
input.classList.add('active');
|
|
1073
|
+
input.tabIndex = 0;
|
|
1074
|
+
} else {
|
|
1075
|
+
input.classList.remove('active');
|
|
1076
|
+
input.tabIndex = -1;
|
|
1077
|
+
input.classList.remove('editable');
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// 更新焦点列表
|
|
1082
|
+
updateFocusableElements();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// IP地址激活按钮事件(修复按键冒泡)
|
|
1086
|
+
ipActivateBtn.addEventListener('click', function() {
|
|
1087
|
+
if (!isIpInputActive) {
|
|
1088
|
+
activateIpInput(true);
|
|
1089
|
+
setFocus(focusableElements.indexOf(protocolLabels[0]));
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
ipActivateBtn.addEventListener('keydown', function(e) {
|
|
1094
|
+
if ((e.key === 'Enter') && !isModalActive) {
|
|
1095
|
+
e.preventDefault();
|
|
1096
|
+
e.stopPropagation();
|
|
1097
|
+
if (!isIpInputActive) {
|
|
1098
|
+
activateIpInput(true);
|
|
1099
|
+
setFocus(focusableElements.indexOf(protocolLabels[0]));
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
// ========== 7. IP输入框自动切换(无冲突) ==========
|
|
1105
|
+
function initInputEvents() {
|
|
1106
|
+
inputGroup.forEach((input, index) => {
|
|
1107
|
+
// 1. 读取当前输入框的maxlength(从HTML属性获取,无需硬编码)
|
|
1108
|
+
const maxLen = parseInt(input.maxLength) || 3;
|
|
1109
|
+
|
|
1110
|
+
// 2. 禁用粘贴,杜绝批量字符溢出
|
|
1111
|
+
input.addEventListener('paste', e => e.preventDefault());
|
|
1112
|
+
|
|
1113
|
+
// 3. 聚焦事件:选中内容,避免光标错位
|
|
1114
|
+
input.addEventListener('focus', function() {
|
|
1115
|
+
isInputEditable = false;
|
|
1116
|
+
input.classList.remove('editable');
|
|
1117
|
+
this.select(); // 选中当前内容,防止光标位置导致的溢出
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// 4. 键盘事件处理(核心:拦截无效按键+Enter逻辑)
|
|
1121
|
+
input.addEventListener('keydown', function(e) {
|
|
1122
|
+
if (!isIpInputActive || isModalActive) return;
|
|
1123
|
+
|
|
1124
|
+
// 仅允许数字、退格、删除、方向键、Tab,其他按键直接阻止
|
|
1125
|
+
const allowedKeys = ['0','1','2','3','4','5','6','7','8','9','Backspace','Delete','Back','ArrowLeft','ArrowRight','Tab'];
|
|
1126
|
+
if (!allowedKeys.includes(e.key) && !e.ctrlKey && !e.metaKey) {
|
|
1127
|
+
e.preventDefault();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Enter键逻辑(保留原有交互,聚焦前清空下一个框)
|
|
1132
|
+
if (e.key === 'Enter') {
|
|
1133
|
+
e.preventDefault();
|
|
1134
|
+
e.stopPropagation();
|
|
1135
|
+
if (!isInputEditable) {
|
|
1136
|
+
isInputEditable = true;
|
|
1137
|
+
input.classList.add('editable');
|
|
1138
|
+
input.focus();
|
|
1139
|
+
input.select();
|
|
1140
|
+
} else {
|
|
1141
|
+
if (index < inputGroup.length - 1) {
|
|
1142
|
+
isInputEditable = false;
|
|
1143
|
+
input.classList.remove('editable');
|
|
1144
|
+
// inputGroup[index + 1].value = ''; // 清空下一个框
|
|
1145
|
+
inputGroup[index + 1].focus();
|
|
1146
|
+
} else {
|
|
1147
|
+
isInputEditable = false;
|
|
1148
|
+
input.classList.remove('editable');
|
|
1149
|
+
setFocus(focusableElements.indexOf(goBtn));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// 5. Input事件:修复溢出+适配不同maxlength(核心修复)
|
|
1156
|
+
input.addEventListener('input', function() {
|
|
1157
|
+
// 过滤非数字字符 + 严格截断到当前框的maxlength
|
|
1158
|
+
this.value = this.value.replace(/\D/g, '').substring(0, maxLen);
|
|
1159
|
+
|
|
1160
|
+
// 达到maxlength且不是最后一个框时,延迟跳转(避开浏览器溢出时机)
|
|
1161
|
+
if (this.value.length === maxLen && index < inputGroup.length - 1) {
|
|
1162
|
+
isInputEditable = false;
|
|
1163
|
+
this.classList.remove('editable');
|
|
1164
|
+
|
|
1165
|
+
setTimeout(() => {
|
|
1166
|
+
// inputGroup[index + 1].value = ''; // 强制清空下一个框
|
|
1167
|
+
inputGroup[index + 1].focus();
|
|
1168
|
+
inputGroup[index + 1].select();
|
|
1169
|
+
}, 50);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// 6. 兜底:keypress事件拦截溢出(彻底杜绝字符推送)
|
|
1174
|
+
input.addEventListener('keypress', function(e) {
|
|
1175
|
+
// 只允许数字键(ASCII 48-57)
|
|
1176
|
+
if (e.which < 48 || e.which > 57) {
|
|
1177
|
+
e.preventDefault();
|
|
1178
|
+
}
|
|
1179
|
+
// 当前框已达maxlength,直接阻止输入
|
|
1180
|
+
if (this.value.length >= maxLen) {
|
|
1181
|
+
e.preventDefault();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
// ========== 8. 焦点管理(统一逻辑,修复冲突) ==========
|
|
1189
|
+
function updateFocusableElements() {
|
|
1190
|
+
if (isModalActive) return;
|
|
1191
|
+
|
|
1192
|
+
focusableElements = Array.from(document.querySelectorAll(
|
|
1193
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
1194
|
+
)).filter(el => {
|
|
1195
|
+
const rect = el.getBoundingClientRect();
|
|
1196
|
+
return rect.width > 0 && rect.height > 0 && el.style.display !== 'none';
|
|
1197
|
+
});
|
|
1198
|
+
currentFocusIndex = Math.max(0, Math.min(currentFocusIndex, focusableElements.length - 1));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function setFocus(index) {
|
|
1202
|
+
if (isModalActive) return;
|
|
1203
|
+
|
|
1204
|
+
updateFocusableElements();
|
|
1205
|
+
if (focusableElements.length === 0) return;
|
|
1206
|
+
|
|
1207
|
+
currentFocusIndex = (index % focusableElements.length + focusableElements.length) % focusableElements.length;
|
|
1208
|
+
focusableElements[currentFocusIndex].focus({ preventScroll: true });
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function focusFullscreenBtn() {
|
|
1212
|
+
if (isModalActive) return;
|
|
1213
|
+
|
|
1214
|
+
updateFocusableElements();
|
|
1215
|
+
if (focusableElements.length === 0) return;
|
|
1216
|
+
|
|
1217
|
+
const idx = focusableElements.indexOf(customFullscreenBtn);
|
|
1218
|
+
if (idx !== -1) {
|
|
1219
|
+
setFocus(idx);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// ========== 9. 重置状态(无冲突) ==========
|
|
1224
|
+
function resetToInitialState() {
|
|
1225
|
+
if (hls) {
|
|
1226
|
+
hls.destroy();
|
|
1227
|
+
hls = null;
|
|
1228
|
+
}
|
|
1229
|
+
videoPlayer.pause();
|
|
1230
|
+
videoPlayer.src = '';
|
|
1231
|
+
// 1. 强制显示视频容器(确保全屏按钮能渲染)
|
|
1232
|
+
videoContainer.style.display = 'block';
|
|
1233
|
+
// 2. 强制显示播放器占位符(修复文字消失)
|
|
1234
|
+
const playerPlaceholder = document.getElementById('player-placeholder');
|
|
1235
|
+
playerPlaceholder.style.display = 'flex';
|
|
1236
|
+
playerPlaceholder.innerHTML = `<div class="player-icon">📺</div><div class="player-tip">输入IP后按Enter投屏</div>`;
|
|
1237
|
+
// 3. 强制显示全屏按钮(恢复初始状态)
|
|
1238
|
+
customFullscreenBtn.style.display = 'flex';
|
|
1239
|
+
customFullscreenBtn.textContent = '⛶';
|
|
1240
|
+
|
|
1241
|
+
currentEpisodeTitle = '未知剧集';
|
|
1242
|
+
currentPlayTitle.textContent = `正在播放:${currentEpisodeTitle}`;
|
|
1243
|
+
currentPlaySection.style.display = 'none';
|
|
1244
|
+
|
|
1245
|
+
targetServerUrl = '';
|
|
1246
|
+
windowHeaders = {};
|
|
1247
|
+
|
|
1248
|
+
updateFocusableElements();
|
|
1249
|
+
setFocus(focusableElements.indexOf(ipActivateBtn));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
// ========== 10. 视频控制(无冲突) ==========
|
|
1254
|
+
function handleVideoControl(key) {
|
|
1255
|
+
if (isModalActive) return false;
|
|
1256
|
+
|
|
1257
|
+
if (!isFullscreen() || !videoPlayer) return false;
|
|
1258
|
+
|
|
1259
|
+
switch(key) {
|
|
1260
|
+
case 'ArrowLeft':
|
|
1261
|
+
const backTime = Math.max(0, videoPlayer.currentTime - SKIP_SECONDS);
|
|
1262
|
+
videoPlayer.currentTime = backTime;
|
|
1263
|
+
return true;
|
|
1264
|
+
case 'ArrowRight':
|
|
1265
|
+
const forwardTime = Math.min(videoPlayer.duration || 0, videoPlayer.currentTime + SKIP_SECONDS);
|
|
1266
|
+
videoPlayer.currentTime = forwardTime;
|
|
1267
|
+
return true;
|
|
1268
|
+
case 'ArrowUp':
|
|
1269
|
+
videoPlayer.volume = Math.min(1, videoPlayer.volume + VOLUME_STEP);
|
|
1270
|
+
return true;
|
|
1271
|
+
case 'ArrowDown':
|
|
1272
|
+
videoPlayer.volume = Math.max(0, videoPlayer.volume - VOLUME_STEP);
|
|
1273
|
+
return true;
|
|
1274
|
+
case 'Enter':
|
|
1275
|
+
videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause();
|
|
1276
|
+
return true;
|
|
1277
|
+
default:
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// ========== 11. 输入框焦点切换(修复冒泡) ==========
|
|
1283
|
+
function isInInputGroup(el) {
|
|
1284
|
+
return inputGroup.includes(el);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function handleInputGroupKey(key) {
|
|
1288
|
+
if (isModalActive) return false;
|
|
1289
|
+
|
|
1290
|
+
const activeEl = document.activeElement;
|
|
1291
|
+
const inputIndex = inputGroup.indexOf(activeEl);
|
|
1292
|
+
if (key === 'ArrowRight' || key === 'Down') {
|
|
1293
|
+
inputIndex < inputGroup.length - 1
|
|
1294
|
+
? inputGroup[inputIndex + 1].focus()
|
|
1295
|
+
: setFocus(focusableElements.indexOf(goBtn));
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
if (key === 'ArrowLeft' || key === 'ArrowUp') {
|
|
1299
|
+
inputIndex > 0
|
|
1300
|
+
? inputGroup[inputIndex - 1].focus()
|
|
1301
|
+
: setFocus(focusableElements.indexOf(document.querySelectorAll('.protocol-label')[1]));
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function handleDirectionKey(key) {
|
|
1308
|
+
if (isModalActive) return;
|
|
1309
|
+
|
|
1310
|
+
if (isFullscreen() && (key === 'Enter')) {
|
|
1311
|
+
handleVideoControl(key);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (handleVideoControl(key)) return;
|
|
1316
|
+
|
|
1317
|
+
if (isInInputGroup(document.activeElement) && handleInputGroupKey(key)) return;
|
|
1318
|
+
|
|
1319
|
+
switch(key) {
|
|
1320
|
+
case 'ArrowUp': case 'ArrowLeft':
|
|
1321
|
+
setFocus(currentFocusIndex - 1);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'ArrowDown': case 'ArrowRight':
|
|
1324
|
+
setFocus(currentFocusIndex + 1);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ========== 12. ESC/Back键处理(统一逻辑,修复弹窗冲突) ==========
|
|
1330
|
+
function handleEscKey() {
|
|
1331
|
+
// 优先关闭弹窗
|
|
1332
|
+
if (isModalActive) {
|
|
1333
|
+
if (errorModal.style.display === 'flex') {
|
|
1334
|
+
closeModal(errorModal);
|
|
1335
|
+
} else if (progressModal.style.display === 'flex') {
|
|
1336
|
+
closeModal(progressModal);
|
|
1337
|
+
} else if (exitModal.style.display === 'flex') {
|
|
1338
|
+
closeModal(exitModal);
|
|
1339
|
+
backPressCount = 0;
|
|
1340
|
+
}
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// 退出全屏
|
|
1345
|
+
if (document.fullscreenElement) {
|
|
1346
|
+
toggleFullscreen(); // 调用统一的全屏切换
|
|
1347
|
+
} else {
|
|
1348
|
+
handleBackPress();
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// ========== 13. URL解析(无冲突) ==========
|
|
1353
|
+
function parseUrlToInput(url) {
|
|
1354
|
+
try {
|
|
1355
|
+
const urlObj = new URL(url);
|
|
1356
|
+
document.querySelector(`input[name="protocol"][value="${urlObj.protocol.slice(0, -1)}"]`).checked = true;
|
|
1357
|
+
const [ip, port] = urlObj.host.split(':');
|
|
1358
|
+
ip.split('.').forEach((seg, i) => ipInputs[i].value = seg);
|
|
1359
|
+
portInput.value = port;
|
|
1360
|
+
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
console.error('URL解析失败:', e);
|
|
1363
|
+
triggerErrorModal('URL解析失败,请检查格式是否正确');
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// ========== 14. 核心:获取视频并播放(替换showErrorModal为triggerErrorModal) ==========
|
|
1368
|
+
function getVideoDataAndPlay(serverUrl) {
|
|
1369
|
+
document.getElementById('player-placeholder').innerHTML = `<div class="player-icon">⏳</div><div class="player-tip">正在获取视频数据...</div>`;
|
|
1370
|
+
targetServerUrl = serverUrl;
|
|
1371
|
+
|
|
1372
|
+
$.get(`${serverUrl}/playUrl?enhance=true`, function(result) {
|
|
1373
|
+
if (!result) {
|
|
1374
|
+
triggerErrorModal('未获取到视频数据,请检查服务器是否正常');
|
|
1375
|
+
resetToInitialState();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
try {
|
|
1380
|
+
result = JSON.parse(result);
|
|
1381
|
+
const videoUrl = result.url;
|
|
1382
|
+
currentVideoUrl = videoUrl;
|
|
1383
|
+
windowHeaders = result.headers || {};
|
|
1384
|
+
|
|
1385
|
+
if (!videoUrl) throw new Error('视频地址为空');
|
|
1386
|
+
|
|
1387
|
+
if (result.title) {
|
|
1388
|
+
currentEpisodeTitle = result.title;
|
|
1389
|
+
} else {
|
|
1390
|
+
currentEpisodeTitle = `剧集${new Date().getTime()}`;
|
|
1391
|
+
}
|
|
1392
|
+
currentPlayTitle.textContent = `正在播放:${currentEpisodeTitle}`;
|
|
1393
|
+
|
|
1394
|
+
const playCallback = () => {
|
|
1395
|
+
if (isTizenEnv) {
|
|
1396
|
+
playVideoForTizen(videoUrl, result.subtitle);
|
|
1397
|
+
} else {
|
|
1398
|
+
playVideoForPC(videoUrl, result.subtitle);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
currentPlaySection.style.display = 'block';
|
|
1402
|
+
videoContainer.style.display = 'block';
|
|
1403
|
+
document.getElementById('player-placeholder').style.display = 'none';
|
|
1404
|
+
|
|
1405
|
+
saveHistory(serverUrl);
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
checkPlayProgress(videoUrl, playCallback);
|
|
1409
|
+
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
console.error('播放失败:', error);
|
|
1412
|
+
triggerErrorModal(`播放失败:${error.message}`);
|
|
1413
|
+
resetToInitialState();
|
|
1414
|
+
}
|
|
1415
|
+
}).fail(function(err) {
|
|
1416
|
+
console.error('投屏失败:', err);
|
|
1417
|
+
triggerErrorModal(`投屏失败:无法连接到 ${serverUrl},请检查IP地址和端口是否正确`);
|
|
1418
|
+
resetToInitialState();
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// ========== 15. 检查播放进度(无冲突) ==========
|
|
1423
|
+
function checkPlayProgress(videoUrl, callbackParam) {
|
|
1424
|
+
callback = callbackParam;
|
|
1425
|
+
|
|
1426
|
+
const lastUrl = localStorage.getItem('last_url');
|
|
1427
|
+
lastPos = localStorage.getItem('last_pos');
|
|
1428
|
+
|
|
1429
|
+
if (lastUrl === videoUrl && lastPos && lastPos > 0) {
|
|
1430
|
+
progressModal.style.display = 'flex';
|
|
1431
|
+
isProgressModalShow = true;
|
|
1432
|
+
setupModalFocus();
|
|
1433
|
+
} else {
|
|
1434
|
+
callback();
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// ========== 16. 播放逻辑(保留原有,未修改) ==========
|
|
1439
|
+
function playVideoForPC(videoUrl, subtitle) {
|
|
1440
|
+
videoPlayer.src = '';
|
|
1441
|
+
videoPlayer.preload = 'auto';
|
|
1442
|
+
videoPlayer.autoplay = true;
|
|
1443
|
+
|
|
1444
|
+
if (videoUrl.includes('.m3u8') && window.Hls && Hls.isSupported()) {
|
|
1445
|
+
const pcHlsConfig = {
|
|
1446
|
+
headers: windowHeaders,
|
|
1447
|
+
maxBufferLength: 15,
|
|
1448
|
+
maxBufferHole: 0.5,
|
|
1449
|
+
segmentLoadingTimeOut: 15000,
|
|
1450
|
+
segmentLoadingMaxRetry: 5,
|
|
1451
|
+
enableWorker: true,
|
|
1452
|
+
enableSoftwareAES: true,
|
|
1453
|
+
audioCodec: 'mp4a.40.2,mp3,ac-3,ec-3',
|
|
1454
|
+
videoCodec: 'avc1.42E01E,avc1.640028,hev1.1.6.L93.90'
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
if (hls) hls.destroy();
|
|
1458
|
+
hls = new Hls(pcHlsConfig);
|
|
1459
|
+
hls.loadSource(videoUrl);
|
|
1460
|
+
hls.attachMedia(videoPlayer);
|
|
1461
|
+
|
|
1462
|
+
hls.on(Hls.Events.MANIFEST_PARSED, function() {
|
|
1463
|
+
videoPlayer.play().catch(err => {
|
|
1464
|
+
console.error('PC端HLS播放失败:', err);
|
|
1465
|
+
try {
|
|
1466
|
+
videoPlayer.src = videoUrl;
|
|
1467
|
+
videoPlayer.play();
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
triggerErrorModal('视频播放失败,请检查流地址是否有效');
|
|
1470
|
+
resetToInitialState();
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
hls.on(Hls.Events.ERROR, function(event, data) {
|
|
1476
|
+
console.error('PC端HLS错误:', data);
|
|
1477
|
+
let errorMsg = '播放错误';
|
|
1478
|
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
|
1479
|
+
errorMsg = '网络加载失败,请检查网络连接';
|
|
1480
|
+
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1481
|
+
errorMsg = '视频解码失败,请检查流格式';
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (data.fatal) {
|
|
1485
|
+
switch(data.type) {
|
|
1486
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
1487
|
+
hls.startLoad();
|
|
1488
|
+
break;
|
|
1489
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
1490
|
+
hls.recoverMediaError();
|
|
1491
|
+
break;
|
|
1492
|
+
default:
|
|
1493
|
+
triggerErrorModal(errorMsg);
|
|
1494
|
+
resetToInitialState();
|
|
1495
|
+
break;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
} else {
|
|
1500
|
+
try {
|
|
1501
|
+
videoPlayer.src = videoUrl;
|
|
1502
|
+
videoPlayer.play().catch(err => {
|
|
1503
|
+
console.error('PC端视频播放失败:', err);
|
|
1504
|
+
triggerErrorModal('视频播放失败,请检查文件格式');
|
|
1505
|
+
resetToInitialState();
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
if (subtitle && subtitle.url) {
|
|
1509
|
+
const track = document.createElement('track');
|
|
1510
|
+
track.kind = 'subtitles';
|
|
1511
|
+
track.src = subtitle.url;
|
|
1512
|
+
track.label = subtitle.label || '中文';
|
|
1513
|
+
track.srclang = subtitle.lang || 'zh-CN';
|
|
1514
|
+
track.default = true;
|
|
1515
|
+
videoPlayer.appendChild(track);
|
|
1516
|
+
}
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
console.error('PC端播放异常:', error);
|
|
1519
|
+
triggerErrorModal(`视频播放错误:${error.message}`);
|
|
1520
|
+
resetToInitialState();
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
videoPlayer.addEventListener('timeupdate', function() {
|
|
1525
|
+
if (!videoPlayer.paused && videoPlayer.duration) {
|
|
1526
|
+
localStorage.setItem('last_url', currentVideoUrl);
|
|
1527
|
+
localStorage.setItem('last_pos', videoPlayer.currentTime);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
videoPlayer.addEventListener('ended', function() {
|
|
1532
|
+
localStorage.removeItem('last_url');
|
|
1533
|
+
localStorage.removeItem('last_pos');
|
|
1534
|
+
resetToInitialState();
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function playVideoForTizen(videoUrl, subtitle) {
|
|
1539
|
+
videoPlayer.src = '';
|
|
1540
|
+
videoPlayer.preload = 'none';
|
|
1541
|
+
videoPlayer.autoplay = false;
|
|
1542
|
+
|
|
1543
|
+
const tizenHlsConfig = {
|
|
1544
|
+
headers: windowHeaders,
|
|
1545
|
+
maxBufferLength: 30,
|
|
1546
|
+
maxMaxBufferLength: 60,
|
|
1547
|
+
startLevel: 0,
|
|
1548
|
+
maxBufferSize: 60 * 1024 * 1024,
|
|
1549
|
+
maxBufferHole: 0.8,
|
|
1550
|
+
manifestLoadingTimeOut: 15000,
|
|
1551
|
+
manifestLoadingMaxRetry: 5,
|
|
1552
|
+
segmentLoadingTimeOut: 20000,
|
|
1553
|
+
segmentLoadingMaxRetry: 8,
|
|
1554
|
+
lowLatencyMode: false,
|
|
1555
|
+
backBufferLength: 90,
|
|
1556
|
+
enableWorker: true,
|
|
1557
|
+
enableSoftwareAES: true,
|
|
1558
|
+
audioCodec: 'mp4a.40.2,mp3,ac-3,ec-3',
|
|
1559
|
+
videoCodec: 'avc1.42E01E,avc1.640028,hev1.1.6.L93.90'
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
if (videoUrl.includes('.m3u8')) {
|
|
1563
|
+
if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
|
|
1564
|
+
try {
|
|
1565
|
+
videoPlayer.src = videoUrl;
|
|
1566
|
+
videoPlayer.crossOrigin = 'anonymous';
|
|
1567
|
+
|
|
1568
|
+
videoPlayer.addEventListener('loadedmetadata', function() {
|
|
1569
|
+
videoPlayer.load();
|
|
1570
|
+
videoPlayer.addEventListener('canplaythrough', function() {
|
|
1571
|
+
videoPlayer.play().catch(err => {
|
|
1572
|
+
console.error('Tizen原生播放失败,降级HLS.js:', err);
|
|
1573
|
+
loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
|
|
1574
|
+
});
|
|
1575
|
+
}, { once: true });
|
|
1576
|
+
}, { once: true });
|
|
1577
|
+
|
|
1578
|
+
videoPlayer.addEventListener('error', function() {
|
|
1579
|
+
console.error('Tizen原生HLS错误,降级HLS.js');
|
|
1580
|
+
loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
|
|
1581
|
+
}, { once: true });
|
|
1582
|
+
} catch (error) {
|
|
1583
|
+
console.error('Tizen原生初始化失败:', error);
|
|
1584
|
+
loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
|
|
1585
|
+
}
|
|
1586
|
+
} else if (window.Hls && Hls.isSupported()) {
|
|
1587
|
+
loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
|
|
1588
|
+
} else {
|
|
1589
|
+
throw new Error('Tizen电视不支持HLS播放');
|
|
1590
|
+
}
|
|
1591
|
+
} else {
|
|
1592
|
+
try {
|
|
1593
|
+
videoPlayer.src = videoUrl;
|
|
1594
|
+
videoPlayer.crossOrigin = 'anonymous';
|
|
1595
|
+
|
|
1596
|
+
videoPlayer.addEventListener('canplaythrough', function() {
|
|
1597
|
+
videoPlayer.play();
|
|
1598
|
+
}, { once: true });
|
|
1599
|
+
|
|
1600
|
+
if (subtitle && subtitle.url) {
|
|
1601
|
+
const track = document.createElement('track');
|
|
1602
|
+
track.kind = 'subtitles';
|
|
1603
|
+
track.src = subtitle.url;
|
|
1604
|
+
track.label = subtitle.label || '中文';
|
|
1605
|
+
track.srclang = subtitle.lang || 'zh-CN';
|
|
1606
|
+
track.default = true;
|
|
1607
|
+
videoPlayer.appendChild(track);
|
|
1608
|
+
}
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
console.error('Tizen普通视频播放错误:', error);
|
|
1611
|
+
triggerErrorModal(`视频播放错误:${error.message}`);
|
|
1612
|
+
resetToInitialState();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
videoPlayer.addEventListener('timeupdate', function() {
|
|
1617
|
+
if (!videoPlayer.paused && videoPlayer.duration) {
|
|
1618
|
+
localStorage.setItem('last_url', currentVideoUrl);
|
|
1619
|
+
localStorage.setItem('last_pos', videoPlayer.currentTime);
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
videoPlayer.addEventListener('ended', function() {
|
|
1624
|
+
localStorage.removeItem('last_url');
|
|
1625
|
+
localStorage.removeItem('last_pos');
|
|
1626
|
+
resetToInitialState();
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
videoPlayer.addEventListener('stalled', function() {
|
|
1630
|
+
console.warn('Tizen缓冲中断,尝试恢复');
|
|
1631
|
+
videoPlayer.pause();
|
|
1632
|
+
|
|
1633
|
+
videoPlayer.addEventListener('canplay', function() {
|
|
1634
|
+
videoPlayer.play();
|
|
1635
|
+
}, { once: true });
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function loadWithHlsJs(videoUrl, config, subtitle) {
|
|
1640
|
+
if (hls) {
|
|
1641
|
+
hls.destroy();
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
hls = new Hls(config);
|
|
1645
|
+
hls.loadSource(videoUrl);
|
|
1646
|
+
hls.attachMedia(videoPlayer);
|
|
1647
|
+
|
|
1648
|
+
hls.on(Hls.Events.MANIFEST_PARSED, function() {
|
|
1649
|
+
videoPlayer.load();
|
|
1650
|
+
videoPlayer.addEventListener('canplaythrough', function() {
|
|
1651
|
+
videoPlayer.play().catch(err => {
|
|
1652
|
+
console.error('HLS.js播放失败:', err);
|
|
1653
|
+
triggerErrorModal('视频播放失败,请检查流格式');
|
|
1654
|
+
resetToInitialState();
|
|
1655
|
+
});
|
|
1656
|
+
}, { once: true });
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
hls.on(Hls.Events.ERROR, function(event, data) {
|
|
1660
|
+
console.error('HLS播放错误:', data);
|
|
1661
|
+
let errorMsg = '播放错误';
|
|
1662
|
+
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
|
1663
|
+
errorMsg = '网络加载失败,请检查网络连接';
|
|
1664
|
+
} else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
|
1665
|
+
errorMsg = '视频解码失败,流格式不兼容';
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
if (data.fatal) {
|
|
1669
|
+
switch(data.type) {
|
|
1670
|
+
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
1671
|
+
hls.startLoad();
|
|
1672
|
+
return;
|
|
1673
|
+
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
1674
|
+
hls.recoverMediaError();
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
triggerErrorModal(errorMsg);
|
|
1680
|
+
resetToInitialState();
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
if (subtitle && subtitle.url) {
|
|
1684
|
+
const track = document.createElement('track');
|
|
1685
|
+
track.kind = 'subtitles';
|
|
1686
|
+
track.src = subtitle.url;
|
|
1687
|
+
track.label = subtitle.label || '中文';
|
|
1688
|
+
track.srclang = subtitle.lang || 'zh-CN';
|
|
1689
|
+
track.default = true;
|
|
1690
|
+
videoPlayer.appendChild(track);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// ========== 17. 历史记录管理(无冲突) ==========
|
|
1695
|
+
function saveHistory(serverUrl) {
|
|
1696
|
+
let history = JSON.parse(localStorage.getItem('castHistory') || '[]');
|
|
1697
|
+
// 先移除重复的URL(避免同一URL多次存储)
|
|
1698
|
+
history = history.filter(item => item !== serverUrl);
|
|
1699
|
+
// 添加新URL到开头
|
|
1700
|
+
history.unshift(serverUrl);
|
|
1701
|
+
// 最多保留4条,超出则删除最后一条(最早的记录)
|
|
1702
|
+
if (history.length > 4) {
|
|
1703
|
+
history = history.slice(0, 4); // 只保留前4条
|
|
1704
|
+
}
|
|
1705
|
+
localStorage.setItem('castHistory', JSON.stringify(history));
|
|
1706
|
+
renderHistoryList();
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// ========== 18. 清空历史记录(修复按键冒泡) ==========
|
|
1710
|
+
document.getElementById('clearHistory').addEventListener('click', function() {
|
|
1711
|
+
localStorage.removeItem('castHistory');
|
|
1712
|
+
renderHistoryList();
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
document.getElementById('clearHistory').addEventListener('keydown', function(e) {
|
|
1716
|
+
if ((e.key === 'Enter') && !isModalActive) {
|
|
1717
|
+
e.preventDefault();
|
|
1718
|
+
e.stopPropagation();
|
|
1719
|
+
localStorage.removeItem('castHistory');
|
|
1720
|
+
renderHistoryList();
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
|
|
1724
|
+
// ========== 19. 投屏按钮点击事件(修复所有冲突) ==========
|
|
1725
|
+
goBtn.addEventListener('click', function() {
|
|
1726
|
+
if (isModalActive) return;
|
|
1727
|
+
activateIpInput(false);
|
|
1728
|
+
const ipSegments = ipInputs.map(input => input.value.trim());
|
|
1729
|
+
const port = portInput.value.trim() || '8080';
|
|
1730
|
+
const protocol = document.querySelector('input[name="protocol"]:checked').value;
|
|
1731
|
+
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
const ipValid = ipSegments.every(seg => /^\d{1,3}$/.test(seg) && parseInt(seg) <= 255);
|
|
1744
|
+
|
|
1745
|
+
const portValid = /^\d{1,5}$/.test(port) && parseInt(port) <= 65535;
|
|
1746
|
+
|
|
1747
|
+
if (!ipValid) {
|
|
1748
|
+
showErrorModal('IP地址格式错误,请输入有效的IPv4地址');
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
if (!portValid) {
|
|
1756
|
+
showErrorModal('端口号格式错误,请输入1-65535之间的数字');
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
const serverUrl = `${protocol}://${ipSegments.join('.')}:${port}`;
|
|
1762
|
+
|
|
1763
|
+
|
|
1764
|
+
|
|
1765
|
+
saveHistory(serverUrl);
|
|
1766
|
+
renderHistoryList();
|
|
1767
|
+
focusFullscreenBtn();
|
|
1768
|
+
getVideoDataAndPlay(serverUrl);
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
goBtn.addEventListener('keydown', function(e) {
|
|
1772
|
+
if ((e.key === 'Enter') && !isModalActive) {
|
|
1773
|
+
e.preventDefault();
|
|
1774
|
+
e.stopPropagation();
|
|
1775
|
+
this.click();
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
// ========== 18. 全局键盘事件监听 ==========
|
|
1787
|
+
document.addEventListener('keydown', function(e) {
|
|
1788
|
+
if (isModalActive) return;
|
|
1789
|
+
|
|
1790
|
+
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight','Enter', 'Escape', 'Back'].includes(e.key)) {
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
e.preventDefault();
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
switch(e.key) {
|
|
1806
|
+
case 'Enter':
|
|
1807
|
+
if (isFullscreen()) {
|
|
1808
|
+
videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause();
|
|
1809
|
+
} else {
|
|
1810
|
+
handleEnterKey();
|
|
1811
|
+
|
|
1812
|
+
|
|
1813
|
+
}
|
|
1814
|
+
break;
|
|
1815
|
+
case 'Escape':
|
|
1816
|
+
case 'Back':
|
|
1817
|
+
handleEscKey();
|
|
1818
|
+
break;
|
|
1819
|
+
case 'ArrowUp':
|
|
1820
|
+
case 'ArrowDown':
|
|
1821
|
+
case 'ArrowLeft':
|
|
1822
|
+
case 'ArrowRight':
|
|
1823
|
+
handleDirectionKey(e.key);
|
|
1824
|
+
break;
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
// ========== 初始化 ==========
|
|
1831
|
+
function init() {
|
|
1832
|
+
initInputEvents();
|
|
1833
|
+
renderHistoryList(); // 先渲染历史记录
|
|
1834
|
+
initFullscreenButton();
|
|
1835
|
+
updateFocusableElements();
|
|
1836
|
+
setFocus(0);
|
|
1837
|
+
window.addEventListener('resize', function() {
|
|
1838
|
+
updateFocusableElements();
|
|
1839
|
+
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
if (isTizenEnv) {
|
|
1843
|
+
console.log('检测到Tizen系统,启用电视适配模式');
|
|
1844
|
+
document.addEventListener('visibilitychange', function() {
|
|
1845
|
+
if (!document.hidden && videoPlayer.paused && videoPlayer.src) {
|
|
1846
|
+
videoPlayer.play();
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
init();
|
|
1853
|
+
});
|
|
1854
|
+
</script>
|
|
1855
|
+
</body>
|
|
1856
|
+
</html>
|