Spaces:
Running
Running
v10: Fix DVR — stream plays at live + user can seek back freely
Browse files- Khôi phục liveSyncDurationCount=3 để stream start đúng ở live edge
- MANIFEST_PARSED: seek to duration (live edge) trước khi play
- Thêm seeking/seeked interceptor: nếu HLS.js snap về live nhưng user đang xem behind → restore position
- _dvrRestoreLock chặn loop khi restore
- Giữ _dvrUserSeeking pause/resume khi drag DVR handle
- static/yt_live.js +34 -14
static/yt_live.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
// === VNEWS — VTV LIVE + Inline Recorder
|
| 2 |
// Features: DVR 5-min rewind buffer (seek & stay), PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
-
//
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
@@ -721,19 +721,13 @@
|
|
| 721 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 722 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 723 |
|
| 724 |
-
//
|
| 725 |
video.addEventListener('timeupdate', function() {
|
| 726 |
-
if (_dvrUserSeeking) return; // don't interfere during seek
|
| 727 |
if (!_hls) return;
|
| 728 |
const dur = video.duration;
|
| 729 |
if (!dur || !isFinite(dur)) return;
|
| 730 |
-
// If playhead is at the very end of buffer (live edge), keep it there
|
| 731 |
const behind = dur - video.currentTime;
|
| 732 |
-
|
| 733 |
-
_dvrLastSeenEdge = true;
|
| 734 |
-
} else {
|
| 735 |
-
_dvrLastSeenEdge = false;
|
| 736 |
-
}
|
| 737 |
});
|
| 738 |
}
|
| 739 |
|
|
@@ -850,20 +844,46 @@
|
|
| 850 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 851 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 852 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 853 |
-
//
|
| 854 |
const hls=new Hls({
|
| 855 |
enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,
|
| 856 |
maxBufferLength:120,maxMaxBufferLength:180,
|
| 857 |
liveBackBufferLength:DVR_BUFFER_SECONDS,
|
| 858 |
-
liveSyncDurationCount:
|
| 859 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 860 |
});
|
| 861 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 862 |
-
|
|
|
|
| 863 |
loadEl.style.display='none';video.style.display='block';
|
| 864 |
-
//
|
|
|
|
|
|
|
| 865 |
video.play().catch(()=>{});
|
| 866 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
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);}}});
|
| 868 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 869 |
video.src=src;
|
|
|
|
| 1 |
+
// === VNEWS — VTV LIVE + Inline Recorder v10 ===
|
| 2 |
// Features: DVR 5-min rewind buffer (seek & stay), PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
+
// v10: Fix DVR — keep liveSync for stream start, but intercept HLS auto-snap via seeking/seeked events to preserve user position
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
|
|
| 721 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 722 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 723 |
|
| 724 |
+
// v10: Track whether playhead is at live edge (for seeked event guard)
|
| 725 |
video.addEventListener('timeupdate', function() {
|
|
|
|
| 726 |
if (!_hls) return;
|
| 727 |
const dur = video.duration;
|
| 728 |
if (!dur || !isFinite(dur)) return;
|
|
|
|
| 729 |
const behind = dur - video.currentTime;
|
| 730 |
+
_dvrLastSeenEdge = (behind < 2);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
});
|
| 732 |
}
|
| 733 |
|
|
|
|
| 844 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 845 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 846 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 847 |
+
// v10: DVR 5min — keep liveSync for edge tracking, but override seek snap-back
|
| 848 |
const hls=new Hls({
|
| 849 |
enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,
|
| 850 |
maxBufferLength:120,maxMaxBufferLength:180,
|
| 851 |
liveBackBufferLength:DVR_BUFFER_SECONDS,
|
| 852 |
+
liveSyncDurationCount:3,liveMaxLatencyDurationCount:6,
|
| 853 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 854 |
});
|
| 855 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 856 |
+
// v10: After manifest loaded, seek to live edge first, then play
|
| 857 |
+
hls.on(Hls.Events.MANIFEST_PARSED,(ev,data)=>{
|
| 858 |
loadEl.style.display='none';video.style.display='block';
|
| 859 |
+
// Start at live edge so stream plays immediately
|
| 860 |
+
const dur = video.duration;
|
| 861 |
+
if(dur && isFinite(dur)){ video.currentTime = dur; }
|
| 862 |
video.play().catch(()=>{});
|
| 863 |
});
|
| 864 |
+
// (live-snap interception handled via video seeking/seeked events below)
|
| 865 |
+
// v10: Block HLS auto-live-snap — restore user position when HLS fights back
|
| 866 |
+
let _preSeekTime = 0;
|
| 867 |
+
let _dvrRestoreLock = false; // prevent restore loop
|
| 868 |
+
video.addEventListener('seeking',function(){
|
| 869 |
+
if(_dvrRestoreLock) return;
|
| 870 |
+
_preSeekTime = video.currentTime;
|
| 871 |
+
});
|
| 872 |
+
video.addEventListener('seeked',function(){
|
| 873 |
+
if(_dvrUserSeeking || _dvrRestoreLock) return;
|
| 874 |
+
const dur = video.duration;
|
| 875 |
+
if(!dur || !isFinite(dur)) return;
|
| 876 |
+
const behind = dur - video.currentTime;
|
| 877 |
+
// If HLS.js snapped to live (behind < 0.5s) but user was >3s behind, restore
|
| 878 |
+
if(behind < 0.5 && _preSeekTime > 0){
|
| 879 |
+
const userBehind = dur - _preSeekTime;
|
| 880 |
+
if(userBehind > 3){
|
| 881 |
+
_dvrRestoreLock = true;
|
| 882 |
+
video.currentTime = _preSeekTime;
|
| 883 |
+
setTimeout(()=>{ _dvrRestoreLock = false; },500);
|
| 884 |
+
}
|
| 885 |
+
}
|
| 886 |
+
});
|
| 887 |
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);}}});
|
| 888 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 889 |
video.src=src;
|