bep40 commited on
Commit
02662c3
·
verified ·
1 Parent(s): 707cab7

fix: DVR replay + recorder select from buffer + Short AI upload flow

Browse files
Files changed (1) hide show
  1. static/yt_live.js +119 -61
static/yt_live.js CHANGED
@@ -28,12 +28,13 @@
28
  };
29
 
30
  // ===== DVR / LIVE REPLAY STATE =====
31
- // Buffer ~30 min of live stream for replay
32
  const _dvr = {
33
  enabled: true,
34
- maxBufferSec: 1800, // 30 minutes
35
- seekOffset: 0, // seconds behind live
36
  isSeeked: false,
 
37
  };
38
 
39
  // ===== STYLES =====
@@ -102,6 +103,7 @@
102
  .vtv-inline-rec{display:none;padding:8px 10px;background:#0a0a1a;border-top:1px solid #1a1a3a}
103
  .vtv-inline-rec.show{display:block}
104
  .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;user-select:none}
 
105
  .vtv-inline-rec .rec-bar .rec-progress{position:absolute;top:0;bottom:0;background:rgba(155,89,182,.3);left:0;width:0%;pointer-events:none}
106
  .vtv-inline-rec .rec-bar .rec-marker{position:absolute;top:0;bottom:0;width:4px;z-index:2;border-radius:2px}
