Spaces:
Running
Running
Upload static/yt_live.js with huggingface_hub
Browse files- static/yt_live.js +88 -12
static/yt_live.js
CHANGED
|
@@ -297,16 +297,47 @@
|
|
| 297 |
if (!video) return;
|
| 298 |
const wasMuted = video.muted;
|
| 299 |
if (_hls) { _hls.destroy(); _hls = null; }
|
| 300 |
-
|
|
|
|
|
|
|
| 301 |
if (typeof Hls !== 'undefined' && Hls.isSupported()) {
|
| 302 |
-
const hls = new Hls({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
_hls = hls;
|
| 304 |
hls.loadSource(src);
|
| 305 |
hls.attachMedia(video);
|
| 306 |
-
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
| 308 |
video.src = src;
|
| 309 |
-
video.play().catch(
|
|
|
|
|
|
|
| 310 |
}
|
| 311 |
video.muted = wasMuted;
|
| 312 |
}
|
|
@@ -321,25 +352,70 @@
|
|
| 321 |
}
|
| 322 |
|
| 323 |
// ===== SCHEDULED RECORD =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
window._vtvSchedRecord = function() {
|
| 325 |
const ch = _currentCh || 'vtv1';
|
| 326 |
-
|
|
|
|
| 327 |
if (!durStr) return;
|
| 328 |
const dur = parseInt(durStr);
|
| 329 |
-
if (!dur || dur < 5 || dur > 3600) { alert('Thời lượng 5
|
| 330 |
-
|
| 331 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
fetch('/api/dvr/record/schedule', {
|
| 333 |
method: 'POST',
|
| 334 |
headers: {'Content-Type': 'application/json'},
|
| 335 |
-
body: JSON.stringify({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
})
|
| 337 |
.then(r => r.json())
|
| 338 |
.then(d => {
|
| 339 |
-
if (d.error) alert('Error: ' + d.error);
|
| 340 |
-
else alert('✅ Đã hẹn giờ record!\
|
| 341 |
})
|
| 342 |
-
.catch(e => alert('Error: ' + e.message));
|
| 343 |
};
|
| 344 |
|
| 345 |
// ===== BUILD INLINE RECORDER =====
|
|
|
|
| 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 |
}
|
|
|
|
| 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 =====
|