Spaces:
Running
Running
Upload static/yt_live.js with huggingface_hub
Browse files- static/yt_live.js +127 -2
static/yt_live.js
CHANGED
|
@@ -20,6 +20,13 @@
|
|
| 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,6 +92,13 @@
|
|
| 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,6 +253,95 @@
|
|
| 239 |
});
|
| 240 |
}
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
// ===== BUILD INLINE RECORDER =====
|
| 243 |
function buildInlineRecorder() {
|
| 244 |
const controls = document.querySelector('.vtv-controls');
|
|
@@ -790,7 +893,7 @@
|
|
| 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,7 +926,12 @@
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,6 +950,23 @@
|
|
| 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);};
|
|
|
|
| 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 |
.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 |
});
|
| 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 |
+
const src = 'https://' + window.location.host + _dvr.bufferUrl;
|
| 301 |
+
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
| 302 |
+
const hls = new Hls({enableWorker:true, lowLatencyMode:false, startLevel:0, capLevelToPlayerSize:true, maxBufferLength:30, liveSyncDurationCount:2});
|
| 303 |
+
_hls = hls;
|
| 304 |
+
hls.loadSource(src);
|
| 305 |
+
hls.attachMedia(video);
|
| 306 |
+
hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play().catch(()=>{}); });
|
| 307 |
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
| 308 |
+
video.src = src;
|
| 309 |
+
video.play().catch(()=>{});
|
| 310 |
+
}
|
| 311 |
+
video.muted = wasMuted;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
function _switchToLiveStream() {
|
| 315 |
+
if (!_currentCh) return;
|
| 316 |
+
const urls = STREAMS[_currentCh] || [];
|
| 317 |
+
const video = document.getElementById('vtv-player');
|
| 318 |
+
if (!video || !urls.length) return;
|
| 319 |
+
if (_hls) { _hls.destroy(); _hls = null; }
|
| 320 |
+
_tryPlay(video, urls, 0, _currentCh, document.getElementById('vtv-load'), document.getElementById('vtv-err'), document.getElementById('vtv-err-msg'));
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// ===== SCHEDULED RECORD =====
|
| 324 |
+
window._vtvSchedRecord = function() {
|
| 325 |
+
const ch = _currentCh || 'vtv1';
|
| 326 |
+
const durStr = prompt('Thời lượng record (giây, max 3600):', '600');
|
| 327 |
+
if (!durStr) return;
|
| 328 |
+
const dur = parseInt(durStr);
|
| 329 |
+
if (!dur || dur < 5 || dur > 3600) { alert('Thời lượng 5-3600s'); return; }
|
| 330 |
+
const now = new Date();
|
| 331 |
+
const startIso = new Date(now.getTime() + 5000).toISOString();
|
| 332 |
+
fetch('/api/dvr/record/schedule', {
|
| 333 |
+
method: 'POST',
|
| 334 |
+
headers: {'Content-Type': 'application/json'},
|
| 335 |
+
body: JSON.stringify({channel: ch, start_iso: startIso, duration_sec: dur, prebuffer: true})
|
| 336 |
+
})
|
| 337 |
+
.then(r => r.json())
|
| 338 |
+
.then(d => {
|
| 339 |
+
if (d.error) alert('Error: ' + d.error);
|
| 340 |
+
else alert('✅ Đã hẹn giờ record!\nKênh: ' + ch.toUpperCase() + '\nThời lượng: ' + dur + 's\nBắt đầu: ' + new Date(startIso).toLocaleString('vi-VN'));
|
| 341 |
+
})
|
| 342 |
+
.catch(e => alert('Error: ' + e.message));
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
// ===== BUILD INLINE RECORDER =====
|
| 346 |
function buildInlineRecorder() {
|
| 347 |
const controls = document.querySelector('.vtv-controls');
|
|
|
|
| 893 |
w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
|
| 894 |
'<div class="vtv-tabs">'+tabs+'</div>'+
|
| 895 |
'<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>'+
|
| 896 |
+
'<div class="vtv-controls"><button class="vtv-dvr-btn" id="vtv-dvr-btn" onclick="window._vtvDvrToggle()" title="Bật DVR 5 phút - xem lại LIVE"><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\'</button><button class="vtv-dvr-sched-btn" id="vtv-dvr-sched-btn" onclick="window._vtvSchedRecord()" title="Hẹn giờ record"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/></svg> Record</button><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>'+
|
| 897 |
'<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>';
|
| 898 |
return w;
|
| 899 |
}
|
|
|
|
| 926 |
if(_hls){_hls.destroy();_hls=null;}
|
| 927 |
const urls=STREAMS[chId]||[];
|
| 928 |
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;}
|
| 929 |
+
// Nếu DVR active cho kênh này, dùng buffer thay vì live
|
| 930 |
+
if (_dvr.active && _dvr.channel === chId && _dvr.bufferUrl) {
|
| 931 |
+
_switchToDvrStream();
|
| 932 |
+
} else {
|
| 933 |
+
_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
|
| 934 |
+
}
|
| 935 |
loadEpg(chId);
|
| 936 |
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(()=>{});}
|
| 937 |
};
|
|
|
|
| 950 |
}else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
|
| 951 |
}
|
| 952 |
|
| 953 |
+
// ===== SYNC DVR BUTTON STATE ON CHANNEL SWITCH =====
|
| 954 |
+
const _origVtvPlay = window._vtvPlay;
|
| 955 |
+
window._vtvPlay = function(chId) {
|
| 956 |
+
// Cập nhật trạng thái nút DVR theo kênh
|
| 957 |
+
const btn = document.getElementById('vtv-dvr-btn');
|
| 958 |
+
if (btn) {
|
| 959 |
+
if (_dvr.active && _dvr.channel === chId) {
|
| 960 |
+
btn.classList.add('on');
|
| 961 |
+
btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="4" y="4" width="16" height="16" rx="2"/></svg> DVR 5\' ON';
|
| 962 |
+
} else {
|
| 963 |
+
btn.classList.remove('on');
|
| 964 |
+
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\'';
|
| 965 |
+
}
|
| 966 |
+
}
|
| 967 |
+
return _origVtvPlay.apply(this, arguments);
|
| 968 |
+
};
|
| 969 |
+
|
| 970 |
// ===== NAV HOOKS =====
|
| 971 |
const _origShowView=window.showView;
|
| 972 |
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);};
|