hu2 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.
Files changed (2) hide show
  1. package/index.html +1910 -0
  2. package/package.json +19 -0
package/index.html ADDED
@@ -0,0 +1,1910 @@
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 = defaultIndex;
668
+ const savedFocusIndex = currentFocusIndex;
669
+
670
+
671
+
672
+
673
+
674
+
675
+
676
+
677
+
678
+
679
+
680
+ // 清空之前的事件监听
681
+ modalElements.forEach(el => {
682
+ el.removeEventListener('keydown', handleModalKeydown);
683
+ });
684
+
685
+
686
+ modalElements[currentModalIndex].focus();
687
+
688
+ modalElements[currentModalIndex].scrollIntoView({ block: 'center' });
689
+
690
+
691
+
692
+
693
+ const focusGuard = function(e) {
694
+ if (isModalActive && !modalElements.includes(e.target)) {
695
+
696
+
697
+ e.preventDefault();
698
+ modalElements[currentModalIndex].focus();
699
+
700
+
701
+
702
+
703
+
704
+
705
+
706
+
707
+
708
+ }
709
+
710
+
711
+
712
+
713
+
714
+
715
+ };
716
+
717
+ document.addEventListener('focusin', focusGuard, true);
718
+
719
+
720
+ function handleModalKeydown(e) {
721
+ if (!isModalActive) return;
722
+
723
+ // 仅Enter/OK能确认,Back/ESC关闭
724
+
725
+
726
+
727
+
728
+
729
+
730
+
731
+
732
+
733
+
734
+
735
+ if (e.key === 'Enter') {
736
+ e.preventDefault();
737
+
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
+ modalElements[currentModalIndex].focus();
747
+
748
+
749
+
750
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
751
+ e.preventDefault();
752
+ currentModalIndex = (currentModalIndex - 1 + modalElements.length) % modalElements.length;
753
+ modalElements[currentModalIndex].focus();
754
+
755
+
756
+
757
+ } else if (e.key === 'Escape' || e.key === 'Back') { // Back键对应Back
758
+ e.preventDefault();
759
+
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
+
775
+ });
776
+ document.removeEventListener('focusin', focusGuard, true);
777
+
778
+ setFocus(savedFocusIndex);
779
+ };
780
+ }
781
+
782
+
783
+ function closeModal(modalDom, restoreIndex = 0) {
784
+ modalDom.style.display = 'none';
785
+ isModalActive = false;
786
+
787
+ setFocus(restoreIndex);
788
+
789
+ }
790
+
791
+
792
+ // 错误弹窗显示(仅Enter/OK关闭)
793
+
794
+
795
+
796
+
797
+
798
+
799
+
800
+
801
+
802
+
803
+
804
+
805
+
806
+
807
+
808
+
809
+
810
+
811
+
812
+
813
+
814
+
815
+
816
+
817
+
818
+ function showErrorModal(message) {
819
+ if (isModalActive) return; // 防止重复弹窗
820
+
821
+
822
+
823
+ const currentActiveElement = document.activeElement;
824
+ const savedFocusIndex = focusableElements.indexOf(currentActiveElement);
825
+
826
+ errorMessage.textContent = message;
827
+ errorModal.style.display = 'flex';
828
+
829
+ const modalElements = [okErrorBtn];
830
+ const releaseFocus = trapFocusInModal(modalElements, errorModal, 0);
831
+
832
+
833
+ // 点击事件
834
+ okErrorBtn.onclick = function() {
835
+
836
+
837
+
838
+ closeModal(errorModal, savedFocusIndex);
839
+ releaseFocus();
840
+
841
+
842
+
843
+ };
844
+
845
+
846
+
847
+
848
+
849
+
850
+
851
+
852
+
853
+
854
+
855
+
856
+
857
+
858
+
859
+
860
+ }
861
+
862
+ // 进度弹窗逻辑
863
+
864
+
865
+
866
+
867
+
868
+
869
+
870
+
871
+ let callback, lastPos;
872
+
873
+ function setupModalFocus() {
874
+ if (isModalActive) return;
875
+
876
+ const currentActiveElement = document.activeElement;
877
+ const savedFocusIndex = focusableElements.indexOf(currentActiveElement);
878
+
879
+ const modalElements = [confirmProgressBtn, cancelProgressBtn];
880
+ const releaseFocus = trapFocusInModal(modalElements, progressModal, 0);
881
+
882
+ confirmProgressBtn.onclick = function() {
883
+ closeModal(progressModal, savedFocusIndex);
884
+ releaseFocus();
885
+
886
+ videoPlayer.addEventListener('loadedmetadata', () => {
887
+ videoPlayer.currentTime = lastPos;
888
+ }, { once: true });
889
+ callback();
890
+ };
891
+
892
+ cancelProgressBtn.onclick = function() {
893
+ closeModal(progressModal, savedFocusIndex);
894
+ releaseFocus();
895
+
896
+ callback();
897
+ };
898
+ }
899
+
900
+ // 退出弹窗逻辑
901
+ function showExitModal() {
902
+ if (isModalActive) return;
903
+
904
+ exitModal.style.display = 'flex';
905
+ const modalElements = [confirmExitBtn, cancelExitBtn];
906
+ const releaseFocus = trapFocusInModal(modalElements, exitModal, 0);
907
+
908
+ confirmExitBtn.onclick = function() {
909
+ closeModal(exitModal);
910
+ releaseFocus();
911
+ if (isTizenEnv && window.tizen.application.getCurrentApplication) {
912
+ const app = window.tizen.application.getCurrentApplication();
913
+ window.tizen.application.exit(app);
914
+ } else {
915
+ window.close();
916
+ }
917
+ };
918
+
919
+ cancelExitBtn.onclick = function() {
920
+ closeModal(exitModal);
921
+ releaseFocus();
922
+ backPressCount = 0;
923
+ };
924
+ }
925
+
926
+ // ========== 2. 全屏按钮控制(全屏隐藏) ==========
927
+ function initFullscreenButton() {
928
+ // 监听全屏状态变化
929
+ function updateFullscreenBtn() {
930
+ if (isFullscreen()) {
931
+ customFullscreenBtn.style.display = 'none'; // 全屏隐藏
932
+ } else {
933
+ customFullscreenBtn.style.display = 'flex'; // 非全屏显示
934
+ customFullscreenBtn.textContent = '⛶';
935
+
936
+ }
937
+ }
938
+
939
+ // 绑定自定义全屏按钮事件
940
+ customFullscreenBtn.addEventListener('click', function() {
941
+ toggleFullscreen();
942
+ });
943
+
944
+ customFullscreenBtn.addEventListener('keydown', function(e) {
945
+ if ((e.key === 'Enter') && !isModalActive && !isFullscreen()) {
946
+ e.preventDefault();
947
+ toggleFullscreen();
948
+
949
+ }
950
+ });
951
+
952
+ // 监听全屏变化
953
+ document.addEventListener('fullscreenchange', updateFullscreenBtn);
954
+ document.addEventListener('webkitfullscreenchange', updateFullscreenBtn);
955
+ document.addEventListener('mozfullscreenchange', updateFullscreenBtn);
956
+ document.addEventListener('MSFullscreenChange', updateFullscreenBtn);
957
+
958
+ // 初始状态
959
+ updateFullscreenBtn();
960
+ }
961
+
962
+ // 全屏控制函数(仅非全屏时可操作)
963
+ function toggleFullscreen() {
964
+
965
+ if (isFullscreen()) return; // 全屏时禁止操作
966
+
967
+
968
+
969
+
970
+
971
+
972
+
973
+
974
+
975
+ if (videoContainer.requestFullscreen) {
976
+ videoContainer.requestFullscreen();
977
+ } else if (videoContainer.webkitRequestFullscreen) {
978
+ videoContainer.webkitRequestFullscreen();
979
+ } else if (videoContainer.mozRequestFullScreen) {
980
+ videoContainer.mozRequestFullScreen();
981
+ } else if (videoContainer.msRequestFullscreen) {
982
+ videoContainer.msRequestFullscreen();
983
+ }
984
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
+ }
995
+
996
+
997
+
998
+ // ========== 3. 投屏历史渲染(固定大小) ==========
999
+ function renderHistoryList() {
1000
+ let history = JSON.parse(localStorage.getItem('castHistory') || '[]');
1001
+ const historyList = document.getElementById('historyList');
1002
+ if (history.length === 0) {
1003
+ historyList.innerHTML = '<div class="empty-history">暂无投屏历史</div>';
1004
+ return;
1005
+ }
1006
+
1007
+ historyList.innerHTML = '';
1008
+ history.forEach(url => {
1009
+ const item = document.createElement('div');
1010
+ item.className = 'history-item';
1011
+ item.tabIndex = 0;
1012
+
1013
+
1014
+ item.innerHTML = `<div class="history-url">${url}</div>`;
1015
+ item.addEventListener('click', function() {
1016
+ parseUrlToInput(url);
1017
+ activateIpInput(false);
1018
+
1019
+ getVideoDataAndPlay(url);
1020
+ });
1021
+ item.addEventListener('keydown', function(e) {
1022
+ if ((e.key === 'Enter') && !isModalActive) {
1023
+ e.preventDefault();
1024
+ this.click();
1025
+ }
1026
+ });
1027
+ historyList.appendChild(item);
1028
+ });
1029
+
1030
+
1031
+
1032
+
1033
+
1034
+
1035
+
1036
+
1037
+ }
1038
+
1039
+
1040
+ // ========== 4. 双击Back/ESC退出逻辑 ==========
1041
+ function handleBackPress() {
1042
+ if (isModalActive) return;
1043
+
1044
+ backPressCount++;
1045
+
1046
+ if (backPressCount === 1) {
1047
+ backPressTimer = setTimeout(() => {
1048
+ backPressCount = 0;
1049
+ }, 3000);
1050
+ } else if (backPressCount === 2) {
1051
+ clearTimeout(backPressTimer);
1052
+ backPressCount = 0;
1053
+ showExitModal();
1054
+ }
1055
+ }
1056
+
1057
+ // ========== 5. IP输入框激活/禁用逻辑 ==========
1058
+ function activateIpInput(active) {
1059
+ isIpInputActive = active;
1060
+
1061
+ // 协议框激活/禁用
1062
+ protocolLabels.forEach(label => {
1063
+ if (active) {
1064
+ label.classList.add('active');
1065
+ label.tabIndex = 0;
1066
+ } else {
1067
+ label.classList.remove('active');
1068
+ label.tabIndex = -1;
1069
+ }
1070
+ });
1071
+
1072
+ // IP输入框激活/禁用
1073
+ inputGroup.forEach(input => {
1074
+ if (active) {
1075
+ input.classList.add('active');
1076
+ input.tabIndex = 0;
1077
+ } else {
1078
+ input.classList.remove('active');
1079
+ input.tabIndex = -1;
1080
+ input.classList.remove('editable');
1081
+ }
1082
+ });
1083
+
1084
+ // 更新焦点列表
1085
+ updateFocusableElements();
1086
+ }
1087
+
1088
+ // IP地址激活按钮事件
1089
+ ipActivateBtn.addEventListener('click', function() {
1090
+ if (!isIpInputActive) {
1091
+ activateIpInput(true);
1092
+ // 焦点移到第一个协议框
1093
+ setFocus(focusableElements.indexOf(protocolLabels[0]));
1094
+ }
1095
+ });
1096
+
1097
+ ipActivateBtn.addEventListener('keydown', function(e) {
1098
+ if ((e.key === 'Enter') && !isModalActive) {
1099
+ e.preventDefault();
1100
+
1101
+ if (!isIpInputActive) {
1102
+ activateIpInput(true);
1103
+ setFocus(focusableElements.indexOf(protocolLabels[0]));
1104
+ }
1105
+ }
1106
+ });
1107
+
1108
+ // ========== 6. IP输入框自动切换 ==========
1109
+ function initInputEvents() {
1110
+ inputGroup.forEach((input, index) => {
1111
+
1112
+
1113
+
1114
+
1115
+
1116
+
1117
+
1118
+ input.addEventListener('focus', function() {
1119
+ isInputEditable = false;
1120
+ input.classList.remove('editable');
1121
+
1122
+ });
1123
+
1124
+
1125
+ input.addEventListener('keydown', function(e) {
1126
+ if (!isIpInputActive || isModalActive) return;
1127
+
1128
+
1129
+
1130
+
1131
+
1132
+
1133
+
1134
+
1135
+
1136
+ if (e.key === 'Enter') {
1137
+ e.preventDefault();
1138
+
1139
+ if (!isInputEditable) {
1140
+ isInputEditable = true;
1141
+ input.classList.add('editable');
1142
+ input.focus();
1143
+
1144
+ } else {
1145
+ // 自动切换到下一个输入框
1146
+ if (index < inputGroup.length - 1) {
1147
+ isInputEditable = false;
1148
+ input.classList.remove('editable');
1149
+
1150
+ inputGroup[index + 1].focus();
1151
+ } else {
1152
+ isInputEditable = false;
1153
+ input.classList.remove('editable');
1154
+ setFocus(focusableElements.indexOf(goBtn));
1155
+ }
1156
+ }
1157
+ }
1158
+ });
1159
+
1160
+ // 输入完成自动切换(输入3个字符后)
1161
+ input.addEventListener('input', function() {
1162
+
1163
+
1164
+
1165
+
1166
+ if (this.value.length >= this.maxLength && index < inputGroup.length - 1) {
1167
+ isInputEditable = false;
1168
+ this.classList.remove('editable');
1169
+
1170
+
1171
+
1172
+ inputGroup[index + 1].focus();
1173
+
1174
+
1175
+ }
1176
+ });
1177
+
1178
+
1179
+
1180
+
1181
+
1182
+
1183
+
1184
+
1185
+
1186
+
1187
+
1188
+ });
1189
+
1190
+ }
1191
+
1192
+
1193
+ // ========== 7. 焦点管理(跳过禁用元素) ==========
1194
+ function updateFocusableElements() {
1195
+ if (isModalActive) return;
1196
+
1197
+ focusableElements = Array.from(document.querySelectorAll(
1198
+ '[tabindex]:not([tabindex="-1"])'
1199
+ )).filter(el => {
1200
+ const rect = el.getBoundingClientRect();
1201
+ return rect.width > 0 && rect.height > 0 && el.style.display !== 'none';
1202
+ });
1203
+ currentFocusIndex = Math.max(0, Math.min(currentFocusIndex, focusableElements.length - 1));
1204
+ }
1205
+
1206
+ function setFocus(index) {
1207
+ if (isModalActive) return;
1208
+
1209
+ updateFocusableElements();
1210
+ if (focusableElements.length === 0) return;
1211
+
1212
+ currentFocusIndex = (index % focusableElements.length + focusableElements.length) % focusableElements.length;
1213
+ focusableElements[currentFocusIndex].focus();
1214
+ }
1215
+
1216
+
1217
+ // ========== 8. 投屏按钮事件(禁用IP输入框) ==========
1218
+ goBtn.addEventListener('click', function() {
1219
+ if (isModalActive) return;
1220
+
1221
+ // 禁用IP输入框
1222
+ activateIpInput(false);
1223
+
1224
+
1225
+
1226
+ // 获取输入的IP和端口
1227
+ const protocol = document.querySelector('input[name="protocol"]:checked').value;
1228
+ const ip = ipInputs.map(input => input.value || input.placeholder).join('.');
1229
+ const port = portInput.value || portInput.placeholder;
1230
+ const url = `${protocol}://${ip}:${port}`;
1231
+
1232
+ // 验证IP格式
1233
+ const ipRegex = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/;
1234
+ if (!ipRegex.test(`${ip}:${port}`)) {
1235
+ showErrorModal('IP地址格式无效,请检查输入!');
1236
+ return;
1237
+ }
1238
+
1239
+ // 保存历史记录
1240
+ saveHistory(url);
1241
+ renderHistoryList();
1242
+
1243
+ // 播放视频
1244
+ getVideoDataAndPlay(url);
1245
+
1246
+ // 焦点移到播放控制面板
1247
+ updateFocusableElements();
1248
+ setFocus(focusableElements.indexOf(document.querySelector('.play-control-panel')));
1249
+ });
1250
+
1251
+ goBtn.addEventListener('keydown', function(e) {
1252
+ if ((e.key === 'Enter') && !isModalActive) {
1253
+ e.preventDefault();
1254
+ this.click();
1255
+ }
1256
+ });
1257
+
1258
+ // ========== 9. 重置状态 ==========
1259
+ function resetToInitialState() {
1260
+ if (hls) {
1261
+ hls.destroy();
1262
+ hls = null;
1263
+ }
1264
+ videoPlayer.pause();
1265
+ videoPlayer.src = '';
1266
+
1267
+ videoContainer.style.display = 'none';
1268
+
1269
+ document.getElementById('player-placeholder').style.display = 'flex';
1270
+
1271
+ document.getElementById('player-placeholder').innerHTML = `<div class="player-icon">📺</div><div class="player-tip">输入IP后按Enter投屏</div>`;
1272
+
1273
+
1274
+
1275
+
1276
+ currentEpisodeTitle = '未知剧集';
1277
+ currentPlayTitle.textContent = `正在播放:${currentEpisodeTitle}`;
1278
+ currentPlaySection.style.display = 'none';
1279
+ customFullscreenBtn.classList.add('hidden');
1280
+
1281
+ targetServerUrl = '';
1282
+ windowHeaders = {};
1283
+
1284
+ updateFocusableElements();
1285
+ setFocus(focusableElements.indexOf(ipActivateBtn));
1286
+ }
1287
+
1288
+
1289
+ // ========== 8. 视频控制 ==========
1290
+ function handleVideoControl(key) {
1291
+ if (isModalActive) return false;
1292
+
1293
+ if (!isFullscreen() || !videoPlayer) return false;
1294
+
1295
+ switch(key) {
1296
+ case 'ArrowLeft':
1297
+ const backTime = Math.max(0, videoPlayer.currentTime - SKIP_SECONDS);
1298
+ videoPlayer.currentTime = backTime;
1299
+ return true;
1300
+ case 'ArrowRight':
1301
+ const forwardTime = Math.min(videoPlayer.duration || 0, videoPlayer.currentTime + SKIP_SECONDS);
1302
+ videoPlayer.currentTime = forwardTime;
1303
+ return true;
1304
+ case 'ArrowUp':
1305
+ videoPlayer.volume = Math.min(1, videoPlayer.volume + VOLUME_STEP);
1306
+ return true;
1307
+ case 'ArrowDown':
1308
+ videoPlayer.volume = Math.max(0, videoPlayer.volume - VOLUME_STEP);
1309
+ return true;
1310
+ case 'Enter':
1311
+ videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause();
1312
+ return true;
1313
+ default:
1314
+ return false;
1315
+ }
1316
+ }
1317
+
1318
+ // ========== 9. 输入框焦点切换 ==========
1319
+ function isInInputGroup(el) {
1320
+ return inputGroup.includes(el);
1321
+ }
1322
+
1323
+ function handleInputGroupKey(key) {
1324
+ if (isModalActive) return false;
1325
+
1326
+ const activeEl = document.activeElement;
1327
+ const inputIndex = inputGroup.indexOf(activeEl);
1328
+ if (key === 'ArrowRight' || key === 'Down') {
1329
+ inputIndex < inputGroup.length - 1
1330
+ ? inputGroup[inputIndex + 1].focus()
1331
+ : setFocus(focusableElements.indexOf(goBtn));
1332
+ return true;
1333
+ }
1334
+ if (key === 'ArrowLeft' || key === 'Up') {
1335
+ inputIndex > 0
1336
+ ? inputGroup[inputIndex - 1].focus()
1337
+ : setFocus(focusableElements.indexOf(document.querySelectorAll('.protocol-label')[1]));
1338
+ return true;
1339
+ }
1340
+ return false;
1341
+ }
1342
+
1343
+ function handleDirectionKey(key) {
1344
+ if (isModalActive) return;
1345
+
1346
+ if (isFullscreen() && (key === 'Enter')) {
1347
+ handleVideoControl(key);
1348
+ return;
1349
+ }
1350
+
1351
+ if (handleVideoControl(key)) return;
1352
+
1353
+ if (isInInputGroup(document.activeElement) && handleInputGroupKey(key)) return;
1354
+
1355
+ switch(key) {
1356
+ case 'ArrowUp': case 'ArrowLeft':
1357
+ setFocus(currentFocusIndex - 1);
1358
+ break;
1359
+ case 'ArrowDown': case 'ArrowRight':
1360
+ setFocus(currentFocusIndex + 1);
1361
+ break;
1362
+ }
1363
+ }
1364
+
1365
+ // ========== 10. 键盘事件 ==========
1366
+ function handleEnterKey() {
1367
+ if (isModalActive) return;
1368
+
1369
+ const activeEl = document.activeElement;
1370
+
1371
+ if (isFullscreen()) {
1372
+ videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause();
1373
+ return;
1374
+ }
1375
+
1376
+ if (activeEl.classList.contains('history-item')) {
1377
+ const url = activeEl.querySelector('.history-url').textContent.trim();
1378
+ parseUrlToInput(url);
1379
+ getVideoDataAndPlay(url);
1380
+ return;
1381
+ }
1382
+
1383
+ if (isInInputGroup(activeEl)) {
1384
+ goBtn.click();
1385
+ return;
1386
+ }
1387
+
1388
+ if (['BUTTON', 'LABEL'].includes(activeEl.tagName)) {
1389
+ activeEl.click();
1390
+ }
1391
+ }
1392
+
1393
+ function handleEscKey() {
1394
+
1395
+ if (isModalActive) {
1396
+ if (errorModal.style.display === 'flex') {
1397
+ closeModal(errorModal);
1398
+ } else if (progressModal.style.display === 'flex') {
1399
+ closeModal(progressModal);
1400
+ } else if (exitModal.style.display === 'flex') {
1401
+ closeModal(exitModal);
1402
+ backPressCount = 0;
1403
+ }
1404
+ return;
1405
+ }
1406
+
1407
+
1408
+ if (document.fullscreenElement) {
1409
+ document.exitFullscreen ? document.exitFullscreen() : document.webkitExitFullscreen();
1410
+ } else {
1411
+ handleBackPress();
1412
+ }
1413
+ }
1414
+
1415
+ // ========== 11. URL解析 ==========
1416
+ function parseUrlToInput(url) {
1417
+ try {
1418
+ const urlObj = new URL(url);
1419
+ document.querySelector(`input[name="protocol"][value="${urlObj.protocol.slice(0, -1)}"]`).checked = true;
1420
+ const [ip, port] = urlObj.host.split(':');
1421
+ ip.split('.').forEach((seg, i) => ipInputs[i].value = seg);
1422
+ portInput.value = port || '8080';
1423
+
1424
+ } catch (e) {
1425
+ console.error('URL解析失败:', e);
1426
+ showErrorModal('URL解析失败,请检查格式是否正确');
1427
+ }
1428
+ }
1429
+
1430
+ // ========== 12. 核心:获取视频并播放 ==========
1431
+ function getVideoDataAndPlay(serverUrl) {
1432
+ document.getElementById('player-placeholder').innerHTML = `<div class="player-icon">⏳</div><div class="player-tip">正在获取视频数据...</div>`;
1433
+ targetServerUrl = serverUrl;
1434
+
1435
+ $.get(`${serverUrl}/playUrl?enhance=true`, function(result) {
1436
+ if (!result) {
1437
+ showErrorModal('未获取到视频数据,请检查服务器是否正常');
1438
+ resetToInitialState();
1439
+ return;
1440
+ }
1441
+
1442
+ try {
1443
+ result = JSON.parse(result);
1444
+ const videoUrl = result.url;
1445
+ currentVideoUrl = videoUrl;
1446
+ windowHeaders = result.headers || {};
1447
+
1448
+ if (!videoUrl) throw new Error('视频地址为空');
1449
+
1450
+ if (result.title) {
1451
+ currentEpisodeTitle = result.title;
1452
+ } else {
1453
+ currentEpisodeTitle = `剧集${new Date().getTime()}`;
1454
+ }
1455
+ currentPlayTitle.textContent = `正在播放:${currentEpisodeTitle}`;
1456
+
1457
+ const playCallback = () => {
1458
+ if (isTizenEnv) {
1459
+ playVideoForTizen(videoUrl, result.subtitle);
1460
+ } else {
1461
+ playVideoForPC(videoUrl, result.subtitle);
1462
+ }
1463
+
1464
+ currentPlaySection.style.display = 'block';
1465
+ videoContainer.style.display = 'block';
1466
+ document.getElementById('player-placeholder').style.display = 'none';
1467
+
1468
+ customFullscreenBtn.classList.remove('hidden');
1469
+ setFocus(focusableElements.indexOf(customFullscreenBtn) || 0);
1470
+ saveHistory(serverUrl);
1471
+ };
1472
+
1473
+ checkPlayProgress(videoUrl, playCallback);
1474
+
1475
+ } catch (error) {
1476
+ console.error('播放失败:', error);
1477
+ showErrorModal(`播放失败:${error.message}`);
1478
+ resetToInitialState();
1479
+ }
1480
+ }).fail(function(err) {
1481
+ console.error('投屏失败:', err);
1482
+ showErrorModal(`投屏失败:无法连接到 ${serverUrl},请检查IP地址和端口是否正确`);
1483
+ resetToInitialState();
1484
+ });
1485
+ }
1486
+
1487
+ // ========== 13. 检查播放进度 ==========
1488
+ function checkPlayProgress(videoUrl, callbackParam) {
1489
+ callback = callbackParam;
1490
+
1491
+ const lastUrl = localStorage.getItem('last_url');
1492
+ lastPos = localStorage.getItem('last_pos');
1493
+
1494
+ if (lastUrl === videoUrl && lastPos && lastPos > 0) {
1495
+ progressModal.style.display = 'flex';
1496
+
1497
+ setupModalFocus();
1498
+ } else {
1499
+ callback();
1500
+ }
1501
+ }
1502
+
1503
+ // ========== 14. 播放逻辑 ==========
1504
+ function playVideoForPC(videoUrl, subtitle) {
1505
+ videoPlayer.src = '';
1506
+ videoPlayer.preload = 'auto';
1507
+ videoPlayer.autoplay = true;
1508
+
1509
+ if (videoUrl.includes('.m3u8') && window.Hls && Hls.isSupported()) {
1510
+ const pcHlsConfig = {
1511
+ headers: windowHeaders,
1512
+ maxBufferLength: 15,
1513
+ maxBufferHole: 0.5,
1514
+ segmentLoadingTimeOut: 15000,
1515
+ segmentLoadingMaxRetry: 5,
1516
+ enableWorker: true,
1517
+ enableSoftwareAES: true,
1518
+ audioCodec: 'mp4a.40.2,mp3,ac-3,ec-3',
1519
+ videoCodec: 'avc1.42E01E,avc1.640028,hev1.1.6.L93.90'
1520
+ };
1521
+
1522
+ if (hls) hls.destroy();
1523
+ hls = new Hls(pcHlsConfig);
1524
+ hls.loadSource(videoUrl);
1525
+ hls.attachMedia(videoPlayer);
1526
+
1527
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
1528
+ videoPlayer.play().catch(err => {
1529
+ console.error('PC端HLS播放失败:', err);
1530
+ try {
1531
+ videoPlayer.src = videoUrl;
1532
+ videoPlayer.play();
1533
+ } catch (e) {
1534
+ showErrorModal('视频播放失败,请检查流地址是否有效');
1535
+ resetToInitialState();
1536
+ }
1537
+ });
1538
+ });
1539
+
1540
+ hls.on(Hls.Events.ERROR, function(event, data) {
1541
+ console.error('PC端HLS错误:', data);
1542
+ let errorMsg = '播放错误';
1543
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
1544
+ errorMsg = '网络加载失败,请检查网络连接';
1545
+ } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
1546
+ errorMsg = '视频解码失败,请检查流格式';
1547
+ }
1548
+
1549
+ if (data.fatal) {
1550
+ switch(data.type) {
1551
+ case Hls.ErrorTypes.NETWORK_ERROR:
1552
+ hls.startLoad();
1553
+ break;
1554
+ case Hls.ErrorTypes.MEDIA_ERROR:
1555
+ hls.recoverMediaError();
1556
+ break;
1557
+ default:
1558
+ showErrorModal(errorMsg);
1559
+ resetToInitialState();
1560
+ break;
1561
+ }
1562
+ }
1563
+ });
1564
+ } else {
1565
+ try {
1566
+ videoPlayer.src = videoUrl;
1567
+ videoPlayer.play().catch(err => {
1568
+ console.error('PC端视频播放失败:', err);
1569
+ showErrorModal('视频播放失败,请检查文件格式');
1570
+ resetToInitialState();
1571
+ });
1572
+
1573
+ if (subtitle && subtitle.url) {
1574
+ const track = document.createElement('track');
1575
+ track.kind = 'subtitles';
1576
+ track.src = subtitle.url;
1577
+ track.label = subtitle.label || '中文';
1578
+ track.srclang = subtitle.lang || 'zh-CN';
1579
+ track.default = true;
1580
+ videoPlayer.appendChild(track);
1581
+ }
1582
+ } catch (error) {
1583
+ console.error('PC端播放异常:', error);
1584
+ showErrorModal(`视频播放错误:${error.message}`);
1585
+ resetToInitialState();
1586
+ }
1587
+ }
1588
+
1589
+ videoPlayer.addEventListener('timeupdate', function() {
1590
+ if (!videoPlayer.paused && videoPlayer.duration) {
1591
+ localStorage.setItem('last_url', currentVideoUrl);
1592
+ localStorage.setItem('last_pos', videoPlayer.currentTime);
1593
+ }
1594
+ });
1595
+
1596
+ videoPlayer.addEventListener('ended', function() {
1597
+ localStorage.removeItem('last_url');
1598
+ localStorage.removeItem('last_pos');
1599
+ resetToInitialState();
1600
+ });
1601
+ }
1602
+
1603
+ function playVideoForTizen(videoUrl, subtitle) {
1604
+ videoPlayer.src = '';
1605
+ videoPlayer.preload = 'none';
1606
+ videoPlayer.autoplay = false;
1607
+
1608
+ const tizenHlsConfig = {
1609
+ headers: windowHeaders,
1610
+ maxBufferLength: 30,
1611
+ maxMaxBufferLength: 60,
1612
+ startLevel: 0,
1613
+ maxBufferSize: 60 * 1024 * 1024,
1614
+ maxBufferHole: 0.8,
1615
+ manifestLoadingTimeOut: 15000,
1616
+ manifestLoadingMaxRetry: 5,
1617
+ segmentLoadingTimeOut: 20000,
1618
+ segmentLoadingMaxRetry: 8,
1619
+ lowLatencyMode: false,
1620
+ backBufferLength: 90,
1621
+ enableWorker: true,
1622
+ enableSoftwareAES: true,
1623
+ audioCodec: 'mp4a.40.2,mp3,ac-3,ec-3',
1624
+ videoCodec: 'avc1.42E01E,avc1.640028,hev1.1.6.L93.90'
1625
+ };
1626
+
1627
+ if (videoUrl.includes('.m3u8')) {
1628
+ if (videoPlayer.canPlayType('application/vnd.apple.mpegurl')) {
1629
+ try {
1630
+ videoPlayer.src = videoUrl;
1631
+ videoPlayer.crossOrigin = 'anonymous';
1632
+
1633
+ videoPlayer.addEventListener('loadedmetadata', function() {
1634
+ videoPlayer.load();
1635
+ videoPlayer.addEventListener('canplaythrough', function() {
1636
+ videoPlayer.play().catch(err => {
1637
+ console.error('Tizen原生播放失败,降级HLS.js:', err);
1638
+ loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
1639
+ });
1640
+ }, { once: true });
1641
+ }, { once: true });
1642
+
1643
+ videoPlayer.addEventListener('error', function() {
1644
+ console.error('Tizen原生HLS错误,降级HLS.js');
1645
+ loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
1646
+ }, { once: true });
1647
+ } catch (error) {
1648
+ console.error('Tizen原生初始化失败:', error);
1649
+ loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
1650
+ }
1651
+ } else if (window.Hls && Hls.isSupported()) {
1652
+ loadWithHlsJs(videoUrl, tizenHlsConfig, subtitle);
1653
+ } else {
1654
+ throw new Error('Tizen电视不支持HLS播放');
1655
+ }
1656
+ } else {
1657
+ try {
1658
+ videoPlayer.src = videoUrl;
1659
+ videoPlayer.crossOrigin = 'anonymous';
1660
+
1661
+ videoPlayer.addEventListener('canplaythrough', function() {
1662
+ videoPlayer.play();
1663
+ }, { once: true });
1664
+
1665
+ if (subtitle && subtitle.url) {
1666
+ const track = document.createElement('track');
1667
+ track.kind = 'subtitles';
1668
+ track.src = subtitle.url;
1669
+ track.label = subtitle.label || '中文';
1670
+ track.srclang = subtitle.lang || 'zh-CN';
1671
+ track.default = true;
1672
+ videoPlayer.appendChild(track);
1673
+ }
1674
+ } catch (error) {
1675
+ console.error('Tizen普通视频播放错误:', error);
1676
+ showErrorModal(`视频播放错误:${error.message}`);
1677
+ resetToInitialState();
1678
+ }
1679
+ }
1680
+
1681
+ videoPlayer.addEventListener('timeupdate', function() {
1682
+ if (!videoPlayer.paused && videoPlayer.duration) {
1683
+ localStorage.setItem('last_url', currentVideoUrl);
1684
+ localStorage.setItem('last_pos', videoPlayer.currentTime);
1685
+ }
1686
+ });
1687
+
1688
+ videoPlayer.addEventListener('ended', function() {
1689
+ localStorage.removeItem('last_url');
1690
+ localStorage.removeItem('last_pos');
1691
+ resetToInitialState();
1692
+ });
1693
+
1694
+ videoPlayer.addEventListener('stalled', function() {
1695
+ console.warn('Tizen缓冲中断,尝试恢复');
1696
+ videoPlayer.pause();
1697
+
1698
+ videoPlayer.addEventListener('canplay', function() {
1699
+ videoPlayer.play();
1700
+ }, { once: true });
1701
+ });
1702
+ }
1703
+
1704
+ function loadWithHlsJs(videoUrl, config, subtitle) {
1705
+ if (hls) {
1706
+ hls.destroy();
1707
+ }
1708
+
1709
+ hls = new Hls(config);
1710
+ hls.loadSource(videoUrl);
1711
+ hls.attachMedia(videoPlayer);
1712
+
1713
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
1714
+ videoPlayer.load();
1715
+ videoPlayer.addEventListener('canplaythrough', function() {
1716
+ videoPlayer.play().catch(err => {
1717
+ console.error('HLS.js播放失败:', err);
1718
+ showErrorModal('视频播放失败,请检查流格式');
1719
+ resetToInitialState();
1720
+ });
1721
+ }, { once: true });
1722
+ });
1723
+
1724
+ hls.on(Hls.Events.ERROR, function(event, data) {
1725
+ console.error('HLS播放错误:', data);
1726
+ let errorMsg = '播放错误';
1727
+ if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
1728
+ errorMsg = '网络加载失败,请检查网络连接';
1729
+ } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
1730
+ errorMsg = '视频解码失败,流格式不兼容';
1731
+ }
1732
+
1733
+ if (data.fatal) {
1734
+ switch(data.type) {
1735
+ case Hls.ErrorTypes.NETWORK_ERROR:
1736
+ hls.startLoad();
1737
+ return;
1738
+ case Hls.ErrorTypes.MEDIA_ERROR:
1739
+ hls.recoverMediaError();
1740
+ return;
1741
+ }
1742
+ }
1743
+
1744
+ showErrorModal(errorMsg);
1745
+ resetToInitialState();
1746
+ });
1747
+
1748
+ if (subtitle && subtitle.url) {
1749
+ const track = document.createElement('track');
1750
+ track.kind = 'subtitles';
1751
+ track.src = subtitle.url;
1752
+ track.label = subtitle.label || '中文';
1753
+ track.srclang = subtitle.lang || 'zh-CN';
1754
+ track.default = true;
1755
+ videoPlayer.appendChild(track);
1756
+ }
1757
+ }
1758
+
1759
+ // ========== 15. 历史记录管理 ==========
1760
+ function saveHistory(serverUrl) {
1761
+ let history = JSON.parse(localStorage.getItem('castHistory') || '[]');
1762
+
1763
+ history = history.filter(item => item !== serverUrl);
1764
+
1765
+ history.unshift(serverUrl);
1766
+
1767
+
1768
+ if (history.length > 10) history = history.slice(0, 10);
1769
+
1770
+ localStorage.setItem('castHistory', JSON.stringify(history));
1771
+ renderHistoryList();
1772
+ }
1773
+
1774
+ // ========== 16. 清空历史记录 ==========
1775
+ document.getElementById('clearHistory').addEventListener('click', function() {
1776
+ localStorage.removeItem('castHistory');
1777
+ renderHistoryList();
1778
+ });
1779
+
1780
+
1781
+
1782
+
1783
+
1784
+
1785
+
1786
+
1787
+
1788
+
1789
+ // ========== 17. 投屏按钮点击事件 ==========
1790
+ goBtn.addEventListener('click', function() {
1791
+
1792
+ const ipSegments = ipInputs.map(input => input.value.trim());
1793
+ const port = portInput.value.trim() || '8080';
1794
+ const protocol = document.querySelector('input[name="protocol"]:checked').value;
1795
+
1796
+
1797
+
1798
+
1799
+
1800
+
1801
+
1802
+
1803
+
1804
+
1805
+
1806
+
1807
+ const ipValid = ipSegments.every(seg => /^\d{1,3}$/.test(seg) && parseInt(seg) <= 255);
1808
+
1809
+ const portValid = /^\d{1,5}$/.test(port) && parseInt(port) <= 65535;
1810
+
1811
+ if (!ipValid) {
1812
+ showErrorModal('IP地址格式错误,请输入有效的IPv4地址');
1813
+
1814
+
1815
+
1816
+ return;
1817
+ }
1818
+
1819
+ if (!portValid) {
1820
+ showErrorModal('端口号格式错误,请输入1-65535之间的数字');
1821
+ return;
1822
+ }
1823
+
1824
+
1825
+ const serverUrl = `${protocol}://${ipSegments.join('.')}:${port}`;
1826
+
1827
+
1828
+
1829
+ getVideoDataAndPlay(serverUrl);
1830
+ });
1831
+
1832
+
1833
+
1834
+
1835
+
1836
+
1837
+
1838
+
1839
+
1840
+ // ========== 18. 全局键盘事件监听 ==========
1841
+ document.addEventListener('keydown', function(e) {
1842
+ if (isModalActive) return;
1843
+
1844
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight','Enter', 'Escape', 'Back'].includes(e.key)) {
1845
+
1846
+
1847
+ e.preventDefault();
1848
+
1849
+
1850
+
1851
+ }
1852
+
1853
+
1854
+
1855
+
1856
+
1857
+
1858
+
1859
+ switch(e.key) {
1860
+ case 'Enter':
1861
+ if (isFullscreen()) {
1862
+ videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause();
1863
+ } else {
1864
+ handleEnterKey();
1865
+
1866
+
1867
+ }
1868
+ break;
1869
+ case 'Escape':
1870
+ case 'Back':
1871
+ handleEscKey();
1872
+ break;
1873
+ case 'ArrowUp':
1874
+ case 'ArrowDown':
1875
+ case 'ArrowLeft':
1876
+ case 'ArrowRight':
1877
+ handleDirectionKey(e.key);
1878
+ break;
1879
+ }
1880
+ });
1881
+
1882
+
1883
+
1884
+ // ========== 初始化 ==========
1885
+ function init() {
1886
+ initInputEvents();
1887
+ renderHistoryList(); // 先渲染历史记录
1888
+ initFullscreenButton();
1889
+ updateFocusableElements();
1890
+ setFocus(0);
1891
+ window.addEventListener('resize', function() {
1892
+ updateFocusableElements();
1893
+
1894
+ });
1895
+
1896
+ if (isTizenEnv) {
1897
+ console.log('检测到Tizen系统,启用电视适配模式');
1898
+ document.addEventListener('visibilitychange', function() {
1899
+ if (!document.hidden && videoPlayer.paused && videoPlayer.src) {
1900
+ videoPlayer.play();
1901
+ }
1902
+ });
1903
+ }
1904
+ }
1905
+
1906
+ init();
1907
+ });
1908
+ </script>
1909
+ </body>
1910
+ </html>