Spaces:
Running
Running
v12: Fix DVR — disable HLS.js liveSync, manage live edge manually
Browse filesv11 lỗi: override startLoad(cur-5) → HLS.js không load đúng segment → stream không phát.
Giải pháp v12: Đơn giản hơn, đáng tin cậy hơn:
- liveSyncDurationCount:0, liveMaxLatencyDurationCount:0 → HLS.js KHÔNG auto-snap về live
- Lần đầu load: tự seek về live edge (video.currentTime = video.duration)
- User tua DVR: HLS.js load segment bình thường, không can thiệp
- Nút LIVE: seek về live edge
- Không override startLoad, không intercept seeked → không loop, không side-effect
- static/yt_live.js +13 -54
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,8 +23,7 @@
|
|
| 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
|
| 27 |
-
let _dvrStartLoadOrig = null; // original hls.startLoad reference
|
| 28 |
|
| 29 |
// ===== RECORDER STATE =====
|
| 30 |
const _rec = {
|
|
@@ -686,10 +685,8 @@
|
|
| 686 |
const bufferStart = Math.max(0, duration - DVR_BUFFER_SECONDS);
|
| 687 |
const seekTime = bufferStart + pct * (duration - bufferStart);
|
| 688 |
_dvrUserSeeking = !isRelease;
|
| 689 |
-
//
|
| 690 |
-
|
| 691 |
-
_dvrHasUserSeeked = true;
|
| 692 |
-
}
|
| 693 |
video.currentTime = seekTime;
|
| 694 |
if (isRelease) {
|
| 695 |
video.play().catch(()=>{});
|
|
@@ -715,7 +712,7 @@
|
|
| 715 |
const video = document.getElementById('vtv-player');
|
| 716 |
if (video && _hls) {
|
| 717 |
_dvrUserSeeking = false;
|
| 718 |
-
|
| 719 |
video.currentTime = video.duration;
|
| 720 |
video.play().catch(()=>{});
|
| 721 |
updateDVRUI();
|
|
@@ -726,17 +723,14 @@
|
|
| 726 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 727 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 728 |
|
| 729 |
-
//
|
| 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 |
-
|
| 736 |
-
|
| 737 |
-
if (behind < 1 && _dvrHasUserSeeked) {
|
| 738 |
-
_dvrHasUserSeeked = false;
|
| 739 |
-
}
|
| 740 |
});
|
| 741 |
}
|
| 742 |
|
|
@@ -853,60 +847,25 @@
|
|
| 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 |
-
//
|
| 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:
|
| 862 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 863 |
});
|
| 864 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 865 |
|
| 866 |
-
//
|
| 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;
|
|
|
|
| 1 |
+
// === VNEWS — VTV LIVE + Inline Recorder v12 ===
|
| 2 |
// Features: DVR 5-min rewind buffer (seek & stay), PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
+
// v12: Fix DVR snap-back — disable HLS.js liveSync (liveSyncDurationCount:0), manage live edge manually. No override, no intercept
|
| 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 _dvrAtLiveEdge = true; // track if playhead is at live edge
|
|
|
|
| 27 |
|
| 28 |
// ===== RECORDER STATE =====
|
| 29 |
const _rec = {
|
|
|
|
| 685 |
const bufferStart = Math.max(0, duration - DVR_BUFFER_SECONDS);
|
| 686 |
const seekTime = bufferStart + pct * (duration - bufferStart);
|
| 687 |
_dvrUserSeeking = !isRelease;
|
| 688 |
+
// v12: Track live edge state
|
| 689 |
+
_dvrAtLiveEdge = (duration - seekTime < 2);
|
|
|
|
|
|
|
| 690 |
video.currentTime = seekTime;
|
| 691 |
if (isRelease) {
|
| 692 |
video.play().catch(()=>{});
|
|
|
|
| 712 |
const video = document.getElementById('vtv-player');
|
| 713 |
if (video && _hls) {
|
| 714 |
_dvrUserSeeking = false;
|
| 715 |
+
_dvrAtLiveEdge = true;
|
| 716 |
video.currentTime = video.duration;
|
| 717 |
video.play().catch(()=>{});
|
| 718 |
updateDVRUI();
|
|
|
|
| 723 |
if (_dvrSeekBarTimer) clearInterval(_dvrSeekBarTimer);
|
| 724 |
_dvrSeekBarTimer = setInterval(updateDVRUI, 1000);
|
| 725 |
|
| 726 |
+
// v12: Track live edge state for UI
|
| 727 |
video.addEventListener('timeupdate', function() {
|
| 728 |
if (!_hls) return;
|
| 729 |
const dur = video.duration;
|
| 730 |
if (!dur || !isFinite(dur)) return;
|
| 731 |
const behind = dur - video.currentTime;
|
| 732 |
+
_dvrAtLiveEdge = (behind < 2);
|
| 733 |
+
_dvrLastSeenEdge = _dvrAtLiveEdge;
|
|
|
|
|
|
|
|
|
|
| 734 |
});
|
| 735 |
}
|
| 736 |
|
|
|
|
| 847 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 848 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 849 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 850 |
+
// v12: DVR 5min — disable HLS.js live sync entirely, manage live edge manually
|
| 851 |
const hls=new Hls({
|
| 852 |
enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,
|
| 853 |
maxBufferLength:120,maxMaxBufferLength:180,
|
| 854 |
liveBackBufferLength:DVR_BUFFER_SECONDS,
|
| 855 |
+
liveSyncDurationCount:0,liveMaxLatencyDurationCount:0,
|
| 856 |
fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000
|
| 857 |
});
|
| 858 |
_hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 859 |
|
| 860 |
+
// v12: After manifest loaded, seek to live edge first, then play
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 861 |
hls.on(Hls.Events.MANIFEST_PARSED,(ev,data)=>{
|
| 862 |
loadEl.style.display='none';video.style.display='block';
|
| 863 |
+
// Seek to live edge manually since we disabled auto-sync
|
| 864 |
const dur = video.duration;
|
| 865 |
if(dur && isFinite(dur)){ video.currentTime = dur; }
|
| 866 |
video.play().catch(()=>{});
|
| 867 |
});
|
| 868 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
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);}}});
|
| 870 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
| 871 |
video.src=src;
|