Spaces:
Running
Running
v11: Fix DVR snap-back — override hls.startLoad
Browse filesGốc rễ: HLS.js startLoad(-1) tự động seek về live edge mỗi khi load segment → user seek back bị snap về LIVE liên tục.
Giải pháp: Override hls.startLoad method:
- Lần đầu load: startLoad(-1) bình thường → stream phát từ live edge
- Sau khi user seek back (_dvrHasUserSeeked=true): startLoad(cur-5) thay vì startLoad(-1) → load segment từ vị trí hiện tại, KHÔNG seek về live
- Nút LIVE: reset _dvrHasUserSeeked=false → lần sau startLoad(-1) hoạt động bình thường
- timeupdate: tự động clear _dvrHasUserSeeked khi playhead chạm live edge
→ Không cần intercept seeked (tránh loop), không cần liveSyncDurationCount nhỏ (tránh snap). Chặn ngay từ gốc.
- static/yt_live.js +51 -28
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;
|
|
@@ -23,6 +23,8 @@
|
|
| 23 |
let _dvrSeekBarTimer = null;
|
| 24 |
let _dvrUserSeeking = false; // true while user is dragging DVR seek handle
|
| 25 |
let _dvrLastSeenEdge = false; // track if playhead was at live edge before seek
|
|
|
|
|
|
|
| 26 |
|
| 27 |
// ===== RECORDER STATE =====
|
| 28 |
const _rec = {
|
|
@@ -684,12 +686,14 @@
|
|
| 684 |
const bufferStart = Math.max(0, duration - DVR_BUFFER_SECONDS);
|
| 685 |
const seekTime = bufferStart + pct * (duration - bufferStart);
|
| 686 |
_dvrUserSeeking = !isRelease;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
video.currentTime = seekTime;
|
| 688 |
if (isRelease) {
|
| 689 |
-
// Resume playback after seek release
|
| 690 |
video.play().catch(()=>{});
|
| 691 |
} else {
|
| 692 |
-
// Pause while dragging for smooth scrubbing
|
| 693 |
video.pause();
|
| 694 |
}
|
| 695 |
updateDVRUI();
|
|
@@ -711,6 +715,7 @@
|
|
| 711 |
const video = document.getElementById('vtv-player');
|
| 712 |
if (video && _hls) {
|
| 713 |
_dvrUserSeeking = false;
|
|
|
|
| 714 |
video.currentTime = video.duration;
|
| 715 |
video.play().catch(()=>{});
|
| 716 |
updateDVRUI();
|
|
@@ -721,13 +726,17 @@
|
|
| 721 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 722 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 723 |
|
| 724 |
-
//
|
| 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,46 +853,60 @@
|
|
| 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 |
-
//
|
| 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:
|
| 853 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 854 |
});
|
| 855 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 856 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 865 |
-
//
|
| 866 |
-
|
| 867 |
-
|
| 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 |
-
|
| 878 |
-
|
| 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;
|
|
|
|
| 1 |
+
// === VNEWS — VTV LIVE + Inline Recorder v11 ===
|
| 2 |
// Features: DVR 5-min rewind buffer (seek & stay), PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
+
// v11: Fix DVR snap-back — override hls.startLoad to block auto-live-snap. Only seek to live on initial load or LIVE btn click
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
|
|
| 23 |
let _dvrSeekBarTimer = null;
|
| 24 |
let _dvrUserSeeking = false; // true while user is dragging DVR seek handle
|
| 25 |
let _dvrLastSeenEdge = false; // track if playhead was at live edge before seek
|
| 26 |
+
let _dvrHasUserSeeked = false; // true once user manually seeks back from live
|
| 27 |
+
let _dvrStartLoadOrig = null; // original hls.startLoad reference
|
| 28 |
|
| 29 |
// ===== RECORDER STATE =====
|
| 30 |
const _rec = {
|
|
|
|
| 686 |
const bufferStart = Math.max(0, duration - DVR_BUFFER_SECONDS);
|
| 687 |
const seekTime = bufferStart + pct * (duration - bufferStart);
|
| 688 |
_dvrUserSeeking = !isRelease;
|
| 689 |
+
// v11: Mark as user-seeked if not at live edge
|
| 690 |
+
if (duration - seekTime > 5) {
|
| 691 |
+
_dvrHasUserSeeked = true;
|
| 692 |
+
}
|
| 693 |
video.currentTime = seekTime;
|
| 694 |
if (isRelease) {
|
|
|
|
| 695 |
video.play().catch(()=>{});
|
| 696 |
} else {
|
|
|
|
| 697 |
video.pause();
|
| 698 |
}
|
| 699 |
updateDVRUI();
|
|
|
|
| 715 |
const video = document.getElementById('vtv-player');
|
| 716 |
if (video && _hls) {
|
| 717 |
_dvrUserSeeking = false;
|
| 718 |
+
_dvrHasUserSeeked = false; // reset — allow auto-live again
|
| 719 |
video.currentTime = video.duration;
|
| 720 |
video.play().catch(()=>{});
|
| 721 |
updateDVRUI();
|
|
|
|
| 726 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 727 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 728 |
|
| 729 |
+
// v11: Track live edge state; auto-clear _dvrHasUserSeeked when buffer catches up
|
| 730 |
video.addEventListener('timeupdate', function() {
|
| 731 |
if (!_hls) return;
|
| 732 |
const dur = video.duration;
|
| 733 |
if (!dur || !isFinite(dur)) return;
|
| 734 |
const behind = dur - video.currentTime;
|
| 735 |
_dvrLastSeenEdge = (behind < 2);
|
| 736 |
+
// If playhead reached live edge naturally, allow auto-live again
|
| 737 |
+
if (behind < 1 && _dvrHasUserSeeked) {
|
| 738 |
+
_dvrHasUserSeeked = false;
|
| 739 |
+
}
|
| 740 |
});
|
| 741 |
}
|
| 742 |
|
|
|
|
| 853 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 854 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 855 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 856 |
+
// v11: DVR 5min — override hls.startLoad to prevent auto-live-snap
|
| 857 |
const hls=new Hls({
|
| 858 |
enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,
|
| 859 |
maxBufferLength:120,maxMaxBufferLength:180,
|
| 860 |
liveBackBufferLength:DVR_BUFFER_SECONDS,
|
| 861 |
+
liveSyncDurationCount:999999,liveMaxLatencyDurationCount:999999,
|
| 862 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 863 |
});
|
| 864 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 865 |
+
|
| 866 |
+
// v11: Override startLoad — only seek to live on initial load, never auto-snap back
|
| 867 |
+
_dvrStartLoadOrig = hls.startLoad.bind(hls);
|
| 868 |
+
hls.startLoad = function(position) {
|
| 869 |
+
// First call (initial load): seek to live edge
|
| 870 |
+
if (!_dvrHasUserSeeked && position === -1) {
|
| 871 |
+
_dvrStartLoadOrig(position);
|
| 872 |
+
return;
|
| 873 |
+
}
|
| 874 |
+
// After user sought back: load segments but DON'T seek to live edge
|
| 875 |
+
// Pass a large negative value to load from buffer start instead
|
| 876 |
+
if (_dvrHasUserSeeked && position === -1) {
|
| 877 |
+
// Load from current buffer position, not live edge
|
| 878 |
+
const dur = video.duration;
|
| 879 |
+
const cur = video.currentTime;
|
| 880 |
+
if (dur && isFinite(dur) && cur && isFinite(cur)) {
|
| 881 |
+
// Load from slightly behind current position to ensure smooth playback
|
| 882 |
+
_dvrStartLoadOrig(cur - 5);
|
| 883 |
+
} else {
|
| 884 |
+
_dvrStartLoadOrig(position);
|
| 885 |
+
}
|
| 886 |
+
return;
|
| 887 |
+
}
|
| 888 |
+
_dvrStartLoadOrig(position);
|
| 889 |
+
};
|
| 890 |
+
|
| 891 |
+
// v11: After manifest loaded, seek to live edge first, then play
|
| 892 |
hls.on(Hls.Events.MANIFEST_PARSED,(ev,data)=>{
|
| 893 |
loadEl.style.display='none';video.style.display='block';
|
|
|
|
| 894 |
const dur = video.duration;
|
| 895 |
if(dur && isFinite(dur)){ video.currentTime = dur; }
|
| 896 |
video.play().catch(()=>{});
|
| 897 |
});
|
| 898 |
+
|
| 899 |
+
// v11: Track when user manually seeks back from live
|
| 900 |
+
video.addEventListener('seeking', function() {
|
| 901 |
+
if (_dvrUserSeeking) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
const dur = video.duration;
|
| 903 |
+
if (!dur || !isFinite(dur)) return;
|
| 904 |
const behind = dur - video.currentTime;
|
| 905 |
+
if (behind > 5) {
|
| 906 |
+
_dvrHasUserSeeked = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
}
|
| 908 |
});
|
| 909 |
+
|
| 910 |
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);}}});
|
| 911 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 912 |
video.src=src;
|