bep40 commited on
Commit
d245ddd
·
verified ·
1 Parent(s): fee94ed

Upload static/yt_live.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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-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>'+
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
- // Nếu DVR active cho kênh này, dùng buffer thay vì live
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);};