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

fix: Short AI 100% reload home after upload + DVR live replay controls

Browse files
Files changed (1) hide show
  1. static/yt_live.js +198 -19
static/yt_live.js CHANGED
@@ -27,6 +27,15 @@
27
  ratio: 'original', _dragMarker: null, _recTimer: null,
28
  };
29
 
 
 
 
 
 
 
 
 
 
30
  // ===== STYLES =====
31
  const style = document.createElement('style');
32
  style.textContent = `
@@ -143,6 +152,8 @@
143
  .vtv-mini-btn.x:hover{background:#900;color:#fff}
144
  .vtv-mini-peek{position:absolute;bottom:-18px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
145
  .vtv-mini.hidden .vtv-mini-peek{display:block}
 
 
146
  `;
147
  document.head.appendChild(style);
148
 
@@ -196,29 +207,41 @@
196
  hideProc();
197
  console.log('[VTV Upload] Parsed data:', data);
198
  if (data && data.ok === true) {
199
- recStatus('✅ Đã tạo Short AI! Xem trong mục Tường AI.', 'ok');
200
- // CRITICAL FIX: Update _wallPosts and refresh slider immediately
201
  if (data.post) {
 
202
  if (typeof _wallPosts !== 'undefined') {
203
  _wallPosts.unshift(data.post);
204
- console.log('[VTV Upload] Added post to _wallPosts, total:', _wallPosts.length);
205
- }
206
- // Refresh Short AI slider on home view
207
- if (typeof refreshShortAISlider === 'function') {
208
- setTimeout(() => {
209
- refreshShortAISlider();
210
- console.log('[VTV Upload] refreshShortAISlider called');
211
- }, 300);
212
  }
213
- // Switch to home so user can see the new slide
214
- if (typeof switchCat === 'function') {
 
 
 
215
  setTimeout(() => {
216
- switchCat('home');
 
 
217
  setTimeout(() => {
218
- const s = document.getElementById('short-ai-section');
219
- if (s) s.scrollIntoView({behavior:'smooth', block:'start'});
220
- }, 500);
221
- }, 800);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
  }
224
  return data;
@@ -809,6 +832,162 @@
809
  mini.classList.add('show'); mini.classList.remove('hidden');
810
  }
811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  // ===== MAIN PLAYER =====
813
  async function loadAllStreams(){
814
  if(_loading) return; _loading=true;
@@ -841,7 +1020,7 @@
841
  function pinBlock(){
842
  const h=document.getElementById('view-home'); if(!h||document.getElementById('vtv-block')) return;
843
  h.insertBefore(buildBlock(),h.firstChild); _blockInserted=true;
844
- setTimeout(()=>{setupPinButton();buildInlineRecorder();},500);
845
  if(!_streamsLoaded){loadAllStreams().then(()=>{setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL),300);});}
846
  }
847
 
@@ -869,7 +1048,7 @@
869
  if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
870
  const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
871
  if(typeof Hls!=='undefined'&&Hls.isSupported()){
872
- const hls=new Hls({enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,maxBufferLength:6,maxMaxBufferLength:10,liveSyncDurationCount:2,liveMaxLatencyDurationCount:3,fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
873
  hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
874
  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);}}});
875
  }else if(video.canPlayType('application/vnd.apple.mpegurl')){
 
27
  ratio: 'original', _dragMarker: null, _recTimer: null,
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 =====
40
  const style = document.createElement('style');
41
  style.textContent = `
 
152
  .vtv-mini-btn.x:hover{background:#900;color:#fff}
153
  .vtv-mini-peek{position:absolute;bottom:-18px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
154
  .vtv-mini.hidden .vtv-mini-peek{display:block}
155
+ #vtv-dvr-slider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#5cb87a;cursor:pointer}
156
+ #vtv-dvr-slider::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#5cb87a;cursor:pointer;border:none}
157
  `;
158
  document.head.appendChild(style);
159
 
 
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;
 
832
  mini.classList.add('show'); mini.classList.remove('hidden');
833
  }
834
 
