bep40 commited on
Commit
caeddef
·
verified ·
1 Parent(s): 12e568b

Upload static/yt_live.js

Browse files
Files changed (1) hide show
  1. static/yt_live.js +183 -339
static/yt_live.js CHANGED
@@ -1,31 +1,27 @@
1
  // === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
2
- // Uses backend /api/vtv/streams for stream URLs
3
- // VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
4
  // Default channel: VTV6
5
- // Features: PiP button, floating mini-player when reading articles
6
 
7
  (function(){
8
  if(window._ytLiveLoaded) return;
9
  window._ytLiveLoaded = true;
10
 
11
  const CHANNELS = [
12
- {id:'vtv1', name:'VTV1', badge:'Tin tức'},
13
- {id:'vtv2', name:'VTV2', badge:'Khoa học'},
14
- {id:'vtv3', name:'VTV3', badge:'Giải trí'},
15
- {id:'vtv4', name:'VTV4', badge:'Quốc tế'},
16
- {id:'vtv5', name:'VTV5', badge:'Miền Nam'},
17
- {id:'vtv6', name:'VTV6', badge:'Thanh niên'},
18
- {id:'vtv7', name:'VTV7', badge:'Giáo dục'},
19
- {id:'vtv8', name:'VTV8', badge:'Miền Trung'},
20
- {id:'vtv9', name:'VTV9', badge:'Miền Bắc'},
21
- {id:'vtv10', name:'VTV10', badge:'VTV10'},
22
- {id:'vtvprime', name:'VTVPrime', badge:'Prime'},
23
  ];
24
-
25
- // Default channel is VTV6
26
  const DEFAULT_CHANNEL = 'vtv6';
27
-
28
  const NEEDS_PROXY = /fptplay\.net/;
 
29
  const STREAMS = {};
30
  let _currentCh = null;
31
  let _hls = null;
@@ -33,11 +29,11 @@
33
  let _blockInserted = false;
34
  let _streamsLoaded = false;
35
  let _pipActive = false;
36
- let _miniPlayerActive = false;
37
 
38
- // ===== CSS =====
39
- const s = document.createElement('style');
40
- s.textContent = `
41
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
42
  .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
43
  .vtv-title{font-size:13px;font-weight:800;color:#00ccff}
@@ -56,47 +52,46 @@
56
  .vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}
57
  .vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
58
  @keyframes vtvspin{to{transform:rotate(360deg)}}
 
 
 
 
 
59
  .vtv-epg{margin:0;padding:6px 10px;background:#0a1628;border-top:1px solid #1a2a3a}
60
  .vtv-epg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
61
  .vtv-epg-title{font-size:10px;font-weight:700;color:#00ccff}
62
  .vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
63
  .vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
64
  .vtv-epg-list::-webkit-scrollbar{display:none}
65
- .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;transition:all .2s}
66
  .vtv-epg-item:hover{background:#2a4a6a}
67
  .vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
68
- .vtv-epg-item .epg-t{font-size:7px;color:#6a8aaa}
69
- .vtv-epg-item.now .epg-t{color:#aaccee}
70
- .vtv-epg-item .epg-n{color:#ccc;font-size:8px}
71
- .vtv-epg-item.now .epg-n{color:#fff}
72
  .vtv-epg-empty{color:#666;font-size:9px;padding:4px}
73
  .vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
74
- .vtv-epg-spinner{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
75
- .vtv-controls{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#0d1a2a;border-top:1px solid #1a2a3a}
76
- .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}
77
- .vtv-pip-btn:hover{background:#0b4a7a;color:#fff}
78
- .vtv-pip-btn.active{background:#0066cc;border-color:#00ccff;color:#fff}
79
- .vtv-pip-btn svg{width:12px;height:12px;fill:currentColor}
80
 
81
- /* ===== MINI PLAYER (floating when reading articles) ===== */
82
- .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,.3);display:none;transition:transform .3s ease}
83
  .vtv-mini.show{display:block}
84
- .vtv-mini.vtv-mini-hidden{transform:translateY(-85%)}
85
- .vtv-mini-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;max-height:200px}
86
  .vtv-mini-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
87
- .vtv-mini-bar{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:linear-gradient(90deg,#003366,#1a1a1a)}
88
- .vtv-mini-info{display:flex;align-items:center;gap:6px}
89
  .vtv-mini-ch{font-size:10px;font-weight:700;color:#00ccff}
90
- .vtv-mini-epg{font-size:9px;color:#8ab4d8;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
91
- .vtv-mini-btns{display:flex;gap:4px}
92
- .vtv-mini-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 6px;border-radius:4px;cursor:pointer}
93
  .vtv-mini-btn:hover{background:#0b4a7a;color:#fff}
94
- .vtv-mini-btn.close{background:#660000;border-color:#990000;color:#ff6666}
95
- .vtv-mini-btn.close:hover{background:#990000;color:#fff}
96
- .vtv-mini-peek{position:absolute;bottom:-20px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
97
- .vtv-mini.vtv-mini-hidden .vtv-mini-peek{display:block}
98
  `;
99
- document.head.appendChild(s);
100
 
101
  // ===== MINI PLAYER =====
102
  function createMiniPlayer(){
@@ -105,177 +100,119 @@
105
  mini.className = 'vtv-mini';
106
  mini.id = 'vtv-mini';
107
  mini.innerHTML = `
108
- <div class="vtv-mini-frame">
109
- <video id="vtv-mini-video" playsinline muted preload="auto"></video>
110
- </div>
111
  <div class="vtv-mini-bar">
112
- <div class="vtv-mini-info">
113
  <span class="vtv-mini-ch" id="vtv-mini-ch">VTV</span>
114
  <span class="vtv-mini-epg" id="vtv-mini-epg"></span>
115
  </div>
116
  <div class="vtv-mini-btns">
117
- <button class="vtv-mini-btn" id="vtv-mini-pip" title="PiP">📺 PiP</button>
118
  <button class="vtv-mini-btn" id="vtv-mini-expand" title="Mở rộng">⬆️</button>
119
  <button class="vtv-mini-btn" id="vtv-mini-hide" title="Thu nhỏ">⬇️</button>
120
- <button class="vtv-mini-btn close" id="vtv-mini-close" title="Đóng">✕</button>
121
  </div>
122
  </div>
123
- <div class="vtv-mini-peek" id="vtv-mini-peek">📺 Xem</div>
124
- `;
125
  document.body.appendChild(mini);
126
-
127
- // Event listeners
128
  document.getElementById('vtv-mini-pip').onclick = toggleMiniPiP;
129
  document.getElementById('vtv-mini-expand').onclick = expandFromMini;
130
- document.getElementById('vtv-mini-hide').onclick = hideMiniPlayer;
131
  document.getElementById('vtv-mini-close').onclick = closeMiniPlayer;
132
- document.getElementById('vtv-mini-peek').onclick = showMiniPlayer;
133
- document.getElementById('vtv-mini').onclick = function(e){
134
- if(e.target === this || e.target.classList.contains('vtv-mini-frame')){
135
- expandFromMini();
136
- }
137
- };
138
- }
139
-
140
- function showMiniPlayer(){
141
- const mini = document.getElementById('vtv-mini');
142
- if(mini){
143
- mini.classList.remove('vtv-mini-hidden');
144
- mini.classList.add('show');
145
- }
146
- }
147
-
148
- function hideMiniPlayer(){
149
- const mini = document.getElementById('vtv-mini');
150
- if(mini){
151
- mini.classList.add('vtv-mini-hidden');
152
- }
153
  }
154
 
155
  function closeMiniPlayer(){
156
  const mini = document.getElementById('vtv-mini');
157
  if(mini){
158
- const video = document.getElementById('vtv-mini-video');
159
- if(video){
160
- video.pause();
161
- video.src = '';
162
- }
163
  mini.remove();
164
  }
165
- _miniPlayerActive = false;
166
  }
167
 
168
  function expandFromMini(){
169
- // Go back to home view to show full player
170
- if(typeof switchCat === 'function'){
171
- switchCat('home');
172
- }
173
- showMiniPlayer();
174
  }
175
 
176
  function activateMiniPlayer(){
177
  if(!_currentCh) return;
178
  createMiniPlayer();
179
- _miniPlayerActive = true;
180
-
181
  const mini = document.getElementById('vtv-mini');
182
- const miniVideo = document.getElementById('vtv-mini-video');
183
- const mainVideo = document.getElementById('vtv-player');
184
-
185
- // Copy video source from main player
186
- if(mainVideo && mainVideo.src){
187
- miniVideo.src = mainVideo.src;
188
- miniVideo.play().catch(()=>{});
189
- } else if(mainVideo && mainVideo.querySelector('source')){
190
- const source = mainVideo.querySelector('source');
191
- if(source && source.src){
192
- miniVideo.src = source.src;
193
- miniVideo.play().catch(()=>{});
194
- }
195
  }
196
 
197
- // Update channel info
198
- const ch = CHANNELS.find(c => c.id === _currentCh);
199
- if(ch){
200
- document.getElementById('vtv-mini-ch').textContent = ch.name;
201
- }
202
-
203
- // Update EPG info
204
  updateMiniEpg();
205
-
206
  mini.classList.add('show');
207
- mini.classList.remove('vtv-mini-hidden');
208
  }
209
 
210
  function updateMiniEpg(){
211
  if(!_currentCh) return;
212
- fetch('/api/vtv/epg/' + _currentCh)
213
- .then(r => r.json())
214
- .then(data => {
215
- const programs = data.programs || [];
216
- const nowProg = programs.find(p => p.now);
217
- if(nowProg){
218
- document.getElementById('vtv-mini-epg').textContent = nowProg.time + ' - ' + nowProg.title;
219
- } else if(programs.length > 0){
220
- document.getElementById('vtv-mini-epg').textContent = programs[0].time + ' - ' + programs[0].title;
221
- }
222
- }).catch(()=>{});
223
  }
224
 
225
  async function toggleMiniPiP(){
226
- const miniVideo = document.getElementById('vtv-mini-video');
227
- if(!miniVideo) return;
228
-
229
  try{
230
- if(document.pictureInPictureElement === miniVideo){
231
  await document.exitPictureInPicture();
232
- _pipActive = false;
233
  } else {
234
- await miniVideo.requestPictureInPicture();
235
- _pipActive = true;
236
  }
237
- } catch(e){
238
- console.warn('PiP error:', e);
239
- alert('Trình duyệt không hỗ trợ PiP hoặc video chưa sẵn sàng');
240
- }
241
  }
242
 
243
  // ===== MAIN PLAYER =====
244
  async function loadAllStreams(){
245
  if(_loading) return;
246
  _loading = true;
247
- const loadEl = document.getElementById('vtv-load');
248
- if(loadEl) loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang tải danh sách kênh...';
249
- try {
250
  const r = await fetch('/api/vtv/streams', {signal: AbortSignal.timeout(10000)});
251
  if(r.ok){
252
  const data = await r.json();
253
- CHANNELS.forEach(ch => {
254
  const info = data[ch.id];
255
  if(info && info.stream_url){
256
  let url = info.stream_url;
257
- if(NEEDS_PROXY.test(url)){
258
- url = '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(url);
259
- }
260
  STREAMS[ch.id] = [url];
261
  } else {
262
  STREAMS[ch.id] = [];
263
  }
264
  });
265
  }
266
- } catch(e) {
267
- console.warn('VTV API error:', e);
268
- }
269
- CHANNELS.forEach(ch => {
270
  const tab = document.getElementById('vtvt-'+ch.id);
271
  if(tab){
272
- if(STREAMS[ch.id] && STREAMS[ch.id].length > 0){
273
- tab.classList.remove('off');
274
- tab.textContent = ch.name;
275
- } else {
276
- tab.style.opacity = '0.35';
277
- tab.textContent = ch.name + ' ✕';
278
- }
279
  }
280
  });
281
  _loading = false;
@@ -287,286 +224,193 @@
287
  w.className = 'vtv-wrap';
288
  w.id = 'vtv-block';
289
  let tabs = '';
290
- CHANNELS.forEach(ch => {
291
  tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';
292
  });
293
  w.innerHTML =
294
  '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>' +
295
- '<div class="vtv-tabs">' + tabs + '</div>' +
296
  '<div class="vtv-frame">' +
297
- '<div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải danh sách kênh...</div>' +
298
  '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>' +
299
  '<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>' +
300
  '</div>' +
301
  '<div class="vtv-controls">' +
302
  '<button class="vtv-pip-btn" id="vtv-pip-btn" onclick="window._vtvTogglePiP()">' +
303
- '<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>' +
304
- 'PiP' +
305
  '</button>' +
306
  '<span style="flex:1"></span>' +
307
  '<span style="font-size:9px;color:#666" id="vtv-status"></span>' +
308
  '</div>' +
309
  '<div class="vtv-epg" id="vtv-epg">' +
310
- '<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng (vtv.vn)</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
311
- '<div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải EPG...</div></div>' +
312
  '</div>';
313
  return w;
314
  }
315
 
316
  async function loadEpg(chId){
317
- const epgList = document.getElementById('vtv-epg-list');
318
- if(!epgList) return;
319
-
320
- epgList.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải lịch phát sóng...</div>';
321
-
322
- try {
323
- const r = await fetch('/api/vtv/epg/' + chId, {signal: AbortSignal.timeout(10000)});
324
- if(!r.ok) throw new Error('EPG fetch failed: ' + r.status);
325
  const data = await r.json();
326
- const programs = data.programs || [];
327
-
328
- if(programs.length === 0){
329
- epgList.innerHTML = '<div class="vtv-epg-empty">Chưa có lịch phát sóng cho kênh này</div>';
330
- return;
331
- }
332
-
333
  let h = '';
334
- programs.forEach(p => {
335
- const isNow = p.now ? ' now' : '';
336
- const desc = p.description ? ' title="' + escAttr(p.description) + '"' : '';
337
- h += '<div class="vtv-epg-item' + isNow + '"' + desc + '>';
338
- h += '<span class="epg-t">' + (p.time || '') + '</span> ';
339
- if(p.end_time) h += '<span class="epg-t">-' + p.end_time + '</span> ';
340
- h += '<span class="epg-n">' + (p.title || '') + '</span>';
341
- h += '</div>';
342
  });
343
- epgList.innerHTML = h;
344
-
345
- // Scroll to current program
346
- const nowItem = epgList.querySelector('.vtv-epg-item.now');
347
- if(nowItem){
348
- nowItem.scrollIntoView({behavior: 'smooth', inline: 'center', block: 'nearest'});
349
- }
350
- } catch(e) {
351
- console.warn('EPG load error:', e);
352
- if(epgList) epgList.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch phát sóng</div>';
353
  }
354
  }
355
 
356
- function escAttr(s){
357
- return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
358
- }
359
-
360
  window._vtvToggleEpg = function(){
361
- const list = document.getElementById('vtv-epg-list');
362
- const btn = document.getElementById('vtv-epg-toggle');
363
- if(!list || !btn) return;
364
- if(list.style.display === 'none'){
365
- list.style.display = 'flex';
366
- btn.textContent = 'Ẩn';
367
- } else {
368
- list.style.display = 'none';
369
- btn.textContent = 'Hiện';
370
- }
371
  };
372
 
373
  window._vtvTogglePiP = async function(){
374
- const video = document.getElementById('vtv-player');
375
  const btn = document.getElementById('vtv-pip-btn');
376
- if(!video) return;
377
-
378
  try{
379
- if(document.pictureInPictureElement === video){
380
  await document.exitPictureInPicture();
381
  _pipActive = false;
382
- if(btn) btn.classList.remove('active');
383
  } else {
384
- await video.requestPictureInPicture();
385
  _pipActive = true;
386
- if(btn) btn.classList.add('active');
387
  }
388
- } catch(e){
389
- console.warn('PiP error:', e);
390
- alert('Trình duyệt không hỗ trợ PiP hoặc video chưa sẵn sàng');
391
- }
392
  };
393
 
394
  function pinBlock(){
395
  const h = document.getElementById('view-home');
396
- if(!h) return;
397
- if(document.getElementById('vtv-block')){
398
- _blockInserted = true;
399
- return;
400
- }
401
- const block = buildBlock();
402
- h.insertBefore(block, h.firstChild);
403
  _blockInserted = true;
404
  if(!_streamsLoaded){
405
- loadAllStreams().then(() => {
406
- setTimeout(() => window._vtvPlay(DEFAULT_CHANNEL), 300);
407
  });
408
  }
409
  }
410
 
411
- window._vtvRetry = function(){
412
- if(_currentCh) window._vtvPlay(_currentCh);
413
- };
414
 
415
  window._vtvPlay = function(chId){
416
- const ch = CHANNELS.find(c => c.id === chId);
417
  if(!ch) return;
418
  _currentCh = chId;
419
- document.querySelectorAll('.vtv-tab').forEach(t => t.classList.remove('on'));
420
  const tab = document.getElementById('vtvt-'+chId);
421
  if(tab) tab.classList.add('on');
422
  const video = document.getElementById('vtv-player');
423
  const errEl = document.getElementById('vtv-err');
424
  const loadEl = document.getElementById('vtv-load');
425
  const errMsg = document.getElementById('vtv-err-msg');
426
- video.style.display = 'none';
427
- errEl.style.display = 'none';
428
- loadEl.style.display = 'flex';
429
- loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + ch.name + '...';
430
- if(_hls){ _hls.destroy(); _hls = null; }
431
- const urls = STREAMS[chId] || [];
432
- if(urls.length === 0){
433
- loadEl.style.display = 'none';
434
- errEl.style.display = 'flex';
435
- if(chId === 'vtvprime'){
436
- errMsg.textContent = 'VTVPrime: Kênh trả phí, không có luồng miễn phí.';
437
- } else {
438
- errMsg.textContent = ch.name + ': Không tìm thấy luồng. Thử lại sau.';
439
- }
440
  return;
441
  }
442
  _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
443
  loadEpg(chId);
444
-
445
- // Update mini player if active
446
- if(_miniPlayerActive){
447
  document.getElementById('vtv-mini-ch').textContent = ch.name;
448
  updateMiniEpg();
449
  }
450
  };
451
 
452
  function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
453
- if(idx >= urls.length){
454
- loadEl.style.display = 'none';
455
- errEl.style.display = 'flex';
456
- errMsg.textContent = name + ': Tất cả nguồn đều lỗi. Thử lại sau.';
457
  return;
458
  }
459
  const src = urls[idx];
460
- const sourceLabel = ' (' + (idx+1) + '/' + urls.length + ')';
461
- loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + name + sourceLabel + '...';
462
- if(typeof Hls !== 'undefined' && Hls.isSupported()){
463
- const hls = new Hls({
464
- enableWorker: true,
465
- lowLatencyMode: true,
466
- startLevel: -1,
467
- capLevelToPlayerSize: true,
468
- maxBufferLength: 20,
469
- xhrSetup: function(xhr, url){
470
- if(url.includes('fptplay')){
471
- xhr.setRequestHeader('Referer', 'https://fptplay.vn/');
472
- xhr.setRequestHeader('Origin', 'https://fptplay.vn');
473
- }
474
- }
475
- });
476
  _hls = hls;
477
  hls.loadSource(src);
478
  hls.attachMedia(video);
479
- hls.on(Hls.Events.MANIFEST_PARSED, () => {
480
- video.play().catch(() => {});
481
- loadEl.style.display = 'none';
482
- video.style.display = 'block';
483
- });
484
- let recoverAttempts = 0;
485
- hls.on(Hls.Events.ERROR, (ev, data) => {
486
  if(data.fatal){
487
- if(data.type === Hls.ErrorTypes.NETWORK_ERROR){
488
- recoverAttempts++;
489
- if(recoverAttempts <= 3){
490
- setTimeout(() => hls.startLoad(), 2000);
491
- } else {
492
- hls.destroy();
493
- _hls = null;
494
- _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
495
- }
496
- } else if(data.type === Hls.ErrorTypes.MEDIA_ERROR){
497
- try { hls.recoverMediaError(); } catch(e) {}
498
- } else {
499
- hls.destroy();
500
- _hls = null;
501
- _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
502
- }
503
  }
504
  });
505
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
506
  video.src = src;
507
- video.addEventListener('loadedmetadata', () => {
508
- video.play().catch(() => {});
509
- loadEl.style.display = 'none';
510
- video.style.display = 'block';
511
- }, {once: true});
512
- video.addEventListener('error', () => {
513
- _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
514
- }, {once: true});
515
  } else {
516
- loadEl.style.display = 'none';
517
- errEl.style.display = 'flex';
518
- errMsg.textContent = 'Trình duyệt không hỗ trợ HLS';
519
  }
520
  }
521
 
522
- // ===== KEEP PLAYER WHEN NAVIGATING =====
523
- // Override showView to activate mini player when leaving home
524
- const origShowView = window.showView;
525
  window.showView = function(id){
526
- // If leaving home/article view and VTV is playing, show mini player
527
- if(id !== 'view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
528
- if(!_miniPlayerActive){
529
- activateMiniPlayer();
530
- }
531
  }
532
- return origShowView.apply(this, arguments);
533
  };
534
 
535
- // Override readArticle to keep player
536
- const origReadArticle = window.readArticle;
537
- if(origReadArticle){
538
- window.readArticle = async function(url){
539
- // Activate mini player before navigating
540
- if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
541
- if(!_miniPlayerActive){
542
- activateMiniPlayer();
543
- }
544
- }
545
- return origReadArticle.apply(this, arguments);
546
  };
547
  }
548
 
549
- // Override switchCat to keep player
550
- const origSwitchCat = window.switchCat;
551
- if(origSwitchCat){
552
- window.switchCat = async function(id){
553
- if(id !== 'view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
554
- if(!_miniPlayerActive){
555
- activateMiniPlayer();
556
- }
557
- }
558
- return origSwitchCat.apply(this, arguments);
559
  };
560
  }
561
 
562
- // Wrap loadHome to inject VTV block
563
- const orig = window.loadHome;
564
- if(orig && !orig.__vtvWrapped){
565
  window.loadHome = async function(){
566
- const oldBlock = document.getElementById('vtv-block');
567
- if(oldBlock) oldBlock.remove();
568
  _blockInserted = false;
569
- const r = await orig.apply(this, arguments);
570
  try{ pinBlock(); }catch(e){}
571
  return r;
572
  };
 
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;
 
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}
 
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(){
 
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;
 
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
  };