Spaces:
Running
Running
Upload static/yt_live.js
Browse files- static/yt_live.js +533 -170
static/yt_live.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
// === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
|
| 2 |
// Default channel: VTV6
|
| 3 |
-
// Features: PiP button, floating mini-player
|
| 4 |
// Fixed: VTV stays at top ONLY when user clicks pin button
|
| 5 |
|
| 6 |
(function(){
|
|
@@ -31,7 +31,7 @@
|
|
| 31 |
let _streamsLoaded = false;
|
| 32 |
let _pipActive = false;
|
| 33 |
let _miniActive = false;
|
| 34 |
-
let _vtvPinned = false;
|
| 35 |
|
| 36 |
// ===== STYLES =====
|
| 37 |
const style = document.createElement('style');
|
|
@@ -95,6 +95,47 @@
|
|
| 95 |
.vtv-wrap.vtv-sticky .vtv-badge{font-size:8px}
|
| 96 |
.vtv-wrap.vtv-sticky .vtv-pin-btn{top:4px;right:6px}
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
/* ===== MINI PLAYER ===== */
|
| 99 |
.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}
|
| 100 |
.vtv-mini.show{display:block}
|
|
@@ -114,6 +155,453 @@
|
|
| 114 |
`;
|
| 115 |
document.head.appendChild(style);
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
// ===== PIN BUTTON =====
|
| 118 |
function setupPinButton(){
|
| 119 |
const block = document.getElementById('vtv-block');
|
|
@@ -121,7 +609,6 @@
|
|
| 121 |
const header = block.querySelector('.vtv-head');
|
| 122 |
if(!header) return;
|
| 123 |
if(document.getElementById('vtv-pin-btn')) return;
|
| 124 |
-
|
| 125 |
const btn = document.createElement('button');
|
| 126 |
btn.id = 'vtv-pin-btn';
|
| 127 |
btn.className = 'vtv-pin-btn';
|
|
@@ -130,15 +617,8 @@
|
|
| 130 |
btn.onclick = function(e){
|
| 131 |
e.stopPropagation();
|
| 132 |
_vtvPinned = !_vtvPinned;
|
| 133 |
-
if(_vtvPinned){
|
| 134 |
-
|
| 135 |
-
btn.classList.add('pinned');
|
| 136 |
-
btn.innerHTML = '📌 Đã ghim';
|
| 137 |
-
} else {
|
| 138 |
-
block.classList.remove('vtv-sticky');
|
| 139 |
-
btn.classList.remove('pinned');
|
| 140 |
-
btn.innerHTML = '📌 Ghim';
|
| 141 |
-
}
|
| 142 |
};
|
| 143 |
header.style.position = 'relative';
|
| 144 |
header.appendChild(btn);
|
|
@@ -175,106 +655,57 @@
|
|
| 175 |
|
| 176 |
function closeMiniPlayer(){
|
| 177 |
const mini = document.getElementById('vtv-mini');
|
| 178 |
-
if(mini){
|
| 179 |
-
const v = document.getElementById('vtv-mini-vid');
|
| 180 |
-
if(v){ v.pause(); v.src = ''; }
|
| 181 |
-
mini.remove();
|
| 182 |
-
}
|
| 183 |
_miniActive = false;
|
| 184 |
}
|
| 185 |
|
| 186 |
-
function expandFromMini(){
|
| 187 |
-
closeMiniPlayer();
|
| 188 |
-
if(typeof switchCat === 'function') switchCat('home');
|
| 189 |
-
}
|
| 190 |
|
| 191 |
function activateMiniPlayer(){
|
| 192 |
if(!_currentCh) return;
|
| 193 |
-
createMiniPlayer();
|
| 194 |
-
_miniActive = true;
|
| 195 |
const mini = document.getElementById('vtv-mini');
|
| 196 |
const miniVid = document.getElementById('vtv-mini-vid');
|
| 197 |
const mainVid = document.getElementById('vtv-player');
|
| 198 |
-
|
| 199 |
if(_hls){
|
| 200 |
const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:10});
|
| 201 |
-
hls.loadSource(STREAMS[_currentCh][0]);
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
miniVid.src = mainVid.src;
|
| 206 |
-
miniVid.play().catch(()=>{});
|
| 207 |
-
}
|
| 208 |
-
|
| 209 |
-
const ch = CHANNELS.find(c=>c.id===_currentCh);
|
| 210 |
-
if(ch) document.getElementById('vtv-mini-ch').textContent = ch.name;
|
| 211 |
-
updateMiniEpg();
|
| 212 |
-
mini.classList.add('show');
|
| 213 |
-
mini.classList.remove('hidden');
|
| 214 |
}
|
| 215 |
|
| 216 |
function updateMiniEpg(){
|
| 217 |
if(!_currentCh) return;
|
| 218 |
fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{
|
| 219 |
-
const progs = d.programs||[];
|
| 220 |
-
const now = progs.find(p=>p.now);
|
| 221 |
document.getElementById('vtv-mini-epg').textContent = now ? now.time+' '+now.title : (progs[0] ? progs[0].time+' '+progs[0].title : '');
|
| 222 |
}).catch(()=>{});
|
| 223 |
}
|
| 224 |
|
| 225 |
async function toggleMiniPiP(){
|
| 226 |
-
const v = document.getElementById('vtv-mini-vid');
|
| 227 |
-
if(
|
| 228 |
-
try{
|
| 229 |
-
if(document.pictureInPictureElement === v){
|
| 230 |
-
await document.exitPictureInPicture();
|
| 231 |
-
} else {
|
| 232 |
-
await v.requestPictureInPicture();
|
| 233 |
-
}
|
| 234 |
-
} catch(e){ alert('Không hỗ trợ PiP'); }
|
| 235 |
}
|
| 236 |
|
| 237 |
// ===== MAIN PLAYER =====
|
| 238 |
async function loadAllStreams(){
|
| 239 |
-
if(_loading) return;
|
| 240 |
-
_loading = true;
|
| 241 |
const el = document.getElementById('vtv-load');
|
| 242 |
if(el) el.innerHTML = '<div class="vtv-spinner"></div>Đang tải kênh...';
|
| 243 |
try{
|
| 244 |
const r = await fetch('/api/vtv/streams', {signal: AbortSignal.timeout(10000)});
|
| 245 |
-
if(r.ok){
|
| 246 |
-
const data = await r.json();
|
| 247 |
-
CHANNELS.forEach(ch=>{
|
| 248 |
-
const info = data[ch.id];
|
| 249 |
-
if(info && info.stream_url){
|
| 250 |
-
let url = info.stream_url;
|
| 251 |
-
if(NEEDS_PROXY.test(url)) url = '/api/proxy/m3u8/vtv?url='+encodeURIComponent(url);
|
| 252 |
-
STREAMS[ch.id] = [url];
|
| 253 |
-
} else {
|
| 254 |
-
STREAMS[ch.id] = [];
|
| 255 |
-
}
|
| 256 |
-
});
|
| 257 |
-
}
|
| 258 |
} catch(e){ console.warn('VTV API error:', e); }
|
| 259 |
-
CHANNELS.forEach(ch=>{
|
| 260 |
-
|
| 261 |
-
if(tab){
|
| 262 |
-
if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){ tab.classList.remove('off'); tab.textContent=ch.name; }
|
| 263 |
-
else { tab.style.opacity='0.35'; tab.textContent=ch.name+' ✕'; }
|
| 264 |
-
}
|
| 265 |
-
});
|
| 266 |
-
_loading = false;
|
| 267 |
-
_streamsLoaded = true;
|
| 268 |
}
|
| 269 |
|
| 270 |
function buildBlock(){
|
| 271 |
const w = document.createElement('div');
|
| 272 |
-
w.className = 'vtv-wrap';
|
| 273 |
-
w.id = 'vtv-block';
|
| 274 |
let tabs = '';
|
| 275 |
-
CHANNELS.forEach(ch=>{
|
| 276 |
-
tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';
|
| 277 |
-
});
|
| 278 |
w.innerHTML =
|
| 279 |
'<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>' +
|
| 280 |
'<div class="vtv-tabs">'+tabs+'</div>' +
|
|
@@ -298,26 +729,19 @@
|
|
| 298 |
}
|
| 299 |
|
| 300 |
async function loadEpg(chId){
|
| 301 |
-
const el = document.getElementById('vtv-epg-list');
|
| 302 |
-
if(!el) return;
|
| 303 |
el.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
|
| 304 |
try{
|
| 305 |
const r = await fetch('/api/vtv/epg/'+chId, {signal: AbortSignal.timeout(10000)});
|
| 306 |
if(!r.ok) throw new Error('fail');
|
| 307 |
-
const data = await r.json();
|
| 308 |
-
const progs = data.programs||[];
|
| 309 |
if(!progs.length){ el.innerHTML='<div class="vtv-epg-empty">Chưa có lịch phát sóng</div>'; return; }
|
| 310 |
let h = '';
|
| 311 |
-
progs.forEach(p=>{
|
| 312 |
-
const nw = p.now ? ' now' : '';
|
| 313 |
-
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>';
|
| 314 |
-
});
|
| 315 |
el.innerHTML = h;
|
| 316 |
const nowItem = el.querySelector('.vtv-epg-item.now');
|
| 317 |
if(nowItem) nowItem.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});
|
| 318 |
-
} catch(e){
|
| 319 |
-
el.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch</div>';
|
| 320 |
-
}
|
| 321 |
}
|
| 322 |
|
| 323 |
window._vtvToggleEpg = function(){
|
|
@@ -328,19 +752,10 @@
|
|
| 328 |
};
|
| 329 |
|
| 330 |
window._vtvTogglePiP = async function(){
|
| 331 |
-
const v = document.getElementById('vtv-player');
|
| 332 |
-
const btn = document.getElementById('vtv-pip-btn');
|
| 333 |
-
if(!v) return;
|
| 334 |
try{
|
| 335 |
-
if(document.pictureInPictureElement === v){
|
| 336 |
-
|
| 337 |
-
_pipActive = false;
|
| 338 |
-
if(btn) btn.classList.remove('on');
|
| 339 |
-
} else {
|
| 340 |
-
await v.requestPictureInPicture();
|
| 341 |
-
_pipActive = true;
|
| 342 |
-
if(btn) btn.classList.add('on');
|
| 343 |
-
}
|
| 344 |
} catch(e){ alert('Không hỗ trợ PiP'); }
|
| 345 |
};
|
| 346 |
|
|
@@ -349,112 +764,58 @@
|
|
| 349 |
if(!h||document.getElementById('vtv-block')) return;
|
| 350 |
h.insertBefore(buildBlock(), h.firstChild);
|
| 351 |
_blockInserted = true;
|
| 352 |
-
|
| 353 |
-
setTimeout(()=>
|
| 354 |
-
if(!_streamsLoaded){
|
| 355 |
-
loadAllStreams().then(()=>{
|
| 356 |
-
setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL), 300);
|
| 357 |
-
});
|
| 358 |
-
}
|
| 359 |
}
|
| 360 |
|
| 361 |
window._vtvRetry = function(){ if(_currentCh) window._vtvPlay(_currentCh); };
|
| 362 |
|
| 363 |
window._vtvPlay = function(chId){
|
| 364 |
-
const ch = CHANNELS.find(c=>c.id===chId);
|
| 365 |
-
if(!ch) return;
|
| 366 |
_currentCh = chId;
|
| 367 |
document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
|
| 368 |
-
const tab = document.getElementById('vtvt-'+chId);
|
| 369 |
-
if(tab) tab.classList.add('on');
|
| 370 |
const video = document.getElementById('vtv-player');
|
| 371 |
-
const errEl = document.getElementById('vtv-err');
|
| 372 |
-
const loadEl = document.getElementById('vtv-load');
|
| 373 |
const errMsg = document.getElementById('vtv-err-msg');
|
| 374 |
-
video.style.display='none';
|
| 375 |
-
errEl.style.display='none';
|
| 376 |
-
loadEl.style.display='flex';
|
| 377 |
loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
|
| 378 |
if(_hls){_hls.destroy(); _hls=null;}
|
| 379 |
const urls = STREAMS[chId]||[];
|
| 380 |
-
if(!urls.length){
|
| 381 |
-
loadEl.style.display='none'; errEl.style.display='flex';
|
| 382 |
-
errMsg.textContent = chId==='vtvprime' ? 'VTVPrime: Kênh trả phí.' : ch.name+': Không có luồng.';
|
| 383 |
-
return;
|
| 384 |
-
}
|
| 385 |
_tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
|
| 386 |
loadEpg(chId);
|
| 387 |
-
if(_miniActive){
|
| 388 |
-
document.getElementById('vtv-mini-ch').textContent = ch.name;
|
| 389 |
-
updateMiniEpg();
|
| 390 |
-
}
|
| 391 |
};
|
| 392 |
|
| 393 |
function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
|
| 394 |
-
if(idx>=urls.length){
|
| 395 |
-
loadEl.style.display='none'; errEl.style.display='flex';
|
| 396 |
-
errMsg.textContent = name+': Tất cả nguồn lỗi.';
|
| 397 |
-
return;
|
| 398 |
-
}
|
| 399 |
const src = urls[idx];
|
| 400 |
loadEl.innerHTML = '<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 401 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 402 |
const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20});
|
| 403 |
-
_hls = hls;
|
| 404 |
-
hls.loadSource(src);
|
| 405 |
-
hls.attachMedia(video);
|
| 406 |
hls.on(Hls.Events.MANIFEST_PARSED,()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; });
|
| 407 |
let rec = 0;
|
| 408 |
-
hls.on(Hls.Events.ERROR,(ev,data)=>{
|
| 409 |
-
if(data.fatal){
|
| 410 |
-
if(data.type===Hls.ErrorTypes.NETWORK_ERROR){
|
| 411 |
-
rec++;
|
| 412 |
-
if(rec<=3) setTimeout(()=>hls.startLoad(), 2000);
|
| 413 |
-
else { hls.destroy(); _hls=null; _tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg); }
|
| 414 |
-
} else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){
|
| 415 |
-
try{hls.recoverMediaError();}catch(e){}
|
| 416 |
-
} else { hls.destroy(); _hls=null; _tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg); }
|
| 417 |
-
}
|
| 418 |
-
});
|
| 419 |
} else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 420 |
video.src = src;
|
| 421 |
video.addEventListener('loadedmetadata',()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; },{once:true});
|
| 422 |
video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
|
| 423 |
-
} else {
|
| 424 |
-
loadEl.style.display='none'; errEl.style.display='flex'; errMsg.textContent='Không hỗ trợ HLS';
|
| 425 |
-
}
|
| 426 |
}
|
| 427 |
|
| 428 |
// ===== NAVIGATION HOOKS =====
|
| 429 |
const _origShowView = window.showView;
|
| 430 |
window.showView = function(id){
|
| 431 |
-
if(id==='view-home' && _miniActive){
|
| 432 |
-
|
| 433 |
-
} else if(id!=='view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive){
|
| 434 |
-
activateMiniPlayer();
|
| 435 |
-
}
|
| 436 |
return _origShowView.apply(this, arguments);
|
| 437 |
};
|
| 438 |
-
|
| 439 |
const _origReadArticle = window.readArticle;
|
| 440 |
-
if(_origReadArticle){
|
| 441 |
-
window.readArticle = function(url){
|
| 442 |
-
if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive) activateMiniPlayer();
|
| 443 |
-
return _origReadArticle.apply(this, arguments);
|
| 444 |
-
};
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
const _origSwitchCat = window.switchCat;
|
| 448 |
-
if(_origSwitchCat){
|
| 449 |
-
window.switchCat = function(id){
|
| 450 |
-
if(id==='home'){
|
| 451 |
-
if(_miniActive) closeMiniPlayer();
|
| 452 |
-
} else if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive){
|
| 453 |
-
activateMiniPlayer();
|
| 454 |
-
}
|
| 455 |
-
return _origSwitchCat.apply(this, arguments);
|
| 456 |
-
};
|
| 457 |
-
}
|
| 458 |
|
| 459 |
// Override loadHome to inject VTV block
|
| 460 |
const _origLoadHome = window.loadHome;
|
|
@@ -469,4 +830,6 @@
|
|
| 469 |
};
|
| 470 |
window.loadHome.__vtvWrapped = true;
|
| 471 |
}
|
|
|
|
|
|
|
| 472 |
})();
|
|
|
|
| 1 |
// === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
|
| 2 |
// Default channel: VTV6
|
| 3 |
+
// Features: PiP button, floating mini-player, INLINE RECORDER
|
| 4 |
// Fixed: VTV stays at top ONLY when user clicks pin button
|
| 5 |
|
| 6 |
(function(){
|
|
|
|
| 31 |
let _streamsLoaded = false;
|
| 32 |
let _pipActive = false;
|
| 33 |
let _miniActive = false;
|
| 34 |
+
let _vtvPinned = false;
|
| 35 |
|
| 36 |
// ===== STYLES =====
|
| 37 |
const style = document.createElement('style');
|
|
|
|
| 95 |
.vtv-wrap.vtv-sticky .vtv-badge{font-size:8px}
|
| 96 |
.vtv-wrap.vtv-sticky .vtv-pin-btn{top:4px;right:6px}
|
| 97 |
|
| 98 |
+
/* ===== INLINE RECORDER ===== */
|
| 99 |
+
.vtv-rec-btn{background:#660000;border:1px solid #990000;color:#ff6666;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px;font-weight:700}
|
| 100 |
+
.vtv-rec-btn:hover{background:#990000;color:#fff}
|
| 101 |
+
.vtv-rec-btn.recording{background:#cc0000;border-color:#ff0000;color:#fff;animation:vtv-rec-pulse 1s infinite}
|
| 102 |
+
@keyframes vtv-rec-pulse{0%,100%{opacity:1}50%{opacity:.5}}
|
| 103 |
+
.vtv-rec-btn svg{width:12px;height:12px;fill:currentColor}
|
| 104 |
+
|
| 105 |
+
.vtv-inline-rec{display:none;padding:8px 10px;background:#0a0a1a;border-top:1px solid #1a1a3a}
|
| 106 |
+
.vtv-inline-rec.show{display:block}
|
| 107 |
+
.vtv-inline-rec .rec-bar{position:relative;height:32px;background:#000;border-radius:4px;overflow:hidden;cursor:pointer;border:1px solid #2a2a4a;margin-bottom:6px}
|
| 108 |
+
.vtv-inline-rec .rec-bar .rec-progress{position:absolute;top:0;bottom:0;background:rgba(155,89,182,.3);left:0;width:0%}
|
| 109 |
+
.vtv-inline-rec .rec-bar .rec-marker{position:absolute;top:0;bottom:0;width:3px;z-index:2}
|
| 110 |
+
.vtv-inline-rec .rec-bar .rec-marker.s{background:#2ecc71}
|
| 111 |
+
.vtv-inline-rec .rec-bar .rec-marker.e{background:#e74c3c}
|
| 112 |
+
.vtv-inline-rec .rec-time{display:flex;justify-content:space-between;font-size:9px;color:#888;margin-bottom:6px}
|
| 113 |
+
.vtv-inline-rec .rec-controls{display:flex;gap:4px}
|
| 114 |
+
.vtv-inline-rec .rec-controls button{flex:1;padding:6px;border:none;border-radius:6px;font-size:10px;font-weight:700;cursor:pointer}
|
| 115 |
+
.vtv-inline-rec .rec-controls .rec-set-start{background:#1a3a1a;border:1px solid #2d6a2d;color:#5cb87a}
|
| 116 |
+
.vtv-inline-rec .rec-controls .rec-set-end{background:#3a1a1a;border:1px solid #6a2d2d;color:#e74c3c}
|
| 117 |
+
.vtv-inline-rec .rec-controls .rec-go{background:#1a0a3a;border:1px solid #3a2a6a;color:#9b59b6}
|
| 118 |
+
.vtv-inline-rec .rec-controls .rec-reset{background:#222;border:1px solid #333;color:#888}
|
| 119 |
+
.vtv-inline-rec .rec-hint{font-size:9px;color:#666;text-align:center;margin-top:4px}
|
| 120 |
+
.vtv-inline-rec .rec-status{font-size:10px;color:#888;text-align:center;padding:4px;margin-top:4px;background:#111;border-radius:4px}
|
| 121 |
+
.vtv-inline-rec .rec-status.ok{color:#2ecc71}
|
| 122 |
+
.vtv-inline-rec .rec-status.err{color:#e74c3c}
|
| 123 |
+
.vtv-inline-rec .rec-status.recording{color:#e74c3c;animation:vtv-rec-pulse 1s infinite}
|
| 124 |
+
|
| 125 |
+
.vtv-rec-panel{display:none;padding:10px;background:#0d0d20;border-top:1px solid #2a1a4a}
|
| 126 |
+
.vtv-rec-panel.show{display:block}
|
| 127 |
+
.vtv-rec-panel .rec-panel-title{font-size:11px;font-weight:700;color:#9b59b6;margin-bottom:8px;display:flex;align-items:center;gap:6px}
|
| 128 |
+
.vtv-rec-panel .rec-ratio-row{display:flex;gap:4px;margin-bottom:8px}
|
| 129 |
+
.vtv-rec-panel .rec-ratio-row button{flex:1;padding:6px;background:#1a1a2e;border:1px solid #2a2a4a;border-radius:6px;color:#888;font-size:10px;cursor:pointer}
|
| 130 |
+
.vtv-rec-panel .rec-ratio-row button.active{border-color:#9b59b6;color:#9b59b6;background:#2a1a4a}
|
| 131 |
+
.vtv-rec-panel .rec-actions{display:flex;gap:4px}
|
| 132 |
+
.vtv-rec-panel .rec-actions button{flex:1;padding:8px;border:none;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer}
|
| 133 |
+
.vtv-rec-panel .rec-actions .rec-download{background:#2d8659;color:#fff}
|
| 134 |
+
.vtv-rec-panel .rec-actions .rec-share{background:#9b59b6;color:#fff}
|
| 135 |
+
.vtv-rec-panel .rec-preview-wrap{margin-top:8px;text-align:center}
|
| 136 |
+
.vtv-rec-panel .rec-preview-wrap video{max-width:100%;max-height:200px;border-radius:6px;background:#000}
|
| 137 |
+
.vtv-rec-panel .rec-processing{text-align:center;padding:12px;color:#9b59b6;font-size:12px}
|
| 138 |
+
|
| 139 |
/* ===== MINI PLAYER ===== */
|
| 140 |
.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}
|
| 141 |
.vtv-mini.show{display:block}
|
|
|
|
| 155 |
`;
|
| 156 |
document.head.appendChild(style);
|
| 157 |
|
| 158 |
+
// ===== INLINE RECORDER STATE =====
|
| 159 |
+
let _recState = {
|
| 160 |
+
active: false,
|
| 161 |
+
startTime: null,
|
| 162 |
+
endTime: null,
|
| 163 |
+
isRecording: false,
|
| 164 |
+
recorder: null,
|
| 165 |
+
chunks: [],
|
| 166 |
+
blob: null,
|
| 167 |
+
ratio: 'original',
|
| 168 |
+
_dragMarker: null,
|
| 169 |
+
_timer: null,
|
| 170 |
+
_recTimer: null,
|
| 171 |
+
};
|
| 172 |
+
|
| 173 |
+
// ===== INLINE RECORDER UI =====
|
| 174 |
+
function buildInlineRecorder() {
|
| 175 |
+
const controls = document.querySelector('.vtv-controls');
|
| 176 |
+
if (!controls || document.getElementById('vtv-rec-btn')) return;
|
| 177 |
+
|
| 178 |
+
// Record button in controls bar
|
| 179 |
+
const recBtn = document.createElement('button');
|
| 180 |
+
recBtn.id = 'vtv-rec-btn';
|
| 181 |
+
recBtn.className = 'vtv-rec-btn';
|
| 182 |
+
recBtn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
|
| 183 |
+
recBtn.title = 'Record video ngắn từ LIVE (max 60s)';
|
| 184 |
+
recBtn.onclick = toggleInlineRecorder;
|
| 185 |
+
controls.appendChild(recBtn);
|
| 186 |
+
|
| 187 |
+
// Inline recorder panel
|
| 188 |
+
const inlineRec = document.createElement('div');
|
| 189 |
+
inlineRec.id = 'vtv-inline-rec';
|
| 190 |
+
inlineRec.className = 'vtv-inline-rec';
|
| 191 |
+
inlineRec.innerHTML = `
|
| 192 |
+
<div class="rec-time">
|
| 193 |
+
<span id="vtv-rec-time-start">00:00</span>
|
| 194 |
+
<span id="vtv-rec-time-dur">Duration: 0s</span>
|
| 195 |
+
<span id="vtv-rec-time-end">01:00</span>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="rec-bar" id="vtv-rec-bar">
|
| 198 |
+
<div class="rec-progress" id="vtv-rec-progress"></div>
|
| 199 |
+
<div class="rec-marker s" id="vtv-rec-mk-start" style="display:none"></div>
|
| 200 |
+
<div class="rec-marker e" id="vtv-rec-mk-end" style="display:none"></div>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="rec-controls">
|
| 203 |
+
<button class="rec-set-start" id="vtv-rec-set-start" onclick="window._vtvRecSetStart()">🔵 Điểm đầu</button>
|
| 204 |
+
<button class="rec-set-end" id="vtv-rec-set-end" onclick="window._vtvRecSetEnd()" disabled>🔴 Điểm cuối</button>
|
| 205 |
+
<button class="rec-go" id="vtv-rec-go" onclick="window._vtvRecGo()" disabled>⏺ Record!</button>
|
| 206 |
+
<button class="rec-reset" onclick="window._vtvRecReset()">↺ Reset</button>
|
| 207 |
+
</div>
|
| 208 |
+
<div class="rec-hint" id="vtv-rec-hint">Chọn điểm bắt đầu → kết thúc (tối đa 60s)</div>
|
| 209 |
+
<div class="rec-status" id="vtv-rec-status">Sẵn sàng</div>
|
| 210 |
+
`;
|
| 211 |
+
controls.parentNode.insertBefore(inlineRec, controls.nextSibling);
|
| 212 |
+
|
| 213 |
+
// Post-record panel (ratio + download/share)
|
| 214 |
+
const recPanel = document.createElement('div');
|
| 215 |
+
recPanel.id = 'vtv-rec-panel';
|
| 216 |
+
recPanel.className = 'vtv-rec-panel';
|
| 217 |
+
recPanel.innerHTML = `
|
| 218 |
+
<div class="rec-panel-title">🎬 Đã record xong!</div>
|
| 219 |
+
<div class="rec-preview-wrap" id="vtv-rec-preview-wrap" style="display:none">
|
| 220 |
+
<video id="vtv-rec-preview-vid" controls style="max-width:100%;max-height:180px;border-radius:6px;background:#000"></video>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="rec-ratio-row">
|
| 223 |
+
<button class="active" data-ratio="original" onclick="window._vtvRecSetRatio('original')">📺 Gốc</button>
|
| 224 |
+
<button data-ratio="1:1" onclick="window._vtvRecSetRatio('1:1')">⬜ 1:1</button>
|
| 225 |
+
<button data-ratio="9:16" onclick="window._vtvRecSetRatio('9:16')">📱 9:16</button>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="rec-actions">
|
| 228 |
+
<button class="rec-download" onclick="window._vtvRecDownload()">💾 Lưu về máy</button>
|
| 229 |
+
<button class="rec-share" onclick="window._vtvRecShare()">🎬 Tạo Short AI</button>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="rec-processing" id="vtv-rec-proc" style="display:none">
|
| 232 |
+
<div class="vtv-spinner" style="width:20px;height:20px;margin:0 auto 6px"></div>
|
| 233 |
+
<span id="vtv-rec-proc-text">Đang xử lý...</span>
|
| 234 |
+
</div>
|
| 235 |
+
`;
|
| 236 |
+
inlineRec.parentNode.insertBefore(recPanel, inlineRec.nextSibling);
|
| 237 |
+
|
| 238 |
+
// Timeline click
|
| 239 |
+
document.getElementById('vtv-rec-bar').addEventListener('click', function(e) {
|
| 240 |
+
const video = document.getElementById('vtv-player');
|
| 241 |
+
if (!video || !video.src) { recStatus('⚠️ Chưa có stream!'); return; }
|
| 242 |
+
const rect = this.getBoundingClientRect();
|
| 243 |
+
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
| 244 |
+
const time = pct * 60; // max 60s
|
| 245 |
+
|
| 246 |
+
if (_recState.startTime === null) {
|
| 247 |
+
_recState.startTime = time;
|
| 248 |
+
const mk = document.getElementById('vtv-rec-mk-start');
|
| 249 |
+
mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
|
| 250 |
+
document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
|
| 251 |
+
document.getElementById('vtv-rec-set-end').disabled = false;
|
| 252 |
+
recStatus('Điểm đầu: ' + fmtSec(time) + '. Chọn điểm cuối.', '');
|
| 253 |
+
} else if (_recState.endTime === null) {
|
| 254 |
+
if (time <= _recState.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
|
| 255 |
+
const dur = time - _recState.startTime;
|
| 256 |
+
if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
|
| 257 |
+
_recState.endTime = time;
|
| 258 |
+
const mk = document.getElementById('vtv-rec-mk-end');
|
| 259 |
+
mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
|
| 260 |
+
document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
|
| 261 |
+
updateRecProgress();
|
| 262 |
+
recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
|
| 263 |
+
document.getElementById('vtv-rec-go').disabled = false;
|
| 264 |
+
}
|
| 265 |
+
});
|
| 266 |
+
|
| 267 |
+
// Drag markers
|
| 268 |
+
setupRecDrag('vtv-rec-mk-start', 'start');
|
| 269 |
+
setupRecDrag('vtv-rec-mk-end', 'end');
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function setupRecDrag(mkId, which) {
|
| 273 |
+
const mk = document.getElementById(mkId);
|
| 274 |
+
if (!mk) return;
|
| 275 |
+
mk.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); _recState._dragMarker = which; });
|
| 276 |
+
mk.addEventListener('touchstart', function(e) { e.preventDefault(); e.stopPropagation(); _recState._dragMarker = which; }, {passive:false});
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
// Global drag handlers
|
| 280 |
+
document.addEventListener('mousemove', function(e) {
|
| 281 |
+
if (!_recState._dragMarker) return;
|
| 282 |
+
const bar = document.getElementById('vtv-rec-bar');
|
| 283 |
+
if (!bar) return;
|
| 284 |
+
const rect = bar.getBoundingClientRect();
|
| 285 |
+
const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
| 286 |
+
const time = pct * 60;
|
| 287 |
+
const mkId = _recState._dragMarker === 'start' ? 'vtv-rec-mk-start' : 'vtv-rec-mk-end';
|
| 288 |
+
const mk = document.getElementById(mkId);
|
| 289 |
+
if (!mk) return;
|
| 290 |
+
if (_recState._dragMarker === 'start') {
|
| 291 |
+
if (_recState.endTime !== null && time >= _recState.endTime - 1) return;
|
| 292 |
+
_recState.startTime = time;
|
| 293 |
+
mk.style.left = (pct * 100) + '%';
|
| 294 |
+
document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
|
| 295 |
+
} else {
|
| 296 |
+
if (_recState.startTime !== null && time <= _recState.startTime + 1) return;
|
| 297 |
+
_recState.endTime = time;
|
| 298 |
+
mk.style.left = (pct * 100) + '%';
|
| 299 |
+
document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
|
| 300 |
+
}
|
| 301 |
+
updateRecProgress();
|
| 302 |
+
});
|
| 303 |
+
document.addEventListener('touchmove', function(e) {
|
| 304 |
+
if (!_recState._dragMarker) return;
|
| 305 |
+
const bar = document.getElementById('vtv-rec-bar');
|
| 306 |
+
if (!bar) return;
|
| 307 |
+
const rect = bar.getBoundingClientRect();
|
| 308 |
+
const pct = Math.max(0, Math.min(1, (e.touches[0].clientX - rect.left) / rect.width));
|
| 309 |
+
const time = pct * 60;
|
| 310 |
+
const mkId = _recState._dragMarker === 'start' ? 'vtv-rec-mk-start' : 'vtv-rec-mk-end';
|
| 311 |
+
const mk = document.getElementById(mkId);
|
| 312 |
+
if (!mk) return;
|
| 313 |
+
if (_recState._dragMarker === 'start') {
|
| 314 |
+
if (_recState.endTime !== null && time >= _recState.endTime - 1) return;
|
| 315 |
+
_recState.startTime = time; mk.style.left = (pct * 100) + '%';
|
| 316 |
+
document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
|
| 317 |
+
} else {
|
| 318 |
+
if (_recState.startTime !== null && time <= _recState.startTime + 1) return;
|
| 319 |
+
_recState.endTime = time; mk.style.left = (pct * 100) + '%';
|
| 320 |
+
document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
|
| 321 |
+
}
|
| 322 |
+
updateRecProgress();
|
| 323 |
+
}, {passive:false});
|
| 324 |
+
document.addEventListener('mouseup', function() { _recState._dragMarker = null; });
|
| 325 |
+
document.addEventListener('touchend', function() { _recState._dragMarker = null; });
|
| 326 |
+
|
| 327 |
+
function updateRecProgress() {
|
| 328 |
+
const s = _recState.startTime || 0;
|
| 329 |
+
const e = _recState.endTime || 60;
|
| 330 |
+
const dur = e - s;
|
| 331 |
+
document.getElementById('vtv-rec-progress').style.left = (s/60*100) + '%';
|
| 332 |
+
document.getElementById('vtv-rec-progress').style.width = (dur/60*100) + '%';
|
| 333 |
+
document.getElementById('vtv-rec-time-dur').textContent = 'Duration: ' + Math.round(dur) + 's';
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
function recStatus(msg, type) {
|
| 337 |
+
const el = document.getElementById('vtv-rec-status');
|
| 338 |
+
if (!el) return;
|
| 339 |
+
el.textContent = msg;
|
| 340 |
+
el.className = 'rec-status' + (type ? ' ' + type : '');
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// ===== INLINE RECORDER CONTROLS =====
|
| 344 |
+
window._vtvRecSetStart = function() {
|
| 345 |
+
const video = document.getElementById('vtv-player');
|
| 346 |
+
if (!video || !video.src) { recStatus('⚠️ Chưa có stream!', 'err'); return; }
|
| 347 |
+
const cur = video.currentTime || 0;
|
| 348 |
+
_recState.startTime = cur;
|
| 349 |
+
const pct = cur / 60 * 100;
|
| 350 |
+
const mk = document.getElementById('vtv-rec-mk-start');
|
| 351 |
+
mk.style.display = 'block'; mk.style.left = pct + '%';
|
| 352 |
+
document.getElementById('vtv-rec-time-start').textContent = fmtSec(cur);
|
| 353 |
+
document.getElementById('vtv-rec-set-end').disabled = false;
|
| 354 |
+
recStatus('Điểm đầu: ' + fmtSec(cur), '');
|
| 355 |
+
};
|
| 356 |
+
|
| 357 |
+
window._vtvRecSetEnd = function() {
|
| 358 |
+
if (_recState.startTime === null) { recStatus('⚠️ Chọn điểm đầu trước!', 'err'); return; }
|
| 359 |
+
const video = document.getElementById('vtv-player');
|
| 360 |
+
if (!video) return;
|
| 361 |
+
const cur = video.currentTime || 0;
|
| 362 |
+
if (cur <= _recState.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
|
| 363 |
+
const dur = cur - _recState.startTime;
|
| 364 |
+
if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
|
| 365 |
+
_recState.endTime = cur;
|
| 366 |
+
const pct = cur / 60 * 100;
|
| 367 |
+
const mk = document.getElementById('vtv-rec-mk-end');
|
| 368 |
+
mk.style.display = 'block'; mk.style.left = pct + '%';
|
| 369 |
+
document.getElementById('vtv-rec-time-end').textContent = fmtSec(cur);
|
| 370 |
+
updateRecProgress();
|
| 371 |
+
recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
|
| 372 |
+
document.getElementById('vtv-rec-go').disabled = false;
|
| 373 |
+
};
|
| 374 |
+
|
| 375 |
+
window._vtvRecReset = function() {
|
| 376 |
+
_recState.startTime = null; _recState.endTime = null;
|
| 377 |
+
_recState.isRecording = false; _recState.blob = null;
|
| 378 |
+
if (_recState.recorder && _recState.recorder.state !== 'inactive') _recState.recorder.stop();
|
| 379 |
+
clearTimeout(_recState._recTimer);
|
| 380 |
+
document.getElementById('vtv-rec-mk-start').style.display = 'none';
|
| 381 |
+
document.getElementById('vtv-rec-mk-end').style.display = 'none';
|
| 382 |
+
document.getElementById('vtv-rec-progress').style.width = '0%';
|
| 383 |
+
document.getElementById('vtv-rec-time-start').textContent = '00:00';
|
| 384 |
+
document.getElementById('vtv-rec-time-end').textContent = '01:00';
|
| 385 |
+
document.getElementById('vtv-rec-time-dur').textContent = 'Duration: 0s';
|
| 386 |
+
document.getElementById('vtv-rec-set-start').disabled = false;
|
| 387 |
+
document.getElementById('vtv-rec-set-end').disabled = true;
|
| 388 |
+
document.getElementById('vtv-rec-go').disabled = true;
|
| 389 |
+
document.getElementById('vtv-rec-panel').classList.remove('show');
|
| 390 |
+
recStatus('Đã reset.', '');
|
| 391 |
+
// Reset rec button
|
| 392 |
+
const btn = document.getElementById('vtv-rec-btn');
|
| 393 |
+
if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
|
| 394 |
+
};
|
| 395 |
+
|
| 396 |
+
window._vtvRecGo = function() {
|
| 397 |
+
if (_recState.startTime === null || _recState.endTime === null) return;
|
| 398 |
+
if (_recState.isRecording) {
|
| 399 |
+
// Stop
|
| 400 |
+
if (_recState.recorder && _recState.recorder.state !== 'inactive') _recState.recorder.stop();
|
| 401 |
+
_recState.isRecording = false;
|
| 402 |
+
clearTimeout(_recState._recTimer);
|
| 403 |
+
const btn = document.getElementById('vtv-rec-btn');
|
| 404 |
+
if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
|
| 405 |
+
return;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
const video = document.getElementById('vtv-player');
|
| 409 |
+
if (!video) { recStatus('⚠️ Không tìm thấy video!', 'err'); return; }
|
| 410 |
+
|
| 411 |
+
_recState.chunks = [];
|
| 412 |
+
_recState.isRecording = true;
|
| 413 |
+
|
| 414 |
+
let mimeType = 'video/webm;codecs=vp9,opus';
|
| 415 |
+
if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm;codecs=vp8,opus';
|
| 416 |
+
if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm';
|
| 417 |
+
|
| 418 |
+
try {
|
| 419 |
+
const stream = video.captureStream ? video.captureStream() : video.mozCaptureStream();
|
| 420 |
+
if (!stream) { recStatus('❌ Không hỗ trợ captureStream!', 'err'); _recState.isRecording = false; return; }
|
| 421 |
+
_recState.recorder = new MediaRecorder(stream, {mimeType});
|
| 422 |
+
} catch(e) { recStatus('❌ Lỗi: ' + e.message, 'err'); _recState.isRecording = false; return; }
|
| 423 |
+
|
| 424 |
+
_recState.recorder.ondataavailable = e => { if(e.data && e.data.size > 0) _recState.chunks.push(e.data); };
|
| 425 |
+
_recState.recorder.onstop = function() {
|
| 426 |
+
_recState.isRecording = false;
|
| 427 |
+
_recState.blob = new Blob(_recState.chunks, {type: 'video/webm'});
|
| 428 |
+
const btn = document.getElementById('vtv-rec-btn');
|
| 429 |
+
if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
|
| 430 |
+
// Show panel
|
| 431 |
+
document.getElementById('vtv-rec-panel').classList.add('show');
|
| 432 |
+
const pv = document.getElementById('vtv-rec-preview-vid');
|
| 433 |
+
pv.src = URL.createObjectURL(_recState.blob);
|
| 434 |
+
document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
|
| 435 |
+
recStatus('✅ Record xong! ' + (_recState.blob.size/1024/1024).toFixed(1) + 'MB', 'ok');
|
| 436 |
+
};
|
| 437 |
+
_recState.recorder.onerror = e => { recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err'); _recState.isRecording = false; };
|
| 438 |
+
|
| 439 |
+
_recState.recorder.start(1000);
|
| 440 |
+
video.currentTime = _recState.startTime;
|
| 441 |
+
|
| 442 |
+
const btn = document.getElementById('vtv-rec-btn');
|
| 443 |
+
if (btn) { btn.classList.add('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> Dừng'; }
|
| 444 |
+
|
| 445 |
+
recStatus('🔴 ĐANG RECORD...', 'recording');
|
| 446 |
+
|
| 447 |
+
const dur = (_recState.endTime - _recState.startTime) * 1000;
|
| 448 |
+
_recState._recTimer = setTimeout(() => {
|
| 449 |
+
if (_recState.isRecording) {
|
| 450 |
+
if (_recState.recorder && _recState.recorder.state !== 'inactive') _recState.recorder.stop();
|
| 451 |
+
_recState.isRecording = false;
|
| 452 |
+
}
|
| 453 |
+
}, dur + 500);
|
| 454 |
+
};
|
| 455 |
+
|
| 456 |
+
window._vtvRecSetRatio = function(ratio) {
|
| 457 |
+
_recState.ratio = ratio;
|
| 458 |
+
document.querySelectorAll('#vtv-rec-panel .rec-ratio-row button').forEach(b => {
|
| 459 |
+
b.classList.toggle('active', b.dataset.ratio === ratio);
|
| 460 |
+
});
|
| 461 |
+
};
|
| 462 |
+
|
| 463 |
+
window._vtvRecDownload = function() {
|
| 464 |
+
if (!_recState.blob) return;
|
| 465 |
+
if (_recState.ratio === 'original') {
|
| 466 |
+
downloadBlob(_recState.blob, `vtv-${_currentCh}-${Date.now()}.webm`);
|
| 467 |
+
} else {
|
| 468 |
+
applyRatioProcess(_recState.blob, _recState.ratio, 'download');
|
| 469 |
+
}
|
| 470 |
+
};
|
| 471 |
+
|
| 472 |
+
window._vtvRecShare = function() {
|
| 473 |
+
if (!_recState.blob) return;
|
| 474 |
+
if (_recState.ratio !== 'original') {
|
| 475 |
+
applyRatioProcess(_recState.blob, _recState.ratio, 'upload');
|
| 476 |
+
} else {
|
| 477 |
+
uploadToServer(_recState.blob);
|
| 478 |
+
}
|
| 479 |
+
};
|
| 480 |
+
|
| 481 |
+
function downloadBlob(blob, filename) {
|
| 482 |
+
const a = document.createElement('a');
|
| 483 |
+
a.href = URL.createObjectURL(blob); a.download = filename; a.click();
|
| 484 |
+
URL.revokeObjectURL(a.href);
|
| 485 |
+
recStatus('✅ Đã tải: ' + filename, 'ok');
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
function uploadToServer(blob) {
|
| 489 |
+
const formData = new FormData();
|
| 490 |
+
formData.append('video', blob, `vtv-${_currentCh}-${Date.now()}.webm`);
|
| 491 |
+
formData.append('title', `Record LIVE ${_currentCh?.toUpperCase()} - ${new Date().toLocaleString('vi-VN')}`);
|
| 492 |
+
formData.append('source', 'vtv_inline_recorder');
|
| 493 |
+
showRecProc('⏳ Đang upload...');
|
| 494 |
+
fetch('/api/wall', { method: 'POST', body: formData })
|
| 495 |
+
.then(r => r.json())
|
| 496 |
+
.then(data => {
|
| 497 |
+
hideRecProc();
|
| 498 |
+
if (data.ok) {
|
| 499 |
+
recStatus('✅ Đã tạo Short AI! Xem trong Tường AI.', 'ok');
|
| 500 |
+
} else {
|
| 501 |
+
recStatus('⚠️ ' + (data.error || 'Upload lỗi') + '. Đang tải về...', 'err');
|
| 502 |
+
setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
|
| 503 |
+
}
|
| 504 |
+
})
|
| 505 |
+
.catch(e => {
|
| 506 |
+
hideRecProc();
|
| 507 |
+
recStatus('⚠️ Mất kết nối. Đang tải về...', 'err');
|
| 508 |
+
setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
|
| 509 |
+
});
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
function showRecProc(text) {
|
| 513 |
+
document.getElementById('vtv-rec-proc').style.display = 'block';
|
| 514 |
+
document.getElementById('vtv-rec-proc-text').textContent = text;
|
| 515 |
+
}
|
| 516 |
+
function hideRecProc() {
|
| 517 |
+
document.getElementById('vtv-rec-proc').style.display = 'none';
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function applyRatioProcess(blob, ratio, action) {
|
| 521 |
+
showRecProc(`🔄 Đang xử lý ${ratio}...`);
|
| 522 |
+
const video = document.createElement('video');
|
| 523 |
+
video.src = URL.createObjectURL(blob);
|
| 524 |
+
video.muted = true;
|
| 525 |
+
video.preload = 'auto';
|
| 526 |
+
|
| 527 |
+
video.onloadedmetadata = function() {
|
| 528 |
+
const vw = video.videoWidth, vh = video.videoHeight;
|
| 529 |
+
let cw, ch, sx, sy;
|
| 530 |
+
if (ratio === '1:1') {
|
| 531 |
+
const size = Math.min(vw, vh); cw = ch = size;
|
| 532 |
+
sx = (vw - size) / 2; sy = (vh - size) / 2;
|
| 533 |
+
} else if (ratio === '9:16') {
|
| 534 |
+
ch = vh; cw = Math.min(vw, Math.round(vh * 9 / 16));
|
| 535 |
+
sx = Math.floor((vw - cw) / 2); sy = 0;
|
| 536 |
+
} else { cw = vw; ch = vh; sx = sy = 0; }
|
| 537 |
+
|
| 538 |
+
const canvas = document.createElement('canvas');
|
| 539 |
+
canvas.width = cw; canvas.height = ch;
|
| 540 |
+
const ctx = canvas.getContext('2d');
|
| 541 |
+
const canvasStream = canvas.captureStream(30);
|
| 542 |
+
|
| 543 |
+
let combinedStream = canvasStream;
|
| 544 |
+
try {
|
| 545 |
+
const vs = video.captureStream ? video.captureStream() : null;
|
| 546 |
+
if (vs) {
|
| 547 |
+
const at = vs.getAudioTracks();
|
| 548 |
+
if (at.length > 0) combinedStream = new MediaStream([...canvasStream.getVideoTracks(), ...at]);
|
| 549 |
+
}
|
| 550 |
+
} catch(e) {}
|
| 551 |
+
|
| 552 |
+
const mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus') ? 'video/webm;codecs=vp9,opus' : 'video/webm';
|
| 553 |
+
let rec;
|
| 554 |
+
try { rec = new MediaRecorder(combinedStream, {mimeType: mime}); }
|
| 555 |
+
catch(e) { hideRecProc(); recStatus('❌ Lỗi: ' + e.message, 'err'); return; }
|
| 556 |
+
|
| 557 |
+
const chunks = [];
|
| 558 |
+
rec.ondataavailable = e => { if(e.data.size > 0) chunks.push(e.data); };
|
| 559 |
+
rec.onstop = () => {
|
| 560 |
+
hideRecProc();
|
| 561 |
+
const out = new Blob(chunks, {type: 'video/webm'});
|
| 562 |
+
URL.revokeObjectURL(video.src);
|
| 563 |
+
if (action === 'download') {
|
| 564 |
+
downloadBlob(out, `vtv-${_currentCh}-${ratio.replace(':','x')}-${Date.now()}.webm`);
|
| 565 |
+
} else {
|
| 566 |
+
uploadToServer(out);
|
| 567 |
+
}
|
| 568 |
+
};
|
| 569 |
+
rec.start(1000);
|
| 570 |
+
video.currentTime = 0;
|
| 571 |
+
video.play().then(() => {
|
| 572 |
+
function draw() {
|
| 573 |
+
if (video.paused || video.ended) { rec.stop(); return; }
|
| 574 |
+
ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch);
|
| 575 |
+
requestAnimationFrame(draw);
|
| 576 |
+
}
|
| 577 |
+
draw();
|
| 578 |
+
setTimeout(() => { if(rec.state !== 'inactive') rec.stop(); }, video.duration * 1000 + 500);
|
| 579 |
+
}).catch(e => { hideRecProc(); recStatus('❌ ' + e.message, 'err'); });
|
| 580 |
+
};
|
| 581 |
+
video.onerror = function() { hideRecProc(); recStatus('❌ Không đọc được video.', 'err'); };
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
window._vtvRecToggle = toggleInlineRecorder;
|
| 585 |
+
|
| 586 |
+
function toggleInlineRecorder() {
|
| 587 |
+
const panel = document.getElementById('vtv-inline-rec');
|
| 588 |
+
if (!panel) { buildInlineRecorder(); setTimeout(toggleInlineRecorder, 200); return; }
|
| 589 |
+
_recState.active = !_recState.active;
|
| 590 |
+
panel.classList.toggle('show', _recState.active);
|
| 591 |
+
const btn = document.getElementById('vtv-rec-btn');
|
| 592 |
+
if (btn) {
|
| 593 |
+
if (_recState.active) {
|
| 594 |
+
btn.style.background = '#3a1a1a';
|
| 595 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Đóng';
|
| 596 |
+
} else {
|
| 597 |
+
btn.style.background = '';
|
| 598 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
|
| 599 |
+
// Also hide post-record panel
|
| 600 |
+
document.getElementById('vtv-rec-panel').classList.remove('show');
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
// ===== PIN BUTTON =====
|
| 606 |
function setupPinButton(){
|
| 607 |
const block = document.getElementById('vtv-block');
|
|
|
|
| 609 |
const header = block.querySelector('.vtv-head');
|
| 610 |
if(!header) return;
|
| 611 |
if(document.getElementById('vtv-pin-btn')) return;
|
|
|
|
| 612 |
const btn = document.createElement('button');
|
| 613 |
btn.id = 'vtv-pin-btn';
|
| 614 |
btn.className = 'vtv-pin-btn';
|
|
|
|
| 617 |
btn.onclick = function(e){
|
| 618 |
e.stopPropagation();
|
| 619 |
_vtvPinned = !_vtvPinned;
|
| 620 |
+
if(_vtvPinned){ block.classList.add('vtv-sticky'); btn.classList.add('pinned'); btn.innerHTML = '📌 Đã ghim'; }
|
| 621 |
+
else { block.classList.remove('vtv-sticky'); btn.classList.remove('pinned'); btn.innerHTML = '📌 Ghim'; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
};
|
| 623 |
header.style.position = 'relative';
|
| 624 |
header.appendChild(btn);
|
|
|
|
| 655 |
|
| 656 |
function closeMiniPlayer(){
|
| 657 |
const mini = document.getElementById('vtv-mini');
|
| 658 |
+
if(mini){ const v = document.getElementById('vtv-mini-vid'); if(v){ v.pause(); v.src = ''; } mini.remove(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
_miniActive = false;
|
| 660 |
}
|
| 661 |
|
| 662 |
+
function expandFromMini(){ closeMiniPlayer(); if(typeof switchCat === 'function') switchCat('home'); }
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
function activateMiniPlayer(){
|
| 665 |
if(!_currentCh) return;
|
| 666 |
+
createMiniPlayer(); _miniActive = true;
|
|
|
|
| 667 |
const mini = document.getElementById('vtv-mini');
|
| 668 |
const miniVid = document.getElementById('vtv-mini-vid');
|
| 669 |
const mainVid = document.getElementById('vtv-player');
|
|
|
|
| 670 |
if(_hls){
|
| 671 |
const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:10});
|
| 672 |
+
hls.loadSource(STREAMS[_currentCh][0]); hls.attachMedia(miniVid); miniVid.play().catch(()=>{});
|
| 673 |
+
} else if(mainVid && mainVid.src){ miniVid.src = mainVid.src; miniVid.play().catch(()=>{}); }
|
| 674 |
+
const ch = CHANNELS.find(c=>c.id===_currentCh); if(ch) document.getElementById('vtv-mini-ch').textContent = ch.name;
|
| 675 |
+
updateMiniEpg(); mini.classList.add('show'); mini.classList.remove('hidden');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
}
|
| 677 |
|
| 678 |
function updateMiniEpg(){
|
| 679 |
if(!_currentCh) return;
|
| 680 |
fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{
|
| 681 |
+
const progs = d.programs||[]; const now = progs.find(p=>p.now);
|
|
|
|
| 682 |
document.getElementById('vtv-mini-epg').textContent = now ? now.time+' '+now.title : (progs[0] ? progs[0].time+' '+progs[0].title : '');
|
| 683 |
}).catch(()=>{});
|
| 684 |
}
|
| 685 |
|
| 686 |
async function toggleMiniPiP(){
|
| 687 |
+
const v = document.getElementById('vtv-mini-vid'); if(!v) return;
|
| 688 |
+
try{ if(document.pictureInPictureElement === v){ await document.exitPictureInPicture(); } else { await v.requestPictureInPicture(); } } catch(e){ alert('Không hỗ trợ PiP'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
}
|
| 690 |
|
| 691 |
// ===== MAIN PLAYER =====
|
| 692 |
async function loadAllStreams(){
|
| 693 |
+
if(_loading) return; _loading = true;
|
|
|
|
| 694 |
const el = document.getElementById('vtv-load');
|
| 695 |
if(el) el.innerHTML = '<div class="vtv-spinner"></div>Đang tải kênh...';
|
| 696 |
try{
|
| 697 |
const r = await fetch('/api/vtv/streams', {signal: AbortSignal.timeout(10000)});
|
| 698 |
+
if(r.ok){ const data = await r.json(); CHANNELS.forEach(ch=>{ const info = data[ch.id]; if(info && info.stream_url){ let url = info.stream_url; if(NEEDS_PROXY.test(url)) url = '/api/proxy/m3u8/vtv?url='+encodeURIComponent(url); STREAMS[ch.id] = [url]; } else { STREAMS[ch.id] = []; } }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
} catch(e){ console.warn('VTV API error:', e); }
|
| 700 |
+
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+' ✕'; } } });
|
| 701 |
+
_loading = false; _streamsLoaded = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
}
|
| 703 |
|
| 704 |
function buildBlock(){
|
| 705 |
const w = document.createElement('div');
|
| 706 |
+
w.className = 'vtv-wrap'; w.id = 'vtv-block';
|
|
|
|
| 707 |
let tabs = '';
|
| 708 |
+
CHANNELS.forEach(ch=>{ tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>'; });
|
|
|
|
|
|
|
| 709 |
w.innerHTML =
|
| 710 |
'<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>' +
|
| 711 |
'<div class="vtv-tabs">'+tabs+'</div>' +
|
|
|
|
| 729 |
}
|
| 730 |
|
| 731 |
async function loadEpg(chId){
|
| 732 |
+
const el = document.getElementById('vtv-epg-list'); if(!el) return;
|
|
|
|
| 733 |
el.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
|
| 734 |
try{
|
| 735 |
const r = await fetch('/api/vtv/epg/'+chId, {signal: AbortSignal.timeout(10000)});
|
| 736 |
if(!r.ok) throw new Error('fail');
|
| 737 |
+
const data = await r.json(); const progs = data.programs||[];
|
|
|
|
| 738 |
if(!progs.length){ el.innerHTML='<div class="vtv-epg-empty">Chưa có lịch phát sóng</div>'; return; }
|
| 739 |
let h = '';
|
| 740 |
+
progs.forEach(p=>{ const nw = p.now ? ' now' : ''; 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>'; });
|
|
|
|
|
|
|
|
|
|
| 741 |
el.innerHTML = h;
|
| 742 |
const nowItem = el.querySelector('.vtv-epg-item.now');
|
| 743 |
if(nowItem) nowItem.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});
|
| 744 |
+
} catch(e){ el.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch</div>'; }
|
|
|
|
|
|
|
| 745 |
}
|
| 746 |
|
| 747 |
window._vtvToggleEpg = function(){
|
|
|
|
| 752 |
};
|
| 753 |
|
| 754 |
window._vtvTogglePiP = async function(){
|
| 755 |
+
const v = document.getElementById('vtv-player'); const btn = document.getElementById('vtv-pip-btn'); if(!v) return;
|
|
|
|
|
|
|
| 756 |
try{
|
| 757 |
+
if(document.pictureInPictureElement === v){ await document.exitPictureInPicture(); _pipActive = false; if(btn) btn.classList.remove('on'); }
|
| 758 |
+
else { await v.requestPictureInPicture(); _pipActive = true; if(btn) btn.classList.add('on'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
} catch(e){ alert('Không hỗ trợ PiP'); }
|
| 760 |
};
|
| 761 |
|
|
|
|
| 764 |
if(!h||document.getElementById('vtv-block')) return;
|
| 765 |
h.insertBefore(buildBlock(), h.firstChild);
|
| 766 |
_blockInserted = true;
|
| 767 |
+
setTimeout(()=>{ setupPinButton(); buildInlineRecorder(); }, 500);
|
| 768 |
+
if(!_streamsLoaded){ loadAllStreams().then(()=>{ setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL), 300); }); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
}
|
| 770 |
|
| 771 |
window._vtvRetry = function(){ if(_currentCh) window._vtvPlay(_currentCh); };
|
| 772 |
|
| 773 |
window._vtvPlay = function(chId){
|
| 774 |
+
const ch = CHANNELS.find(c=>c.id===chId); if(!ch) return;
|
|
|
|
| 775 |
_currentCh = chId;
|
| 776 |
document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
|
| 777 |
+
const tab = document.getElementById('vtvt-'+chId); if(tab) tab.classList.add('on');
|
|
|
|
| 778 |
const video = document.getElementById('vtv-player');
|
| 779 |
+
const errEl = document.getElementById('vtv-err'); const loadEl = document.getElementById('vtv-load');
|
|
|
|
| 780 |
const errMsg = document.getElementById('vtv-err-msg');
|
| 781 |
+
video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
|
|
|
|
|
|
|
| 782 |
loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
|
| 783 |
if(_hls){_hls.destroy(); _hls=null;}
|
| 784 |
const urls = STREAMS[chId]||[];
|
| 785 |
+
if(!urls.length){ loadEl.style.display='none'; errEl.style.display='flex'; errMsg.textContent = chId==='vtvprime' ? 'VTVPrime: Kênh trả phí.' : ch.name+': Không có luồng.'; return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
_tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
|
| 787 |
loadEpg(chId);
|
| 788 |
+
if(_miniActive){ document.getElementById('vtv-mini-ch').textContent = ch.name; updateMiniEpg(); }
|
|
|
|
|
|
|
|
|
|
| 789 |
};
|
| 790 |
|
| 791 |
function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
|
| 792 |
+
if(idx>=urls.length){ loadEl.style.display='none'; errEl.style.display='flex'; errMsg.textContent = name+': Tất cả nguồn lỗi.'; return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
const src = urls[idx];
|
| 794 |
loadEl.innerHTML = '<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 795 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 796 |
const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20});
|
| 797 |
+
_hls = hls; hls.loadSource(src); hls.attachMedia(video);
|
|
|
|
|
|
|
| 798 |
hls.on(Hls.Events.MANIFEST_PARSED,()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; });
|
| 799 |
let rec = 0;
|
| 800 |
+
hls.on(Hls.Events.ERROR,(ev,data)=>{ if(data.fatal){ if(data.type===Hls.ErrorTypes.NETWORK_ERROR){ rec++; if(rec<=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); } } });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
} else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 802 |
video.src = src;
|
| 803 |
video.addEventListener('loadedmetadata',()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; },{once:true});
|
| 804 |
video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
|
| 805 |
+
} else { loadEl.style.display='none'; errEl.style.display='flex'; errMsg.textContent='Không hỗ trợ HLS'; }
|
|
|
|
|
|
|
| 806 |
}
|
| 807 |
|
| 808 |
// ===== NAVIGATION HOOKS =====
|
| 809 |
const _origShowView = window.showView;
|
| 810 |
window.showView = function(id){
|
| 811 |
+
if(id==='view-home' && _miniActive){ closeMiniPlayer(); }
|
| 812 |
+
else if(id!=='view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive){ activateMiniPlayer(); }
|
|
|
|
|
|
|
|
|
|
| 813 |
return _origShowView.apply(this, arguments);
|
| 814 |
};
|
|
|
|
| 815 |
const _origReadArticle = window.readArticle;
|
| 816 |
+
if(_origReadArticle){ window.readArticle = function(url){ if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive) activateMiniPlayer(); return _origReadArticle.apply(this, arguments); }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
const _origSwitchCat = window.switchCat;
|
| 818 |
+
if(_origSwitchCat){ window.switchCat = function(id){ if(id==='home'){ if(_miniActive) closeMiniPlayer(); } else if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length>0 && !_miniActive){ activateMiniPlayer(); } return _origSwitchCat.apply(this, arguments); }; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
// Override loadHome to inject VTV block
|
| 821 |
const _origLoadHome = window.loadHome;
|
|
|
|
| 830 |
};
|
| 831 |
window.loadHome.__vtvWrapped = true;
|
| 832 |
}
|
| 833 |
+
|
| 834 |
+
function fmtSec(s){ return String(Math.floor(s/60)).padStart(2,'0')+':'+String(Math.floor(s%60)).padStart(2,'0'); }
|
| 835 |
})();
|