bep40 commited on
Commit
7c6bbda
·
verified ·
1 Parent(s): f244fc1

Upload static/yt_live.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. static/yt_live.js +419 -0
static/yt_live.js ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
2
+ // Default channel: VTV6
3
+ // Features: PiP button, floating mini-player when reading articles/videos
4
+
5
+ (function(){
6
+ if(window._ytLiveLoaded) return;
7
+ window._ytLiveLoaded = true;
8
+
9
+ const CHANNELS = [
10
+ {id:'vtv1', name:'VTV1', badge:'Tin tức'},
11
+ {id:'vtv2', name:'VTV2', badge:'Khoa học'},
12
+ {id:'vtv3', name:'VTV3', badge:'Giải trí'},
13
+ {id:'vtv4', name:'VTV4', badge:'Quốc tế'},
14
+ {id:'vtv5', name:'VTV5', badge:'Miền Nam'},
15
+ {id:'vtv6', name:'VTV6', badge:'Thanh niên'},
16
+ {id:'vtv7', name:'VTV7', badge:'Giáo dục'},
17
+ {id:'vtv8', name:'VTV8', badge:'Miền Trung'},
18
+ {id:'vtv9', name:'VTV9', badge:'Miền Bắc'},
19
+ {id:'vtv10', name:'VTV10', badge:'VTV Cần Thơ'},
20
+ {id:'vtvprime', name:'VTVPrime', badge:'Prime'},
21
+ ];
22
+ const DEFAULT_CHANNEL = 'vtv6';
23
+ const NEEDS_PROXY = /fptplay\.net/;
24
+
25
+ const STREAMS = {};
26
+ let _currentCh = null;
27
+ let _hls = null;
28
+ let _loading = false;
29
+ let _blockInserted = false;
30
+ let _streamsLoaded = false;
31
+ let _pipActive = false;
32
+ let _miniActive = false;
33
+
34
+ // ===== STYLES =====
35
+ const style = document.createElement('style');
36
+ style.textContent = `
37
+ .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
38
+ .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
39
+ .vtv-title{font-size:13px;font-weight:800;color:#00ccff}
40
+ .vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}
41
+ @keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}
42
+ .vtv-tabs{display:flex;gap:3px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;background:#0d1a2a}
43
+ .vtv-tabs::-webkit-scrollbar{display:none}
44
+ .vtv-tab{padding:4px 8px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:10px;color:#8ab4d8;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
45
+ .vtv-tab:hover{background:#0b4a7a;color:#fff}
46
+ .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
47
+ .vtv-tab.off{opacity:.35;pointer-events:none}
48
+ .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
49
+ .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
50
+ .vtv-err{display:flex;align-items:center;justify-content:center;height:180px;color:#888;font-size:12px;text-align:center;padding:20px;flex-direction:column;gap:8px}
51
+ .vtv-err button{background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer}
52
+ .vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}
53
+ .vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
54
+ @keyframes vtvspin{to{transform:rotate(360deg)}}
55
+ .vtv-controls{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#0d1a2a;border-top:1px solid #1a2a3a}
56
+ .vtv-pip-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px}
57
+ .vtv-pip-btn:hover{background:#0b4a7a;color:#fff}
58
+ .vtv-pip-btn.on{background:#0066cc;border-color:#00ccff;color:#fff}
59
+ .vtv-pip-btn svg{width:12px;height:12px;fill:currentColor}
60
+ .vtv-epg{margin:0;padding:6px 10px;background:#0a1628;border-top:1px solid #1a2a3a}
61
+ .vtv-epg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
62
+ .vtv-epg-title{font-size:10px;font-weight:700;color:#00ccff}
63
+ .vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
64
+ .vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
65
+ .vtv-epg-list::-webkit-scrollbar{display:none}
66
+ .vtv-epg-item{flex:0 0 auto;padding:3px 6px;background:#1a2a3a;border-radius:4px;font-size:8px;color:#8ab4d8;white-space:nowrap;cursor:pointer}
67
+ .vtv-epg-item:hover{background:#2a4a6a}
68
+ .vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
69
+ .vtv-epg-item .t{font-size:7px;color:#6a8aaa}
70
+ .vtv-epg-item.now .t{color:#aaccee}
71
+ .vtv-epg-item .n{color:#ccc;font-size:8px}
72
+ .vtv-epg-item.now .n{color:#fff}
73
+ .vtv-epg-empty{color:#666;font-size:9px;padding:4px}
74
+ .vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
75
+ .vtv-epg-sp{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
76
+
77
+ /* ===== MINI PLAYER ===== */
78
+ .vtv-mini{position:fixed;top:0;left:0;right:0;z-index:99990;background:#000;border-bottom:2px solid #0066cc;box-shadow:0 4px 20px rgba(0,102,204,.4);display:none;transition:transform .3s ease}
79
+ .vtv-mini.show{display:block}
80
+ .vtv-mini.hidden{transform:translateY(-88%)}
81
+ .vtv-mini-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;max-height:180px}
82
+ .vtv-mini-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
83
+ .vtv-mini-bar{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:linear-gradient(90deg,#003366,#1a1a1a)}
84
+ .vtv-mini-ch{font-size:10px;font-weight:700;color:#00ccff}
85
+ .vtv-mini-epg{font-size:9px;color:#8ab4d8;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-left:8px}
86
+ .vtv-mini-btns{display:flex;gap:3px}
87
+ .vtv-mini-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 6px;border-radius:4px;cursor:pointer}
88
+ .vtv-mini-btn:hover{background:#0b4a7a;color:#fff}
89
+ .vtv-mini-btn.x{background:#600;border-color:#900;color:#f66}
90
+ .vtv-mini-btn.x:hover{background:#900;color:#fff}
91
+ .vtv-mini-peek{position:absolute;bottom:-18px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
92
+ .vtv-mini.hidden .vtv-mini-peek{display:block}
93
+ `;
94
+ document.head.appendChild(style);
95
+
96
+ // ===== MINI PLAYER =====
97
+ function createMiniPlayer(){
98
+ if(document.getElementById('vtv-mini')) return;
99
+ const mini = document.createElement('div');
100
+ mini.className = 'vtv-mini';
101
+ mini.id = 'vtv-mini';
102
+ mini.innerHTML = `
103
+ <div class="vtv-mini-frame"><video id="vtv-mini-vid" playsinline muted preload="auto"></video></div>
104
+ <div class="vtv-mini-bar">
105
+ <div style="display:flex;align-items:center;flex:1;min-width:0">
106
+ <span class="vtv-mini-ch" id="vtv-mini-ch">VTV</span>
107
+ <span class="vtv-mini-epg" id="vtv-mini-epg"></span>
108
+ </div>
109
+ <div class="vtv-mini-btns">
110
+ <button class="vtv-mini-btn" id="vtv-mini-pip" title="PiP">📺</button>
111
+ <button class="vtv-mini-btn" id="vtv-mini-expand" title="Mở rộng">⬆️</button>
112
+ <button class="vtv-mini-btn" id="vtv-mini-hide" title="Thu nhỏ">⬇️</button>
113
+ <button class="vtv-mini-btn x" id="vtv-mini-close" title="Đóng">✕</button>
114
+ </div>
115
+ </div>
116
+ <div class="vtv-mini-peek" id="vtv-mini-peek">📺 Xem</div>`;
117
+ document.body.appendChild(mini);
118
+ document.getElementById('vtv-mini-pip').onclick = toggleMiniPiP;
119
+ document.getElementById('vtv-mini-expand').onclick = expandFromMini;
120
+ document.getElementById('vtv-mini-hide').onclick = ()=>{ mini.classList.add('hidden'); };
121
+ document.getElementById('vtv-mini-close').onclick = closeMiniPlayer;
122
+ document.getElementById('vtv-mini-peek').onclick = ()=>{ mini.classList.remove('hidden'); };
123
+ }
124
+
125
+ function closeMiniPlayer(){
126
+ const mini = document.getElementById('vtv-mini');
127
+ if(mini){
128
+ const v = document.getElementById('vtv-mini-vid');
129
+ if(v){ v.pause(); v.src = ''; }
130
+ mini.remove();
131
+ }
132
+ _miniActive = false;
133
+ }
134
+
135
+ function expandFromMini(){
136
+ if(typeof switchCat === 'function') switchCat('home');
137
+ const mini = document.getElementById('vtv-mini');
138
+ if(mini) mini.classList.remove('hidden');
139
+ }
140
+
141
+ function activateMiniPlayer(){
142
+ if(!_currentCh) return;
143
+ createMiniPlayer();
144
+ _miniActive = true;
145
+ const mini = document.getElementById('vtv-mini');
146
+ const miniVid = document.getElementById('vtv-mini-vid');
147
+ const mainVid = document.getElementById('vtv-player');
148
+
149
+ // Copy source from main player
150
+ if(_hls){
151
+ // HLS: load same source into mini
152
+ const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:10});
153
+ hls.loadSource(STREAMS[_currentCh][0]);
154
+ hls.attachMedia(miniVid);
155
+ miniVid.play().catch(()=>{});
156
+ } else if(mainVid.src){
157
+ miniVid.src = mainVid.src;
158
+ miniVid.play().catch(()=>{});
159
+ }
160
+
161
+ const ch = CHANNELS.find(c=>c.id===_currentCh);
162
+ if(ch) document.getElementById('vtv-mini-ch').textContent = ch.name;
163
+ updateMiniEpg();
164
+ mini.classList.add('show');
165
+ mini.classList.remove('hidden');
166
+ }
167
+
168
+ function updateMiniEpg(){
169
+ if(!_currentCh) return;
170
+ fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{
171
+ const progs = d.programs||[];
172
+ const now = progs.find(p=>p.now);
173
+ document.getElementById('vtv-mini-epg').textContent = now ? now.time+' '+now.title : (progs[0] ? progs[0].time+' '+progs[0].title : '');
174
+ }).catch(()=>{});
175
+ }
176
+
177
+ async function toggleMiniPiP(){
178
+ const v = document.getElementById('vtv-mini-vid');
179
+ if(!v) return;
180
+ try{
181
+ if(document.pictureInPictureElement === v){
182
+ await document.exitPictureInPicture();
183
+ } else {
184
+ await v.requestPictureInPicture();
185
+ }
186
+ } catch(e){ alert('Không hỗ trợ PiP'); }
187
+ }
188
+
189
+ // ===== MAIN PLAYER =====
190
+ async function loadAllStreams(){
191
+ if(_loading) return;
192
+ _loading = true;
193
+ const el = document.getElementById('vtv-load');
194
+ if(el) el.innerHTML = '<div class="vtv-spinner"></div>Đang tải kênh...';
195
+ try{
196
+ const r = await fetch('/api/vtv/streams', {signal: AbortSignal.timeout(10000)});
197
+ if(r.ok){
198
+ const data = await r.json();
199
+ CHANNELS.forEach(ch=>{
200
+ const info = data[ch.id];
201
+ if(info && info.stream_url){
202
+ let url = info.stream_url;
203
+ if(NEEDS_PROXY.test(url)) url = '/api/proxy/m3u8/vtv?url='+encodeURIComponent(url);
204
+ STREAMS[ch.id] = [url];
205
+ } else {
206
+ STREAMS[ch.id] = [];
207
+ }
208
+ });
209
+ }
210
+ } catch(e){ console.warn('VTV API error:', e); }
211
+ CHANNELS.forEach(ch=>{
212
+ const tab = document.getElementById('vtvt-'+ch.id);
213
+ if(tab){
214
+ if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){ tab.classList.remove('off'); tab.textContent=ch.name; }
215
+ else { tab.style.opacity='0.35'; tab.textContent=ch.name+' ✕'; }
216
+ }
217
+ });
218
+ _loading = false;
219
+ _streamsLoaded = true;
220
+ }
221
+
222
+ function buildBlock(){
223
+ const w = document.createElement('div');
224
+ w.className = 'vtv-wrap';
225
+ w.id = 'vtv-block';
226
+ let tabs = '';
227
+ CHANNELS.forEach(ch=>{
228
+ tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';
229
+ });
230
+ w.innerHTML =
231
+ '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>' +
232
+ '<div class="vtv-tabs">'+tabs+'</div>' +
233
+ '<div class="vtv-frame">' +
234
+ '<div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải kênh...</div>' +
235
+ '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>' +
236
+ '<div class="vtv-err" id="vtv-err" style="display:none"><span id="vtv-err-msg">Không thể tải kênh</span><button onclick="window._vtvRetry()">Thử lại</button></div>' +
237
+ '</div>' +
238
+ '<div class="vtv-controls">' +
239
+ '<button class="vtv-pip-btn" id="vtv-pip-btn" onclick="window._vtvTogglePiP()">' +
240
+ '<svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="12" y="10" width="8" height="6" rx="1" fill="currentColor"/></svg>PiP' +
241
+ '</button>' +
242
+ '<span style="flex:1"></span>' +
243
+ '<span style="font-size:9px;color:#666" id="vtv-status"></span>' +
244
+ '</div>' +
245
+ '<div class="vtv-epg" id="vtv-epg">' +
246
+ '<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
247
+ '<div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải...</div></div>' +
248
+ '</div>';
249
+ return w;
250
+ }
251
+
252
+ async function loadEpg(chId){
253
+ const el = document.getElementById('vtv-epg-list');
254
+ if(!el) return;
255
+ el.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
256
+ try{
257
+ const r = await fetch('/api/vtv/epg/'+chId, {signal: AbortSignal.timeout(10000)});
258
+ if(!r.ok) throw new Error('fail');
259
+ const data = await r.json();
260
+ const progs = data.programs||[];
261
+ if(!progs.length){ el.innerHTML='<div class="vtv-epg-empty">Chưa có lịch phát sóng</div>'; return; }
262
+ let h = '';
263
+ progs.forEach(p=>{
264
+ const nw = p.now ? ' now' : '';
265
+ h += '<div class="vtv-epg-item'+nw+'"><span class="t">'+(p.time||'')+(p.end_time?'-'+p.end_time:'')+'</span> <span class="n">'+(p.title||'')+'</span></div>';
266
+ });
267
+ el.innerHTML = h;
268
+ const nowItem = el.querySelector('.vtv-epg-item.now');
269
+ if(nowItem) nowItem.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});
270
+ } catch(e){
271
+ el.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch</div>';
272
+ }
273
+ }
274
+
275
+ window._vtvToggleEpg = function(){
276
+ const l = document.getElementById('vtv-epg-list'), b = document.getElementById('vtv-epg-toggle');
277
+ if(!l||!b) return;
278
+ if(l.style.display==='none'){ l.style.display='flex'; b.textContent='Ẩn'; }
279
+ else { l.style.display='none'; b.textContent='Hiện'; }
280
+ };
281
+
282
+ window._vtvTogglePiP = async function(){
283
+ const v = document.getElementById('vtv-player');
284
+ const btn = document.getElementById('vtv-pip-btn');
285
+ if(!v) return;
286
+ try{
287
+ if(document.pictureInPictureElement === v){
288
+ await document.exitPictureInPicture();
289
+ _pipActive = false;
290
+ if(btn) btn.classList.remove('on');
291
+ } else {
292
+ await v.requestPictureInPicture();
293
+ _pipActive = true;
294
+ if(btn) btn.classList.add('on');
295
+ }
296
+ } catch(e){ alert('Không hỗ trợ PiP'); }
297
+ };
298
+
299
+ function pinBlock(){
300
+ const h = document.getElementById('view-home');
301
+ if(!h||document.getElementById('vtv-block')) return;
302
+ h.insertBefore(buildBlock(), h.firstChild);
303
+ _blockInserted = true;
304
+ if(!_streamsLoaded){
305
+ loadAllStreams().then(()=>{
306
+ setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL), 300);
307
+ });
308
+ }
309
+ }
310
+
311
+ window._vtvRetry = function(){ if(_currentCh) window._vtvPlay(_currentCh); };
312
+
313
+ window._vtvPlay = function(chId){
314
+ const ch = CHANNELS.find(c=>c.id===chId);
315
+ if(!ch) return;
316
+ _currentCh = chId;
317
+ document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
318
+ const tab = document.getElementById('vtvt-'+chId);
319
+ if(tab) tab.classList.add('on');
320
+ const video = document.getElementById('vtv-player');
321
+ const errEl = document.getElementById('vtv-err');
322
+ const loadEl = document.getElementById('vtv-load');
323
+ const errMsg = document.getElementById('vtv-err-msg');
324
+ video.style.display='none';
325
+ errEl.style.display='none';
326
+ loadEl.style.display='flex';
327
+ loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
328
+ if(_hls){_hls.destroy(); _hls=null;}
329
+ const urls = STREAMS[chId]||[];
330
+ if(!urls.length){
331
+ loadEl.style.display='none'; errEl.style.display='flex';
332
+ errMsg.textContent = chId==='vtvprime' ? 'VTVPrime: Kênh trả phí.' : ch.name+': Không có luồng.';
333
+ return;
334
+ }
335
+ _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
336
+ loadEpg(chId);
337
+ if(_miniActive){
338
+ document.getElementById('vtv-mini-ch').textContent = ch.name;
339
+ updateMiniEpg();
340
+ }
341
+ };
342
+
343
+ function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
344
+ if(idx>=urls.length){
345
+ loadEl.style.display='none'; errEl.style.display='flex';
346
+ errMsg.textContent = name+': Tất cả nguồn lỗi.';
347
+ return;
348
+ }
349
+ const src = urls[idx];
350
+ loadEl.innerHTML = '<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
351
+ if(typeof Hls!=='undefined'&&Hls.isSupported()){
352
+ const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20});
353
+ _hls = hls;
354
+ hls.loadSource(src);
355
+ hls.attachMedia(video);
356
+ hls.on(Hls.Events.MANIFEST_PARSED,()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; });
357
+ let rec = 0;
358
+ hls.on(Hls.Events.ERROR,(ev,data)=>{
359
+ if(data.fatal){
360
+ if(data.type===Hls.ErrorTypes.NETWORK_ERROR){
361
+ rec++;
362
+ if(rec<=3) setTimeout(()=>hls.startLoad(), 2000);
363
+ else { hls.destroy(); _hls=null; _tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg); }
364
+ } else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){
365
+ try{hls.recoverMediaError();}catch(e){}
366
+ } else { hls.destroy(); _hls=null; _tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg); }
367
+ }
368
+ });
369
+ } else if(video.canPlayType('application/vnd.apple.mpegurl')){
370
+ video.src = src;
371
+ video.addEventListener('loadedmetadata',()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; },{once:true});
372
+ video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
373
+ } else {
374
+ loadEl.style.display='none'; errEl.style.display='flex'; errMsg.textContent='Không hỗ trợ HLS';
375
+ }
376
+ }
377
+
378
+ // ===== KEEP PLAYER ALIVE WHEN NAVIGATING =====
379
+ // Override showView to show mini player when leaving home
380
+ const _origShowView = window.showView;
381
+ window.showView = function(id){
382
+ if(id!=='view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive){
383
+ activateMiniPlayer();
384
+ }
385
+ return _origShowView.apply(this, arguments);
386
+ };
387
+
388
+ // Override readArticle
389
+ const _origReadArticle = window.readArticle;
390
+ if(_origReadArticle){
391
+ window.readArticle = function(url){
392
+ if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive) activateMiniPlayer();
393
+ return _origReadArticle.apply(this, arguments);
394
+ };
395
+ }
396
+
397
+ // Override switchCat
398
+ const _origSwitchCat = window.switchCat;
399
+ if(_origSwitchCat){
400
+ window.switchCat = function(id){
401
+ if(id!=='view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive) activateMiniPlayer();
402
+ return _origSwitchCat.apply(this, arguments);
403
+ };
404
+ }
405
+
406
+ // Override loadHome to inject VTV block
407
+ const _origLoadHome = window.loadHome;
408
+ if(_origLoadHome && !_origLoadHome.__vtvWrapped){
409
+ window.loadHome = async function(){
410
+ const old = document.getElementById('vtv-block');
411
+ if(old) old.remove();
412
+ _blockInserted = false;
413
+ const r = await _origLoadHome.apply(this, arguments);
414
+ try{ pinBlock(); }catch(e){}
415
+ return r;
416
+ };
417
+ window.loadHome.__vtvWrapped = true;
418
+ }
419
+ })();