Spaces:
Running
Running
Upload static/yt_live.js with huggingface_hub
Browse files- static/yt_live.js +2 -203
static/yt_live.js
CHANGED
|
@@ -20,13 +20,6 @@
|
|
| 20 |
let _currentCh = null, _hls = null, _loading = false, _blockInserted = false;
|
| 21 |
let _streamsLoaded = false, _pipActive = false, _miniActive = false, _vtvPinned = false;
|
| 22 |
|
| 23 |
-
// ===== DVR / TIMESHIFT STATE =====
|
| 24 |
-
const _dvr = {
|
| 25 |
-
active: false, // DVR buffer đang chạy cho kênh hiện tại?
|
| 26 |
-
bufferUrl: null, // URL playlist HLS từ server buffer
|
| 27 |
-
channel: null, // kênh đang DVR
|
| 28 |
-
};
|
| 29 |
-
|
| 30 |
// ===== RECORDER STATE =====
|
| 31 |
const _rec = {
|
| 32 |
active: false, startTime: null, endTime: null,
|
|
@@ -92,13 +85,6 @@
|
|
| 92 |
.vtv-wrap.vtv-sticky .vtv-title{font-size:11px}
|
| 93 |
.vtv-wrap.vtv-sticky .vtv-badge{font-size:8px}
|
| 94 |
.vtv-wrap.vtv-sticky .vtv-pin-btn{top:4px;right:6px}
|
| 95 |
-
.vtv-dvr-btn{background:#0a2a0a;border:1px solid #2d6a2d;color:#5cb87a;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px;font-weight:700}
|
| 96 |
-
.vtv-dvr-btn:hover{background:#1a4a1a;color:#fff}
|
| 97 |
-
.vtv-dvr-btn.on{background:#2d8659;border-color:#5cb87a;color:#fff}
|
| 98 |
-
.vtv-dvr-btn svg{width:11px;height:11px;fill:currentColor}
|
| 99 |
-
.vtv-dvr-sched-btn{background:#2a1a00;border:1px solid #6a4a2d;color:#f0c040;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px;font-weight:700;margin-left:2px}
|
| 100 |
-
.vtv-dvr-sched-btn:hover{background:#4a3a10;color:#fff}
|
| 101 |
-
.vtv-dvr-sched-btn svg{width:11px;height:11px;fill:currentColor}
|
| 102 |
.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}
|
| 103 |
.vtv-rec-btn:hover{background:#990000;color:#fff}
|
| 104 |
.vtv-rec-btn.recording{background:#cc0000;border-color:#ff0000;color:#fff;animation:vtv-rec-pulse 1s infinite}
|
|
@@ -253,171 +239,6 @@
|
|
| 253 |
});
|
| 254 |
}
|
| 255 |
|
| 256 |
-
// ===== DVR / TIMESHIFT =====
|
| 257 |
-
window._vtvDvrStart = function(chId) {
|
| 258 |
-
if (!chId) chId = _currentCh;
|
| 259 |
-
if (!chId) return;
|
| 260 |
-
const statusEl = document.getElementById('vtv-status');
|
| 261 |
-
if (statusEl) statusEl.textContent = '⏳ Đang bật DVR buffer...';
|
| 262 |
-
fetch('/api/dvr/buffer/start/' + chId)
|
| 263 |
-
.then(r => r.json())
|
| 264 |
-
.then(d => {
|
| 265 |
-
if (d.error) { alert('DVR: ' + d.error); return; }
|
| 266 |
-
_dvr.active = true;
|
| 267 |
-
_dvr.channel = chId;
|
| 268 |
-
_dvr.bufferUrl = d.playlist;
|
| 269 |
-
const btn = document.getElementById('vtv-dvr-btn');
|
| 270 |
-
if (btn) { btn.classList.add('on'); btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/></svg> DVR 5\' ON'; }
|
| 271 |
-
if (statusEl) statusEl.textContent = '✅ DVR 5\' active — tua lùi được!';
|
| 272 |
-
if (_currentCh === chId) _switchToDvrStream();
|
| 273 |
-
})
|
| 274 |
-
.catch(e => { alert('DVR start error: ' + e.message); });
|
| 275 |
-
};
|
| 276 |
-
|
| 277 |
-
window._vtvDvrStop = function(chId) {
|
| 278 |
-
if (!chId) chId = _dvr.channel || _currentCh;
|
| 279 |
-
fetch('/api/dvr/buffer/stop/' + chId).catch(() => {});
|
| 280 |
-
_dvr.active = false;
|
| 281 |
-
_dvr.bufferUrl = null;
|
| 282 |
-
const btn = document.getElementById('vtv-dvr-btn');
|
| 283 |
-
if (btn) { btn.classList.remove('on'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2"/></svg> DVR 5\''; }
|
| 284 |
-
if (_currentCh === chId) _switchToLiveStream();
|
| 285 |
-
const statusEl = document.getElementById('vtv-status');
|
| 286 |
-
if (statusEl) statusEl.textContent = 'Đã tắt DVR';
|
| 287 |
-
};
|
| 288 |
-
|
| 289 |
-
window._vtvDvrToggle = function() {
|
| 290 |
-
if (_dvr.active) window._vtvDvrStop();
|
| 291 |
-
else window._vtvDvrStart();
|
| 292 |
-
};
|
| 293 |
-
|
| 294 |
-
function _switchToDvrStream() {
|
| 295 |
-
if (!_dvr.bufferUrl) return;
|
| 296 |
-
const video = document.getElementById('vtv-player');
|
| 297 |
-
if (!video) return;
|
| 298 |
-
const wasMuted = video.muted;
|
| 299 |
-
if (_hls) { _hls.destroy(); _hls = null; }
|
| 300 |
-
// Dùng relative URL để HLS.js resolve segment paths đúng
|
| 301 |
-
const src = _dvr.bufferUrl;
|
| 302 |
-
console.log('[DVR] Switching to buffer:', src);
|
| 303 |
-
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
| 304 |
-
const hls = new Hls({
|
| 305 |
-
enableWorker: true,
|
| 306 |
-
lowLatencyMode: false,
|
| 307 |
-
startLevel: 0,
|
| 308 |
-
capLevelToPlayerSize: true,
|
| 309 |
-
maxBufferLength: 30,
|
| 310 |
-
liveSyncDurationCount: 2,
|
| 311 |
-
fragLoadingTimeOut: 15000,
|
| 312 |
-
manifestLoadingTimeOut: 15000,
|
| 313 |
-
});
|
| 314 |
-
_hls = hls;
|
| 315 |
-
hls.loadSource(src);
|
| 316 |
-
hls.attachMedia(video);
|
| 317 |
-
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
| 318 |
-
console.log('[DVR] Manifest parsed, playing...');
|
| 319 |
-
video.play().catch(e => console.warn('[DVR] play failed:', e));
|
| 320 |
-
});
|
| 321 |
-
hls.on(Hls.Events.ERROR, (ev, data) => {
|
| 322 |
-
console.error('[DVR] HLS error:', data.type, data.details, data.fatal);
|
| 323 |
-
if (data.fatal) {
|
| 324 |
-
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
| 325 |
-
console.log('[DVR] Network error, retrying in 3s...');
|
| 326 |
-
setTimeout(() => { if (_dvr.active) _switchToDvrStream(); }, 3000);
|
| 327 |
-
} else {
|
| 328 |
-
console.log('[DVR] Fatal error, falling back to live stream');
|
| 329 |
-
_dvr.active = false;
|
| 330 |
-
const btn = document.getElementById('vtv-dvr-btn');
|
| 331 |
-
if (btn) { btn.classList.remove('on'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2"/></svg> DVR 5\''; }
|
| 332 |
-
_switchToLiveStream();
|
| 333 |
-
}
|
| 334 |
-
}
|
| 335 |
-
});
|
| 336 |
-
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
| 337 |
-
video.src = src;
|
| 338 |
-
video.play().catch(e => console.warn('[DVR] native play failed:', e));
|
| 339 |
-
} else {
|
| 340 |
-
alert('Trình duyệt không hỗ trợ HLS — cần hls.js');
|
| 341 |
-
}
|
| 342 |
-
video.muted = wasMuted;
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
function _switchToLiveStream() {
|
| 346 |
-
if (!_currentCh) return;
|
| 347 |
-
const urls = STREAMS[_currentCh] || [];
|
| 348 |
-
const video = document.getElementById('vtv-player');
|
| 349 |
-
if (!video || !urls.length) return;
|
| 350 |
-
if (_hls) { _hls.destroy(); _hls = null; }
|
| 351 |
-
_tryPlay(video, urls, 0, _currentCh, document.getElementById('vtv-load'), document.getElementById('vtv-err'), document.getElementById('vtv-err-msg'));
|
| 352 |
-
}
|
| 353 |
-
|
| 354 |
-
// ===== SCHEDULED RECORD =====
|
| 355 |
-
// Helper: format Date theo giờ VN (UTC+7) thành ISO string có timezone
|
| 356 |
-
function _toVNISO(date) {
|
| 357 |
-
// Tạo ISO string với offset +07:00
|
| 358 |
-
const tz = 7 * 60; // UTC+7 = +420 minutes
|
| 359 |
-
const d = new Date(date.getTime() + tz * 60000);
|
| 360 |
-
const iso = d.toISOString().replace('Z', '');
|
| 361 |
-
return iso + '+07:00';
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
window._vtvSchedRecord = function() {
|
| 365 |
-
const ch = _currentCh || 'vtv1';
|
| 366 |
-
// Bước 1: Hỏi thời lượng
|
| 367 |
-
const durStr = prompt('⏱️ Thời lượng record (giây):\n\n• 60 = 1 phút\n• 300 = 5 phút\n• 600 = 10 phút\n• 1800 = 30 phút\n• 3600 = 1 giờ', '600');
|
| 368 |
-
if (!durStr) return;
|
| 369 |
-
const dur = parseInt(durStr);
|
| 370 |
-
if (!dur || dur < 5 || dur > 3600) { alert('⚠️ Thời lượng từ 5 đến 3600 giây!'); return; }
|
| 371 |
-
|
| 372 |
-
// Bước 2: Hỏi thời điểm bắt đầu (giờ VN)
|
| 373 |
-
const nowVN = new Date(Date.now() + 7*3600000 - new Date().getTimezoneOffset()*60000);
|
| 374 |
-
const nowVNStr = nowVN.toISOString().slice(0, 16); // YYYY-MM-DDTHH:mm
|
| 375 |
-
const startStr = prompt('🕐 Thời điểm BẮT ĐẦU (giờ VN):\n\nĐịnh dạng: YYYY-MM-DD HH:MM\nVí dụ: ' + nowVNStr.replace('T', ' '), nowVNStr.replace('T', ' '));
|
| 376 |
-
if (!startStr) return;
|
| 377 |
-
|
| 378 |
-
// Parse giờ VN thành ISO
|
| 379 |
-
const startVN = new Date(startStr.replace(' ', 'T') + ':00+07:00');
|
| 380 |
-
if (isNaN(startVN.getTime())) { alert('⚠️ Giờ không hợp lệ! Dùng format: YYYY-MM-DD HH:MM'); return; }
|
| 381 |
-
|
| 382 |
-
// Bước 3: Hỏi thời điểm kết thúc (giờ VN)
|
| 383 |
-
const defaultEnd = new Date(startVN.getTime() + dur*1000);
|
| 384 |
-
const defaultEndStr = new Date(defaultEnd.getTime() + 7*3600000 - defaultEnd.getTimezoneOffset()*60000).toISOString().slice(0,16).replace('T',' ');
|
| 385 |
-
const endStr = prompt('🕐 Thời điểm KẾT THÚC (giờ VN):\n\nMặc định = bắt đầu + ' + dur + 's', defaultEndStr);
|
| 386 |
-
if (!endStr) return;
|
| 387 |
-
const endVN = new Date(endStr.replace(' ', 'T') + ':00+07:00');
|
| 388 |
-
if (isNaN(endVN.getTime()) || endVN <= startVN) { alert('⚠️ Giờ kết thúc phải sau giờ bắt đầu!'); return; }
|
| 389 |
-
|
| 390 |
-
// Confirm
|
| 391 |
-
const durActual = Math.round((endVN - startVN) / 1000);
|
| 392 |
-
const ok = confirm(
|
| 393 |
-
'📋 XÁC NHẬN HẸN GIỜ RECORD\n\n' +
|
| 394 |
-
'📺 Kênh: ' + ch.toUpperCase() + '\n' +
|
| 395 |
-
'🕐 Bắt đầu: ' + startStr + ' (VN)\n' +
|
| 396 |
-
'🕐 Kết thúc: ' + endStr + ' (VN)\n' +
|
| 397 |
-
'⏱️ Thời lượng: ' + durActual + 's (' + Math.floor(durActual/60) + 'p ' + (durActual%60) + 's)\n\n' +
|
| 398 |
-
'OK để xác nhận?'
|
| 399 |
-
);
|
| 400 |
-
if (!ok) return;
|
| 401 |
-
|
| 402 |
-
fetch('/api/dvr/record/schedule', {
|
| 403 |
-
method: 'POST',
|
| 404 |
-
headers: {'Content-Type': 'application/json'},
|
| 405 |
-
body: JSON.stringify({
|
| 406 |
-
channel: ch,
|
| 407 |
-
start_iso: startVN.toISOString(),
|
| 408 |
-
end_iso: endVN.toISOString(),
|
| 409 |
-
duration_sec: durActual,
|
| 410 |
-
prebuffer: true
|
| 411 |
-
})
|
| 412 |
-
})
|
| 413 |
-
.then(r => r.json())
|
| 414 |
-
.then(d => {
|
| 415 |
-
if (d.error) alert('❌ Error: ' + d.error);
|
| 416 |
-
else alert('✅ Đã hẹn giờ record!\n\n📺 Kênh: ' + ch.toUpperCase() + '\n🕐 ' + startStr + ' → ' + endStr + ' (giờ VN)\n⏱️ ' + durActual + 's');
|
| 417 |
-
})
|
| 418 |
-
.catch(e => alert('❌ Error: ' + e.message));
|
| 419 |
-
};
|
| 420 |
-
|
| 421 |
// ===== BUILD INLINE RECORDER =====
|
| 422 |
function buildInlineRecorder() {
|
| 423 |
const controls = document.querySelector('.vtv-controls');
|
|
@@ -969,7 +790,7 @@
|
|
| 969 |
w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
|
| 970 |
'<div class="vtv-tabs">'+tabs+'</div>'+
|
| 971 |
'<div class="vtv-frame"><div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải 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>'+
|
| 972 |
-
'<div class="vtv-controls"><button class="vtv-
|
| 973 |
'<div class="vtv-epg" id="vtv-epg"><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><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></div>';
|
| 974 |
return w;
|
| 975 |
}
|
|
@@ -1002,12 +823,7 @@
|
|
| 1002 |
if(_hls){_hls.destroy();_hls=null;}
|
| 1003 |
const urls=STREAMS[chId]||[];
|
| 1004 |
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;}
|
| 1005 |
-
|
| 1006 |
-
if (_dvr.active && _dvr.channel === chId && _dvr.bufferUrl) {
|
| 1007 |
-
_switchToDvrStream();
|
| 1008 |
-
} else {
|
| 1009 |
-
_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
|
| 1010 |
-
}
|
| 1011 |
loadEpg(chId);
|
| 1012 |
if(_miniActive){document.getElementById('vtv-mini-ch').textContent=ch.name;fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[];const n=p.find(x=>x.now);document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});}
|
| 1013 |
};
|
|
@@ -1026,23 +842,6 @@
|
|
| 1026 |
}else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
|
| 1027 |
}
|
| 1028 |
|
| 1029 |
-
// ===== SYNC DVR BUTTON STATE ON CHANNEL SWITCH =====
|
| 1030 |
-
const _origVtvPlay = window._vtvPlay;
|
| 1031 |
-
window._vtvPlay = function(chId) {
|
| 1032 |
-
// Cập nhật trạng thái nút DVR theo kênh
|
| 1033 |
-
const btn = document.getElementById('vtv-dvr-btn');
|
| 1034 |
-
if (btn) {
|
| 1035 |
-
if (_dvr.active && _dvr.channel === chId) {
|
| 1036 |
-
btn.classList.add('on');
|
| 1037 |
-
btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/></svg> DVR 5\' ON';
|
| 1038 |
-
} else {
|
| 1039 |
-
btn.classList.remove('on');
|
| 1040 |
-
btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/><line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2"/></svg> DVR 5\'';
|
| 1041 |
-
}
|
| 1042 |
-
}
|
| 1043 |
-
return _origVtvPlay.apply(this, arguments);
|
| 1044 |
-
};
|
| 1045 |
-
|
| 1046 |
// ===== NAV HOOKS =====
|
| 1047 |
const _origShowView=window.showView;
|
| 1048 |
window.showView=function(id){if(id==='view-home'&&_miniActive)closeMiniPlayer();else if(id!=='view-home'&&_currentCh&&STREAMS[_currentCh]&&STREAMS[_currentCh].length>0&&!_miniActive)activateMiniPlayer();return _origShowView.apply(this,arguments);};
|
|
|
|
| 20 |
let _currentCh = null, _hls = null, _loading = false, _blockInserted = false;
|
| 21 |
let _streamsLoaded = false, _pipActive = false, _miniActive = false, _vtvPinned = false;
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
// ===== RECORDER STATE =====
|
| 24 |
const _rec = {
|
| 25 |
active: false, startTime: null, endTime: null,
|
|
|
|
| 85 |
.vtv-wrap.vtv-sticky .vtv-title{font-size:11px}
|
| 86 |
.vtv-wrap.vtv-sticky .vtv-badge{font-size:8px}
|
| 87 |
.vtv-wrap.vtv-sticky .vtv-pin-btn{top:4px;right:6px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
.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}
|
| 89 |
.vtv-rec-btn:hover{background:#990000;color:#fff}
|
| 90 |
.vtv-rec-btn.recording{background:#cc0000;border-color:#ff0000;color:#fff;animation:vtv-rec-pulse 1s infinite}
|
|
|
|
| 239 |
});
|
| 240 |
}
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
// ===== BUILD INLINE RECORDER =====
|
| 243 |
function buildInlineRecorder() {
|
| 244 |
const controls = document.querySelector('.vtv-controls');
|
|
|
|
| 790 |
w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
|
| 791 |
'<div class="vtv-tabs">'+tabs+'</div>'+
|
| 792 |
'<div class="vtv-frame"><div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải 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>'+
|
| 793 |
+
'<div class="vtv-controls"><button class="vtv-pip-btn" id="vtv-pip-btn" onclick="window._vtvTogglePiP()"><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</button><span style="flex:1"></span><span style="font-size:9px;color:#666" id="vtv-status"></span></div>'+
|
| 794 |
'<div class="vtv-epg" id="vtv-epg"><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><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></div>';
|
| 795 |
return w;
|
| 796 |
}
|
|
|
|
| 823 |
if(_hls){_hls.destroy();_hls=null;}
|
| 824 |
const urls=STREAMS[chId]||[];
|
| 825 |
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;}
|
| 826 |
+
_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 827 |
loadEpg(chId);
|
| 828 |
if(_miniActive){document.getElementById('vtv-mini-ch').textContent=ch.name;fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[];const n=p.find(x=>x.now);document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});}
|
| 829 |
};
|
|
|
|
| 842 |
}else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
|
| 843 |
}
|
| 844 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
// ===== NAV HOOKS =====
|
| 846 |
const _origShowView=window.showView;
|
| 847 |
window.showView=function(id){if(id==='view-home'&&_miniActive)closeMiniPlayer();else if(id!=='view-home'&&_currentCh&&STREAMS[_currentCh]&&STREAMS[_currentCh].length>0&&!_miniActive)activateMiniPlayer();return _origShowView.apply(this,arguments);};
|