bep40 commited on
Commit
cc3e711
·
verified ·
1 Parent(s): c451232

Restore yt_live_v2.js for VTV live channels

Browse files
Files changed (1) hide show
  1. static/yt_live_v2.js +72 -0
static/yt_live_v2.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS + EPG ===
2
+ (function(){
3
+ if(window._ytLiveLoaded) return;
4
+ window._ytLiveLoaded = true;
5
+ const CHANNELS = [
6
+ {id:'vtv1',name:'VTV1',badge:'Tin tức'},{id:'vtv2',name:'VTV2',badge:'Khoa học'},{id:'vtv3',name:'VTV3',badge:'Giải trí'},
7
+ {id:'vtv4',name:'VTV4',badge:'Quốc tế'},{id:'vtv5',name:'VTV5',badge:'Miền Nam'},{id:'vtv6',name:'VTV6',badge:'Thanh niên'},
8
+ {id:'vtv7',name:'VTV7',badge:'Giáo dục'},{id:'vtv8',name:'VTV8',badge:'Miền Trung'},{id:'vtv9',name:'VTV9',badge:'Miền Bắc'},
9
+ {id:'vtv10',name:'VTV10',badge:'VTV10'},{id:'vtvprime',name:'VTVPrime',badge:'Prime'},
10
+ ];
11
+ const STREAMS = {};
12
+ let _currentCh = null, _hls = null, _loading = false;
13
+ const s = document.createElement('style');
14
+ s.textContent = '.vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}.vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}.vtv-title{font-size:13px;font-weight:800;color:#00ccff}.vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}@keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}.vtv-tabs{display:flex;gap:3px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;background:#0d1a2a}.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}.vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}.vtv-tab.off{opacity:.35;pointer-events:none}.vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}.vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.vtv-err{display:flex;align-items:center;justify-content:center;height:180px;color:#888;font-size:12px;flex-direction:column;gap:8px}.vtv-err button{background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer}.vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}.vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}@keyframes vtvspin{to{transform:rotate(360deg)}}';
15
+ document.head.appendChild(s);
16
+
17
+ async function loadAllStreams(){
18
+ if(_loading) return; _loading = true;
19
+ const loadEl = document.getElementById('vtv-load');
20
+ if(loadEl) loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang tải danh sách kênh...';
21
+ try { const r = await fetch('/api/vtv/streams',{signal:AbortSignal.timeout(10000)}); if(r.ok){ const data = await r.json(); CHANNELS.forEach(ch => { const info = data[ch.id]; STREAMS[ch.id] = (info && info.stream_url) ? ['/api/proxy/m3u8/vtv?url='+encodeURIComponent(info.stream_url)] : []; }); } } catch(e){}
22
+ CHANNELS.forEach(ch => { const tab = document.getElementById('vtvt-'+ch.id); if(tab){ if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){tab.classList.remove('off');tab.textContent=ch.name;}else{tab.style.opacity='0.35';tab.textContent=ch.name+' ✕';} } });
23
+ _loading = false;
24
+ }
25
+
26
+ function buildBlock(){
27
+ const w = document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
28
+ let tabs = '';
29
+ CHANNELS.forEach(ch => { tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>'; });
30
+ w.innerHTML = '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div><div class="vtv-tabs">'+tabs+'</div><div class="vtv-frame"><div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải danh sách kênh...</div><video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video><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></div>';
31
+ return w;
32
+ }
33
+
34
+ function pinBlock(){
35
+ const h = document.getElementById('view-home');
36
+ if(!h || document.getElementById('vtv-block')) return;
37
+ h.insertBefore(buildBlock(), h.firstChild);
38
+ loadAllStreams().then(() => { const order=['vtv6','vtv1','vtv2','vtv3','vtv4','vtv5','vtv7','vtv8','vtv9','vtv10']; for(const c of order){if(STREAMS[c]&&STREAMS[c].length>0){setTimeout(()=>window._vtvPlay(c),300);return;}} });
39
+ }
40
+
41
+ window._vtvRetry = function(){ if(_currentCh) window._vtvPlay(_currentCh); };
42
+
43
+ window._vtvPlay = function(chId){
44
+ const ch = CHANNELS.find(c => c.id === chId); if(!ch) return;
45
+ _currentCh = chId; document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
46
+ const tab = document.getElementById('vtvt-'+chId); if(tab) tab.classList.add('on');
47
+ const video = document.getElementById('vtv-player'), errEl = document.getElementById('vtv-err'), loadEl = document.getElementById('vtv-load'), errMsg = document.getElementById('vtv-err-msg');
48
+ video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
49
+ loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
50
+ if(_hls){_hls.destroy();_hls=null;}
51
+ const urls = STREAMS[chId] || [];
52
+ if(!urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=ch.name+': Không tìm thấy luồng.';return;}
53
+ _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
54
+ };
55
+
56
+ function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
57
+ if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
58
+ const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
59
+ if(typeof Hls!=='undefined'&&Hls.isSupported()){
60
+ const hls=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20,xhrSetup:function(xhr,url){if(url.includes('fptplay')){xhr.setRequestHeader('Referer','https://fptplay.vn/');xhr.setRequestHeader('Origin','https://fptplay.vn');}}});
61
+ _hls=hls; hls.loadSource(src); hls.attachMedia(video);
62
+ hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
63
+ let ra=0; hls.on(Hls.Events.ERROR,(ev,data)=>{if(data.fatal){if(data.type===Hls.ErrorTypes.NETWORK_ERROR){ra++;if(ra<=3)setTimeout(()=>hls.startLoad(),2000);else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){try{hls.recoverMediaError();}catch(e){}}else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}});
64
+ }else if(video.canPlayType('application/vnd.apple.mpegurl')){
65
+ video.src=src; video.addEventListener('loadedmetadata',()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';},{once:true});
66
+ video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
67
+ }else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Trình duyệt không hỗ trợ HLS';}
68
+ }
69
+
70
+ const orig=window.loadHome;
71
+ if(orig&&!orig.__vtvWrapped){window.loadHome=async function(){const r=await orig.apply(this,arguments);try{pinBlock();}catch(e){}return r;};window.loadHome.__vtvWrapped=true;}
72
+ })();