107
  .vtv-inline-rec .rec-bar .rec-marker.s{background:#2ecc71}
@@ -207,42 +209,43 @@
207
  hideProc();
208
  console.log('[VTV Upload] Parsed data:', data);
209
  if (data && data.ok === true) {
210
- recStatus('✅ Đã tạo Short AI! Đang tải lại trang chủ...', 'ok');
211
- // 100% FIX: Reload wall from server, then re-render home
212
  if (data.post) {
213
- // Update local cache immediately
214
  if (typeof _wallPosts !== 'undefined') {
215
  _wallPosts.unshift(data.post);
216
  }
217
- // Reload home to show new Short AI slide
218
- // Use a flag to prevent double reload
219
- if (window._vtvReloading) return data;
220
- window._vtvReloading = true;
221
- if (typeof loadHome === 'function') {
222
- setTimeout(() => {
223
- // Switch to home first
224
- if (typeof switchCat === 'function') switchCat('home');
225
- // Then reload home (this re-renders everything including Short AI)
226
- setTimeout(() => {
227
- loadHome().then(() => {
228
- // Scroll to Short AI section after reload
229
- setTimeout(() => {
230
- const s = document.getElementById('short-ai-section');
231
- if (s) {
232
- s.scrollIntoView({behavior:'smooth', block:'start'});
233
- // Highlight animation
234
- s.style.transition = 'box-shadow 0.5s';
235
- s.style.boxShadow = '0 0 20px rgba(155,89,182,0.8)';
236
- setTimeout(() => s.style.boxShadow = '', 2000);
237
- }
238
- window._vtvReloading = false;
239
- }, 300);
240
- }).catch(() => { window._vtvReloading = false; });
241
- }, 200);
242
- }, 500);
243
- } else {
244
- window._vtvReloading = false;
245
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  }
247
  return data;
248
  } else {
@@ -310,6 +313,7 @@
310
  <span id="vtv-rec-time-end">01:00</span>
311
  </div>
312
  <div class="rec-bar" id="vtv-rec-bar">
 
313
  <div class="rec-progress" id="vtv-rec-progress"></div>
314
  <div class="rec-marker s" id="vtv-rec-mk-start" style="display:none"></div>
315
  <div class="rec-marker e" id="vtv-rec-mk-end" style="display:none"></div>
@@ -320,7 +324,7 @@
320
  <button class="rec-go" id="vtv-rec-go" onclick="window._vtvRecGo()" disabled>⏺ Record!</button>
321
  <button class="rec-reset" onclick="window._vtvRecReset()">↺ Reset</button>
322
  </div>
323
- <div class="rec-hint" id="vtv-rec-hint">Chọn điểm bắt đầu → kết thúc (tối đa 60s)</div>
324
  <div class="rec-status" id="vtv-rec-status">Sẵn sàng</div>
325
  `;
326
  controls.parentNode.insertBefore(inlineRec, controls.nextSibling);
@@ -419,11 +423,32 @@
419
  }
420
  }
421
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  function updateRecBar() {
423
- const s = _rec.startTime || 0, e = _rec.endTime || 60;
424
  const dur = e - s;
425
- document.getElementById('vtv-rec-progress').style.left = (s/60*100) + '%';
426
- document.getElementById('vtv-rec-progress').style.width = (dur/60*100) + '%';
427
  document.getElementById('vtv-rec-time-dur').textContent = 'Duration: ' + Math.round(dur) + 's';
428
  }
429
 
@@ -431,27 +456,29 @@
431
  window._vtvRecSetStart = function() {
432
  const video = document.getElementById('vtv-player');
433
  if (!video || !video.src) { recStatus('⚠️ Chưa có stream!', 'err'); return; }
 
434
  _rec.startTime = video.currentTime || 0;
435
- const pct = _rec.startTime / 60 * 100;
436
  const mk = document.getElementById('vtv-rec-mk-start');
437
- mk.style.display = 'block'; mk.style.left = pct + '%';
438
  document.getElementById('vtv-rec-time-start').textContent = fmtSec(_rec.startTime);
439
  document.getElementById('vtv-rec-set-end').disabled = false;
440
- recStatus('Điểm đầu: ' + fmtSec(_rec.startTime), '');
441
  };
442
 
443
  window._vtvRecSetEnd = function() {
444
  if (_rec.startTime === null) { recStatus('⚠️ Chọn điểm đầu trước!', 'err'); return; }
445
  const video = document.getElementById('vtv-player');
446
  if (!video) return;
 
447
  const cur = video.currentTime || 0;
448
  if (cur <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
449
  const dur = cur - _rec.startTime;
450
- if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
451
  _rec.endTime = cur;
452
- const pct = cur / 60 * 100;
453
  const mk = document.getElementById('vtv-rec-mk-end');
454
- mk.style.display = 'block'; mk.style.left = pct + '%';
455
  document.getElementById('vtv-rec-time-end').textContent = fmtSec(cur);
456
  updateRecBar();
457
  recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
@@ -466,7 +493,7 @@
466
  document.getElementById('vtv-rec-mk-end').style.display = 'none';
467
  document.getElementById('vtv-rec-progress').style.width = '0%';
468
  document.getElementById('vtv-rec-time-start').textContent = '00:00';
469
- document.getElementById('vtv-rec-time-end').textContent = '01:00';
470
  document.getElementById('vtv-rec-time-dur').textContent = 'Duration: 0s';
471
  document.getElementById('vtv-rec-set-start').disabled = false;
472
  document.getElementById('vtv-rec-set-end').disabled = true;
@@ -483,7 +510,11 @@
483
  document.querySelectorAll('#vtv-rec-panel .rec-ratio-row button').forEach(b => b.classList.toggle('active', b.dataset.ratio === ratio));
484
  };
485
 
486
- // ===== RECORD captureStream with audio (unmute during record) =====
 
 
 
 
487
  window._vtvRecGo = function() {
488
  if (_rec.startTime === null || _rec.endTime === null) return;
489
  if (_rec.isRecording) {
@@ -498,14 +529,28 @@
498
  const video = document.getElementById('vtv-player');
499
  if (!video) { recStatus('⚠️ Không tìm thấy video!', 'err'); return; }
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  _rec.chunks = [];
502
  _rec.isRecording = true;
503
 
504
- // CRITICAL: Unmute video so captureStream includes audio tracks
505
  const wasMuted = video.muted;
506
  video.muted = false;
507
  const wasVolume = video.volume;
508
- video.volume = 0.3;
509
 
510
  let stream;
511
  try {
@@ -547,6 +592,7 @@
547
  document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
548
  const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
549
  recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
 
550
  };
551
  _rec.recorder.onerror = e => {
552
  _rec.isRecording = false;
@@ -554,17 +600,25 @@
554
  recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
555
  };
556
 
557
- _rec.recorder.start(500);
 
558
  video.currentTime = _rec.startTime;
559
 
560
- const btn = document.getElementById('vtv-rec-btn');
561
- 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'; }
562
- recStatus('🔴 ĐANG RECORD...' + (audioTracks.length > 0 ? ' (có audio)' : ''), 'recording');
563
-
564
- const dur = (_rec.endTime - _rec.startTime) * 1000;
565
- _rec._recTimer = setTimeout(() => {
566
- if (_rec.isRecording && _rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
567
- }, dur + 500);
 
 
 
 
 
 
 
568
  };
569
 
570
  // ===== AI TITLE =====
@@ -978,14 +1032,18 @@
978
  window._vtvDvrGoLive = function() {
979
  const video = document.getElementById('vtv-player');
980
  if (!video) return;
 
 
 
 
 
 
981
  if (video.buffered && video.buffered.length > 0) {
982
- video.currentTime = video.buffered.end(video.buffered.length - 1);
983
  }
984
  window._vtvDvrSeek(0);
985
  const slider = document.getElementById('vtv-dvr-slider');
986
  if (slider) slider.value = 0;
987
- _dvr.isSeeked = false;
988
- _dvr.seekOffset = 0;
989
  };
990
 
991
  // ===== MAIN PLAYER =====
@@ -1048,7 +1106,7 @@
1048
  if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
1049
  const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
1050
  if(typeof Hls!=='undefined'&&Hls.isSupported()){
1051
- const hls=new Hls({enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,maxBufferLength:120,maxMaxBufferLength:300,liveSyncDurationCount:2,liveMaxLatencyDurationCount:5,liveDurationInfinity:false,fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
1052
  hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
1053
  let rec=0; 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);}}});
1054
  }else if(video.canPlayType('application/vnd.apple.mpegurl')){
 
28
  };
29
 
30
  // ===== DVR / LIVE REPLAY STATE =====
31
+ // Buffer ~10 min of live stream for replay
32
  const _dvr = {
33
  enabled: true,
34
+ maxBufferSec: 600, // 10 minutes
35
+ seekOffset: 0,
36
  isSeeked: false,
37
+ lastBufferedEnd: 0,
38
  };
39
 
40
  // ===== STYLES =====
 
103
  .vtv-inline-rec{display:none;padding:8px 10px;background:#0a0a1a;border-top:1px solid #1a1a3a}
104
  .vtv-inline-rec.show{display:block}
105
  .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;user-select:none}
106
+ .vtv-inline-rec .rec-buffer-range{border-right:2px solid #2ecc71;pointer-events:none}
107
  .vtv-inline-rec .rec-bar .rec-progress{position:absolute;top:0;bottom:0;background:rgba(155,89,182,.3);left:0;width:0%;pointer-events:none}
108
  .vtv-inline-rec .rec-bar .rec-marker{position:absolute;top:0;bottom:0;width:4px;z-index:2;border-radius:2px}
109
  .vtv-inline-rec .rec-bar .rec-marker.s{background:#2ecc71}
 
209
  hideProc();
210
  console.log('[VTV Upload] Parsed data:', data);
211
  if (data && data.ok === true) {
212
+ recStatus('✅ Đã tạo Short AI! Chuyển đến trang chủ...', 'ok');
 
213
  if (data.post) {
214
+ // Update local cache
215
  if (typeof _wallPosts !== 'undefined') {
216
  _wallPosts.unshift(data.post);
217
  }
218
+ // Switch to home view
219
+ if (typeof switchCat === 'function') {
220
+ switchCat('home');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  }
222
+ // Wait for home to be visible, then refresh slider
223
+ setTimeout(() => {
224
+ // Try refreshShortAISlider first (fast path)
225
+ if (typeof refreshShortAISlider === 'function') {
226
+ refreshShortAISlider();
227
+ }
228
+ // Scroll to Short AI section
229
+ setTimeout(() => {
230
+ const s = document.getElementById('short-ai-section');
231
+ if (s) {
232
+ s.scrollIntoView({behavior:'smooth', block:'start'});
233
+ s.style.transition = 'box-shadow 0.5s';
234
+ s.style.boxShadow = '0 0 20px rgba(155,89,182,0.8)';
235
+ setTimeout(() => s.style.boxShadow = '', 2000);
236
+ } else {
237
+ // Section not found — full reload as fallback
238
+ if (typeof loadHome === 'function') {
239
+ loadHome().then(() => {
240
+ setTimeout(() => {
241
+ const s2 = document.getElementById('short-ai-section');
242
+ if (s2) s2.scrollIntoView({behavior:'smooth', block:'start'});
243
+ }, 300);
244
+ });
245
+ }
246
+ }
247
+ }, 300);
248
+ }, 200);
249
  }
250
  return data;
251
  } else {
 
313
  <span id="vtv-rec-time-end">01:00</span>
314
  </div>
315
  <div class="rec-bar" id="vtv-rec-bar">
316
+ <div class="rec-buffer-range" id="vtv-rec-buffer-range" style="position:absolute;top:0;bottom:0;background:rgba(46,204,113,0.15);left:0;width:0%"></div>
317
  <div class="rec-progress" id="vtv-rec-progress"></div>
318
  <div class="rec-marker s" id="vtv-rec-mk-start" style="display:none"></div>
319
  <div class="rec-marker e" id="vtv-rec-mk-end" style="display:none"></div>
 
324
  <button class="rec-go" id="vtv-rec-go" onclick="window._vtvRecGo()" disabled>⏺ Record!</button>
325
  <button class="rec-reset" onclick="window._vtvRecReset()">↺ Reset</button>
326
  </div>
327
+ <div class="rec-hint" id="vtv-rec-hint">Xem lại LIVE → chọn điểm đầu → kết thúc → Record (tối đa 120s)</div>
328
  <div class="rec-status" id="vtv-rec-status">Sẵn sàng</div>
329
  `;
330
  controls.parentNode.insertBefore(inlineRec, controls.nextSibling);
 
423
  }
424
  }
425
 
426
+ // Recorder bar scale: dynamic based on buffer
427
+ let _recBarMax = 120; // default 2 min, updated from buffer
428
+
429
+ function updateRecBarScale() {
430
+ const video = document.getElementById('vtv-player');
431
+ if (!video) return;
432
+ if (video.buffered && video.buffered.length > 0) {
433
+ const bufEnd = video.buffered.end(video.buffered.length - 1);
434
+ const bufStart = video.buffered.start(0);
435
+ _recBarMax = Math.max(60, Math.ceil(bufEnd - bufStart));
436
+ }
437
+ // Update buffer range indicator
438
+ const bufRange = document.getElementById('vtv-rec-buffer-range');
439
+ if (bufRange && video.buffered && video.buffered.length > 0) {
440
+ const bufStart = video.buffered.start(0);
441
+ const bufEnd = video.buffered.end(video.buffered.length - 1);
442
+ bufRange.style.left = (bufStart / _recBarMax * 100) + '%';
443
+ bufRange.style.width = ((bufEnd - bufStart) / _recBarMax * 100) + '%';
444
+ }
445
+ }
446
+
447
  function updateRecBar() {
448
+ const s = _rec.startTime || 0, e = _rec.endTime || Math.min(60, _recBarMax);
449
  const dur = e - s;
450
+ document.getElementById('vtv-rec-progress').style.left = (s/_recBarMax*100) + '%';
451
+ document.getElementById('vtv-rec-progress').style.width = (dur/_recBarMax*100) + '%';
452
  document.getElementById('vtv-rec-time-dur').textContent = 'Duration: ' + Math.round(dur) + 's';
453
  }
454
 
 
456
  window._vtvRecSetStart = function() {
457
  const video = document.getElementById('vtv-player');
458
  if (!video || !video.src) { recStatus('⚠️ Chưa có stream!', 'err'); return; }
459
+ updateRecBarScale();
460
  _rec.startTime = video.currentTime || 0;
461
+ const pct = (_rec.startTime / _recBarMax) * 100;
462
  const mk = document.getElementById('vtv-rec-mk-start');
463
+ mk.style.display = 'block'; mk.style.left = Math.min(100, Math.max(0, pct)) + '%';
464
  document.getElementById('vtv-rec-time-start').textContent = fmtSec(_rec.startTime);
465
  document.getElementById('vtv-rec-set-end').disabled = false;
466
+ recStatus('Điểm đầu: ' + fmtSec(_rec.startTime) + ' (buffer: ' + _recBarMax + 's)', '');
467
  };
468
 
469
  window._vtvRecSetEnd = function() {
470
  if (_rec.startTime === null) { recStatus('⚠️ Chọn điểm đầu trước!', 'err'); return; }
471
  const video = document.getElementById('vtv-player');
472
  if (!video) return;
473
+ updateRecBarScale();
474
  const cur = video.currentTime || 0;
475
  if (cur <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
476
  const dur = cur - _rec.startTime;
477
+ if (dur > _recBarMax) { recStatus('⚠️ Tối đa ' + _recBarMax + 's!', 'err'); return; }
478
  _rec.endTime = cur;
479
+ const pct = (cur / _recBarMax) * 100;
480
  const mk = document.getElementById('vtv-rec-mk-end');
481
+ mk.style.display = 'block'; mk.style.left = Math.min(100, Math.max(0, pct)) + '%';
482
  document.getElementById('vtv-rec-time-end').textContent = fmtSec(cur);
483
  updateRecBar();
484
  recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
 
493
  document.getElementById('vtv-rec-mk-end').style.display = 'none';
494
  document.getElementById('vtv-rec-progress').style.width = '0%';
495
  document.getElementById('vtv-rec-time-start').textContent = '00:00';
496
+ document.getElementById('vtv-rec-time-end').textContent = fmtSec(Math.min(60, _recBarMax));
497
  document.getElementById('vtv-rec-time-dur').textContent = 'Duration: 0s';
498
  document.getElementById('vtv-rec-set-start').disabled = false;
499
  document.getElementById('vtv-rec-set-end').disabled = true;
 
510
  document.querySelectorAll('#vtv-rec-panel .rec-ratio-row button').forEach(b => b.classList.toggle('active', b.dataset.ratio === ratio));
511
  };
512
 
513
+ // Update buffer range indicator every 2s
514
+ updateRecBarScale();
515
+ setInterval(updateRecBarScale, 2000);
516
+
517
+ // ===== RECORD — DVR-based: seek to start, play, captureStream =====
518
  window._vtvRecGo = function() {
519
  if (_rec.startTime === null || _rec.endTime === null) return;
520
  if (_rec.isRecording) {
 
529
  const video = document.getElementById('vtv-player');
530
  if (!video) { recStatus('⚠️ Không tìm thấy video!', 'err'); return; }
531
 
532
+ // Check if buffered range covers the requested segment
533
+ let canSeek = false;
534
+ if (video.buffered && video.buffered.length > 0) {
535
+ for (let i = 0; i < video.buffered.length; i++) {
536
+ if (video.buffered.start(i) <= _rec.startTime && video.buffered.end(i) >= _rec.endTime) {
537
+ canSeek = true;
538
+ break;
539
+ }
540
+ }
541
+ }
542
+ if (!canSeek) {
543
+ recStatus('⚠️ Buffer chưa đủ! Xem lại ít nhất ' + Math.round(_rec.endTime) + 's trước khi record.', 'err');
544
+ return;
545
+ }
546
+
547
  _rec.chunks = [];
548
  _rec.isRecording = true;
549
 
 
550
  const wasMuted = video.muted;
551
  video.muted = false;
552
  const wasVolume = video.volume;
553
+ video.volume = 0.5;
554
 
555
  let stream;
556
  try {
 
592
  document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
593
  const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
594
  recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
595
+ console.log('[VTV Rec] Done. Blob size:', sizeMB, 'MB, chunks:', _rec.chunks.length);
596
  };
597
  _rec.recorder.onerror = e => {
598
  _rec.isRecording = false;
 
600
  recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
601
  };
602
 
603
+ // Seek to start time, then start recording after seek completes
604
+ recStatus('🔴 Seek đến ' + fmtSec(_rec.startTime) + '...', 'recording');
605
  video.currentTime = _rec.startTime;
606
 
607
+ function startAfterSeek() {
608
+ if (Math.abs(video.currentTime - _rec.startTime) < 1) {
609
+ _rec.recorder.start(500);
610
+ const btn = document.getElementById('vtv-rec-btn');
611
+ 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'; }
612
+ recStatus('🔴 RECORDING ' + fmtSec(_rec.startTime) + '→' + fmtSec(_rec.endTime) + (audioTracks.length > 0 ? ' (có audio)' : ''), 'recording');
613
+ const dur = (_rec.endTime - _rec.startTime) * 1000;
614
+ _rec._recTimer = setTimeout(() => {
615
+ if (_rec.isRecording && _rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
616
+ }, dur + 500);
617
+ } else {
618
+ setTimeout(startAfterSeek, 200);
619
+ }
620
+ }
621
+ setTimeout(startAfterSeek, 500);
622
  };
623
 
624
  // ===== AI TITLE =====
 
1032
  window._vtvDvrGoLive = function() {
1033
  const video = document.getElementById('vtv-player');
1034
  if (!video) return;
1035
+ _dvr.isSeeked = false;
1036
+ _dvr.seekOffset = 0;
1037
+ // Force HLS to go to live edge
1038
+ if (_hls) {
1039
+ _hls.trigger(Hls.Events.LIVE_SEEK_STATE_CHANGED, { liveSyncPosition: _hls.liveSyncPosition });
1040
+ }
1041
  if (video.buffered && video.buffered.length > 0) {
1042
+ video.currentTime = video.buffered.end(video.buffered.length - 1) - 0.5;
1043
  }
1044
  window._vtvDvrSeek(0);
1045
  const slider = document.getElementById('vtv-dvr-slider');
1046
  if (slider) slider.value = 0;
 
 
1047
  };
1048
 
1049
  // ===== MAIN PLAYER =====
 
1106
  if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
1107
  const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
1108
  if(typeof Hls!=='undefined'&&Hls.isSupported()){
1109
+ const hls=new Hls({enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,maxBufferLength:600,maxMaxBufferLength:600,liveSyncDurationCount:30,liveMaxLatencyDurationCount:60,liveDurationInfinity:true,lowLatencyMode:false,fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
1110
  hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
1111
  let rec=0; 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);}}});
1112
  }else if(video.canPlayType('application/vnd.apple.mpegurl')){