Spaces:
Running
Running
Fix VTV mapping: VTV6=VTV Cần Thơ, VTV10=VTV10 (not Cần Thơ)
Browse files- static/yt_live.js +143 -2
static/yt_live.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
// === VNEWS — VTV LIVE CHANNELS ===
|
| 2 |
-
// VTV6 = VTV Cần Thơ, VTV10 = VTV10 (riêng biệt)
|
| 3 |
// All streams played directly from user's browser (server IP blocked by fptplay CDN)
|
| 4 |
// Sources: iptv-org M3U, Ykcitus M3U, xemtv.net PHP fallback
|
|
|
|
| 5 |
|
| 6 |
(function(){
|
| 7 |
if(window._ytLiveLoaded) return;
|
|
@@ -31,4 +32,144 @@
|
|
| 31 |
'https://hd.xemtv.net/kenh/vtv3.php',
|
| 32 |
'https://hd.xemtv.net/kenh/vtv4.php',
|
| 33 |
'https://hd.xemtv.net/kenh/vtv5.php',
|
| 34 |
-
'https:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
// === VNEWS — VTV LIVE CHANNELS ===
|
| 2 |
+
// VTV6 = VTV Cần Thơ, VTV10 = VTV10 (riêng biệt, không phải Cần Thơ)
|
| 3 |
// All streams played directly from user's browser (server IP blocked by fptplay CDN)
|
| 4 |
// Sources: iptv-org M3U, Ykcitus M3U, xemtv.net PHP fallback
|
| 5 |
+
// Uses HLS.js with auto-failover
|
| 6 |
|
| 7 |
(function(){
|
| 8 |
if(window._ytLiveLoaded) return;
|
|
|
|
| 32 |
'https://hd.xemtv.net/kenh/vtv3.php',
|
| 33 |
'https://hd.xemtv.net/kenh/vtv4.php',
|
| 34 |
'https://hd.xemtv.net/kenh/vtv5.php',
|
| 35 |
+
'https://hd.xemtv.net/kenh/vtv6.php',
|
| 36 |
+
'https://hd.xemtv.net/kenh/vtv7.php',
|
| 37 |
+
'https://hd.xemtv.net/kenh/vtv8.php',
|
| 38 |
+
'https://hd.xemtv.net/kenh/vtv9.php',
|
| 39 |
+
'https://hd.xemtv.net/kenh/vtv10.php',
|
| 40 |
+
];
|
| 41 |
+
|
| 42 |
+
const STREAMS = {};
|
| 43 |
+
let _currentCh = null;
|
| 44 |
+
let _hls = null;
|
| 45 |
+
let _loading = false;
|
| 46 |
+
|
| 47 |
+
const s=document.createElement('style');
|
| 48 |
+
s.textContent=`
|
| 49 |
+
.vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
|
| 50 |
+
.vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
|
| 51 |
+
.vtv-title{font-size:13px;font-weight:800;color:#00ccff}
|
| 52 |
+
.vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}
|
| 53 |
+
@keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}
|
| 54 |
+
.vtv-tabs{display:flex;gap:3px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;background:#0d1a2a}
|
| 55 |
+
.vtv-tabs::-webkit-scrollbar{display:none}
|
| 56 |
+
.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}
|
| 57 |
+
.vtv-tab:hover{background:#0b4a7a;color:#fff}
|
| 58 |
+
.vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
|
| 59 |
+
.vtv-tab.off{opacity:.35;pointer-events:none}
|
| 60 |
+
.vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
|
| 61 |
+
.vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
|
| 62 |
+
.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}
|
| 63 |
+
.vtv-err button{background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer}
|
| 64 |
+
.vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}
|
| 65 |
+
.vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
|
| 66 |
+
@keyframes vtvspin{to{transform:rotate(360deg)}}
|
| 67 |
+
`;
|
| 68 |
+
document.head.appendChild(s);
|
| 69 |
+
|
| 70 |
+
function parseM3U(text){
|
| 71 |
+
const lines=text.split('\n');const entries=[];let cur=null;
|
| 72 |
+
for(let i=0;i<lines.length;i++){
|
| 73 |
+
const line=lines[i].trim();
|
| 74 |
+
if(line.startsWith('#EXTINF:')){cur={info:line,url:''};const nm=line.match(/,(.+)$/);cur.name=nm?nm[1].trim():'';const tv=line.match(/tvg-id="([^"]*)"/);cur.tvgId=tv?tv[1]:'';}
|
| 75 |
+
else if(line&&!line.startsWith('#')&&cur){cur.url=line;entries.push(cur);cur=null;}
|
| 76 |
+
}
|
| 77 |
+
return entries;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function matchChannel(entry,channel){
|
| 81 |
+
const search=(entry.name+' '+entry.tvgId+' '+entry.info).toLowerCase();
|
| 82 |
+
for(const pat of channel.patterns){
|
| 83 |
+
if(search.includes(pat.toLowerCase())){
|
| 84 |
+
if(channel.id==='vtv5'&&(search.includes('tây')||search.includes('tay')))continue;
|
| 85 |
+
return true;
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
return false;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
async function fetchM3U(url){
|
| 92 |
+
try{const r=await fetch(url,{signal:AbortSignal.timeout(10000)});if(r.ok)return await r.text();}catch(e){}
|
| 93 |
+
return null;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
async function fetchXemtvPhp(phpUrl){
|
| 97 |
+
try{
|
| 98 |
+
const r=await fetch('/api/proxy/page?url='+encodeURIComponent(phpUrl),{signal:AbortSignal.timeout(10000)});
|
| 99 |
+
if(r.ok){const text=await r.text();const m=text.match(/file\s*:\s*['"]([^'"]*\.m3u8[^'"]*)['"]/i);if(m&&m[1].length>20)return m[1];}
|
| 100 |
+
}catch(e){}
|
| 101 |
+
return null;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async function loadAllStreams(){
|
| 105 |
+
if(_loading)return;_loading=true;
|
| 106 |
+
const loadEl=document.getElementById('vtv-load');
|
| 107 |
+
if(loadEl)loadEl.innerHTML='<div class="vtv-spinner"></div>Đang tải danh sách kênh...';
|
| 108 |
+
const results=await Promise.allSettled(M3U_SOURCES.map(src=>fetchM3U(src.url)));
|
| 109 |
+
CHANNELS.forEach(ch=>{STREAMS[ch.id]=[];});
|
| 110 |
+
results.forEach(result=>{
|
| 111 |
+
if(result.status==='fulfilled'&&result.value){
|
| 112 |
+
const entries=parseM3U(result.value);
|
| 113 |
+
CHANNELS.forEach(ch=>{for(const entry of entries){if(matchChannel(entry,ch)){if(entry.url&&!STREAMS[ch.id].includes(entry.url))STREAMS[ch.id].push(entry.url);break;}}});
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
+
await Promise.allSettled(CHANNELS.map(async(ch,idx)=>{
|
| 117 |
+
if(STREAMS[ch.id].length===0&&XEMTV_PHP[idx]){const url=await fetchXemtvPhp(XEMTV_PHP[idx]);if(url)STREAMS[ch.id].push(url);}
|
| 118 |
+
}));
|
| 119 |
+
CHANNELS.forEach(ch=>{const tab=document.getElementById('vtvt-'+ch.id);if(tab){if(STREAMS[ch.id].length>0)tab.classList.remove('off');else{tab.style.opacity='0.35';tab.textContent=ch.name+' ✕';}}});
|
| 120 |
+
_loading=false;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
function buildBlock(){
|
| 124 |
+
const w=document.createElement('div');w.className='vtv-wrap';w.id='vtv-block';
|
| 125 |
+
let tabs='';
|
| 126 |
+
CHANNELS.forEach(ch=>{tabs+='<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';});
|
| 127 |
+
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>';
|
| 128 |
+
return w;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
function pinBlock(){
|
| 132 |
+
const h=document.getElementById('view-home');
|
| 133 |
+
if(!h||document.getElementById('vtv-block'))return;
|
| 134 |
+
h.insertBefore(buildBlock(),h.firstChild);
|
| 135 |
+
loadAllStreams().then(()=>{for(const ch of CHANNELS){if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){setTimeout(()=>window._vtvPlay(ch.id),300);break;}}});
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
window._vtvRetry=function(){if(_currentCh)window._vtvPlay(_currentCh);};
|
| 139 |
+
|
| 140 |
+
window._vtvPlay=function(chId){
|
| 141 |
+
const ch=CHANNELS.find(c=>c.id===chId);if(!ch)return;
|
| 142 |
+
_currentCh=chId;
|
| 143 |
+
document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
|
| 144 |
+
const tab=document.getElementById('vtvt-'+chId);if(tab)tab.classList.add('on');
|
| 145 |
+
const video=document.getElementById('vtv-player');const errEl=document.getElementById('vtv-err');const loadEl=document.getElementById('vtv-load');const errMsg=document.getElementById('vtv-err-msg');
|
| 146 |
+
video.style.display='none';errEl.style.display='none';loadEl.style.display='flex';
|
| 147 |
+
loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
|
| 148 |
+
if(_hls){_hls.destroy();_hls=null;}
|
| 149 |
+
const urls=STREAMS[chId]||[];
|
| 150 |
+
if(urls.length===0){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=ch.name+': Không tìm thấy luồng. Thử lại sau.';return;}
|
| 151 |
+
_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
function _tryPlay(video,urls,idx,name,loadEl,errEl,errMsg){
|
| 155 |
+
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.name+': Tất cả nguồn đều lỗi. Thử lại sau.';return;}
|
| 156 |
+
const src=urls[idx];const label=' ('+(idx+1)+'/'+urls.length+')';
|
| 157 |
+
loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+name+label+'...';
|
| 158 |
+
// All streams played directly from browser (server IP blocked by fptplay CDN)
|
| 159 |
+
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 160 |
+
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');}}});
|
| 161 |
+
_hls=hls;hls.loadSource(src);hls.attachMedia(video);
|
| 162 |
+
hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
|
| 163 |
+
hls.on(Hls.Events.ERROR,(ev,data)=>{if(data.fatal){if(data.type===Hls.ErrorTypes.NETWORK_ERROR){setTimeout(()=>{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},1500);}else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){hls.recoverMediaError();}else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}});
|
| 164 |
+
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 165 |
+
video.src=src;
|
| 166 |
+
video.addEventListener('loadedmetadata',()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';},{once:true});
|
| 167 |
+
video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
|
| 168 |
+
}else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Trình duyệt không hỗ trợ HLS';}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
const orig=window.loadHome;
|
| 172 |
+
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;}
|
| 173 |
+
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>setTimeout(pinBlock,500));
|
| 174 |
+
else setTimeout(pinBlock,500);
|
| 175 |
+
})();
|