835
+ // ===== DVR / LIVE REPLAY =====
836
+ // Add replay controls to VTV player
837
+ function setupDvrControls() {
838
+ const controls = document.querySelector('.vtv-controls');
839
+ if (!controls || document.getElementById('vtv-dvr-btn')) return;
840
+
841
+ const dvrBtn = document.createElement('button');
842
+ dvrBtn.id = 'vtv-dvr-btn';
843
+ dvrBtn.className = 'vtv-pip-btn';
844
+ dvrBtn.style.cssText = 'background:#1a3a1a;border-color:#2d6a2d;color:#5cb87a;';
845
+ dvrBtn.innerHTML = '⏪ Xem lại';
846
+ dvrBtn.title = 'Xem lại LIVE từ thời điểm mở kênh';
847
+ dvrBtn.onclick = toggleDvrPanel;
848
+ controls.insertBefore(dvrBtn, controls.firstChild);
849
+
850
+ // DVR panel (hidden by default)
851
+ const dvrPanel = document.createElement('div');
852
+ dvrPanel.id = 'vtv-dvr-panel';
853
+ dvrPanel.style.cssText = 'display:none;padding:6px 10px;background:#0a1a0a;border-top:1px solid #1a3a1a;';
854
+ dvrPanel.innerHTML = `
855
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:4px">
856
+ <span style="font-size:9px;color:#5cb87a;font-weight:700">⏪ Xem lại LIVE</span>
857
+ <span id="vtv-dvr-time" style="font-size:9px;color:#888;margin-left:auto">00:00 trước</span>
858
+ </div>
859
+ <input type="range" id="vtv-dvr-slider" min="0" max="1800" value="0" step="10"
860
+ style="width:100%;height:4px;-webkit-appearance:none;background:#1a3a1a;border-radius:2px;cursor:pointer"
861
+ oninput="window._vtvDvrSeek(this.value)">
862
+ <div style="display:flex;justify-content:space-between;margin-top:2px">
863
+ <span style="font-size:8px;color:#666">LIVE</span>
864
+ <span style="font-size:8px;color:#666">30 phút trước</span>
865
+ </div>
866
+ <div style="display:flex;gap:4px;margin-top:4px">
867
+ <button onclick="window._vtvDvrJump(-60)" style="flex:1;padding:4px;background:#1a2a1a;border:1px solid #2d6a2d;border-radius:4px;color:#5cb87a;font-size:9px;cursor:pointer">⏪ 1 phút</button>
868
+ <button onclick="window._vtvDvrJump(-300)" style="flex:1;padding:4px;background:#1a2a1a;border:1px solid #2d6a2d;border-radius:4px;color:#5cb87a;font-size:9px;cursor:pointer">⏪ 5 phút</button>
869
+ <button onclick="window._vtvDvrJump(-600)" style="flex:1;padding:4px;background:#1a2a1a;border:1px solid #2d6a2d;border-radius:4px;color:#5cb87a;font-size:9px;cursor:pointer">⏪ 10 phút</button>
870
+ <button onclick="window._vtvDvrGoLive()" style="flex:1;padding:4px;background:#1a3a1a;border:1px solid #2d6a2d;border-radius:4px;color:#5cb87a;font-size:9px;cursor:pointer;font-weight:700">🔴 LIVE</button>
871
+ </div>
872
+ `;
873
+ controls.parentNode.insertBefore(dvrPanel, controls.nextSibling);
874
+ }
875
+
876
+ function toggleDvrPanel() {
877
+ const panel = document.getElementById('vtv-dvr-panel');
878
+ const btn = document.getElementById('vtv-dvr-btn');
879
+ if (!panel) return;
880
+ if (panel.style.display === 'none') {
881
+ panel.style.display = 'block';
882
+ if (btn) { btn.style.background = '#2d6a2d'; btn.style.color = '#fff'; }
883
+ updateDvrSliderMax();
884
+ } else {
885
+ panel.style.display = 'none';
886
+ if (btn) { btn.style.background = '#1a3a1a'; btn.style.color = '#5cb87a'; }
887
+ }
888
+ }
889
+
890
+ function updateDvrSliderMax() {
891
+ const slider = document.getElementById('vtv-dvr-slider');
892
+ const video = document.getElementById('vtv-player');
893
+ if (!slider || !video) return;
894
+ // Calculate how much buffer we have
895
+ try {
896
+ if (video.buffered && video.buffered.length > 0) {
897
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
898
+ const currentTime = video.currentTime;
899
+ const bufferAhead = bufferedEnd - currentTime;
900
+ // Max seek back is min(30min, available buffer)
901
+ const maxSeek = Math.min(1800, Math.floor(bufferAhead));
902
+ slider.max = maxSeek;
903
+ }
904
+ } catch(e) {}
905
+ }
906
+
907
+ // Update DVR time display periodically
908
+ setInterval(() => {
909
+ const video = document.getElementById('vtv-player');
910
+ const slider = document.getElementById('vtv-dvr-slider');
911
+ const timeEl = document.getElementById('vtv-dvr-time');
912
+ if (!video || !slider || !timeEl) return;
913
+ const panel = document.getElementById('vtv-dvr-panel');
914
+ if (!panel || panel.style.display === 'none') return;
915
+
916
+ updateDvrSliderMax();
917
+
918
+ if (video.buffered && video.buffered.length > 0) {
919
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
920
+ const currentTime = video.currentTime;
921
+ const behind = Math.floor(bufferedEnd - currentTime);
922
+ if (behind > 5) {
923
+ _dvr.isSeeked = true;
924
+ _dvr.seekOffset = behind;
925
+ const mins = Math.floor(behind / 60);
926
+ const secs = behind % 60;
927
+ timeEl.textContent = `${mins}:${String(secs).padStart(2,'0')} trước LIVE`;
928
+ timeEl.style.color = '#e67e22';
929
+ slider.value = behind;
930
+ } else {
931
+ _dvr.isSeeked = false;
932
+ _dvr.seekOffset = 0;
933
+ timeEl.textContent = '🔴 LIVE';
934
+ timeEl.style.color = '#e74c3c';
935
+ slider.value = 0;
936
+ }
937
+ }
938
+ }, 2000);
939
+
940
+ window._vtvDvrSeek = function(val) {
941
+ const video = document.getElementById('vtv-player');
942
+ if (!video) return;
943
+ const behind = parseInt(val);
944
+ if (video.buffered && video.buffered.length > 0) {
945
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
946
+ const targetTime = bufferedEnd - behind;
947
+ if (targetTime >= 0) {
948
+ video.currentTime = targetTime;
949
+ _dvr.seekOffset = behind;
950
+ _dvr.isSeeked = behind > 5;
951
+ }
952
+ }
953
+ const timeEl = document.getElementById('vtv-dvr-time');
954
+ if (timeEl) {
955
+ if (behind > 0) {
956
+ const mins = Math.floor(behind / 60);
957
+ const secs = behind % 60;
958
+ timeEl.textContent = `${mins}:${String(secs).padStart(2,'0')} trước LIVE`;
959
+ timeEl.style.color = '#e67e22';
960
+ } else {
961
+ timeEl.textContent = '🔴 LIVE';
962
+ timeEl.style.color = '#e74c3c';
963
+ }
964
+ }
965
+ };
966
+
967
+ window._vtvDvrJump = function(sec) {
968
+ const video = document.getElementById('vtv-player');
969
+ if (!video) return;
970
+ // sec is negative (go back)
971
+ const newBehind = Math.min(1800, Math.max(0, _dvr.seekOffset - sec));
972
+ window._vtvDvrSeek(newBehind);
973
+ // Also update slider
974
+ const slider = document.getElementById('vtv-dvr-slider');
975
+ if (slider) slider.value = newBehind;
976
+ };
977
+
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 =====
992
  async function loadAllStreams(){
993
  if(_loading) return; _loading=true;
 
1020
  function pinBlock(){
1021
  const h=document.getElementById('view-home'); if(!h||document.getElementById('vtv-block')) return;
1022
  h.insertBefore(buildBlock(),h.firstChild); _blockInserted=true;
1023
+ setTimeout(()=>{setupPinButton();setupDvrControls();buildInlineRecorder();},500);
1024
  if(!_streamsLoaded){loadAllStreams().then(()=>{setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL),300);});}
1025
  }
1026
 
 
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')){