bep40 commited on
Commit
3fe63b0
·
verified ·
1 Parent(s): be6ca46

Fix VTV: load 1 lần + EPG UI + HLS stability

Browse files
Files changed (1) hide show
  1. static/yt_live.js +78 -14
static/yt_live.js CHANGED
@@ -1,10 +1,12 @@
1
- // === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
2
  // Uses backend /api/vtv/streams for stream URLs
3
- // VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
 
4
 
5
  (function(){
6
- if(window._ytLiveLoaded) return;
7
- window._ytLiveLoaded = true;
 
8
 
9
  const CHANNELS = [
10
  {id:'vtv1', name:'VTV1', badge:'Tin tức'},
@@ -20,14 +22,15 @@
20
  {id:'vtvprime', name:'VTVPrime', badge:'Prime'},
21
  ];
22
 
23
- // fptplay URLs that need proxying (can't play directly due to hotlink protection)
24
  const NEEDS_PROXY = /fptplay\.net/;
25
-
26
  const STREAMS = {};
27
  let _currentCh = null;
28
  let _hls = null;
29
  let _loading = false;
 
 
30
 
 
31
  const s = document.createElement('style');
32
  s.textContent = `
33
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
@@ -48,9 +51,56 @@
48
  .vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}
49
  .vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
50
  @keyframes vtvspin{to{transform:rotate(360deg)}}
 
 
 
 
 
 
 
 
 
 
 
 
51
  `;
52
  document.head.appendChild(s);
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  async function loadAllStreams(){
55
  if(_loading) return;
56
  _loading = true;
@@ -65,7 +115,6 @@
65
  const info = data[ch.id];
66
  if(info && info.stream_url){
67
  let url = info.stream_url;
68
- // If it's a fptplay URL, route through our proxy
69
  if(NEEDS_PROXY.test(url)){
70
  url = '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(url);
71
  }
@@ -79,7 +128,6 @@
79
  console.warn('VTV API error:', e);
80
  }
81
 
82
- // Update UI tabs
83
  CHANNELS.forEach(ch => {
84
  const tab = document.getElementById('vtvt-'+ch.id);
85
  if(tab){
@@ -114,6 +162,7 @@
114
  return w;
115
  }
116
 
 
117
  function pinBlock(){
118
  const h = document.getElementById('view-home');
119
  if(!h || document.getElementById('vtv-block')) return;
@@ -132,7 +181,7 @@
132
  if(_currentCh) window._vtvPlay(_currentCh);
133
  };
134
 
135
- window._vtvPlay = function(chId){
136
  const ch = CHANNELS.find(c => c.id === chId);
137
  if(!ch) return;
138
  _currentCh = chId;
@@ -159,6 +208,18 @@
159
  }
160
  return;
161
  }
 
 
 
 
 
 
 
 
 
 
 
 
162
  _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
163
  };
164
 
@@ -175,10 +236,14 @@
175
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
176
  const hls = new Hls({
177
  enableWorker: true,
178
- lowLatencyMode: true,
179
  startLevel: -1,
180
  capLevelToPlayerSize: true,
181
- maxBufferLength: 20,
 
 
 
 
182
  xhrSetup: function(xhr, url){
183
  if(url.includes('fptplay')){
184
  xhr.setRequestHeader('Referer', 'https://fptplay.vn/');
@@ -232,6 +297,7 @@
232
  }
233
  }
234
 
 
235
  const orig = window.loadHome;
236
  if(orig && !orig.__vtvWrapped){
237
  window.loadHome = async function(){
@@ -241,6 +307,4 @@
241
  };
242
  window.loadHome.__vtvWrapped = true;
243
  }
244
- if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(pinBlock, 500));
245
- else setTimeout(pinBlock, 500);
246
- })();
 
1
+ // === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS + EPG ===
2
  // Uses backend /api/vtv/streams for stream URLs
3
+ // Uses backend /api/vtv/epg/{channel_id} for EPG schedule
4
+ // FIX: Load only once, no double-load
5
 
6
  (function(){
7
+ // Strict single-load guard
8
+ if(window._vtvLoaded) return;
9
+ window._vtvLoaded = true;
10
 
11
  const CHANNELS = [
12
  {id:'vtv1', name:'VTV1', badge:'Tin tức'},
 
22
  {id:'vtvprime', name:'VTVPrime', badge:'Prime'},
23
  ];
24
 
 
25
  const NEEDS_PROXY = /fptplay\.net/;
 
26
  const STREAMS = {};
27
  let _currentCh = null;
28
  let _hls = null;
29
  let _loading = false;
30
+ let _epgVisible = true;
31
+ let _epgData = {};
32
 
33
+ // Inject CSS once
34
  const s = document.createElement('style');
35
  s.textContent = `
36
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
 
51
  .vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}
52
  .vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
53
  @keyframes vtvspin{to{transform:rotate(360deg)}}
54
+ .vtv-epg{padding:6px 10px;background:#0a1628;border-top:1px solid #1a2a3a}
55
+ .vtv-epg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
56
+ .vtv-epg-title{font-size:10px;font-weight:700;color:#00ccff}
57
+ .vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
58
+ .vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
59
+ .vtv-epg-list::-webkit-scrollbar{display:none}
60
+ .vtv-epg-item{flex:0 0 auto;padding:3px 6px;background:#1a2a3a;border-radius:4px;font-size:8px;color:#8ab4d8;white-space:nowrap;cursor:default}
61
+ .vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
62
+ .vtv-epg-item .epg-t{font-size:7px;color:#6a8aaa}
63
+ .vtv-epg-item.now .epg-t{color:#aaccee}
64
+ .vtv-epg-item .epg-n{color:#ccc;font-size:8px}
65
+ .vtv-epg-item.now .epg-n{color:#fff}
66
  `;
67
  document.head.appendChild(s);
68
 
69
+ function buildEPGHTML(chId){
70
+ const schedule = _epgData[chId] || [];
71
+ if(!schedule.length){
72
+ return '<div class="vtv-epg"><div class="vtv-epg-title">📋 Lịch phát sóng: Đang tải...</div></div>';
73
+ }
74
+ let items = '';
75
+ schedule.forEach(item => {
76
+ const cls = item.is_now ? ' now' : '';
77
+ items += `<div class="vtv-epg-item${cls}"><span class="epg-t">${item.time}</span> <span class="epg-n">${item.name}</span></div>`;
78
+ });
79
+ return `<div class="vtv-epg" id="vtv-epg">' +
80
+ '<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng</span>' +
81
+ '<button class="vtv-epg-toggle" onclick="window._vtvToggleEPG()">Ẩn/Hiện</button></div>' +
82
+ '<div class="vtv-epg-list" id="vtv-epg-list">' + items + '</div></div>';
83
+ }
84
+
85
+ window._vtvToggleEPG = function(){
86
+ _epgVisible = !_epgVisible;
87
+ const list = document.getElementById('vtv-epg-list');
88
+ if(list) list.style.display = _epgVisible ? 'flex' : 'none';
89
+ };
90
+
91
+ async function loadEPG(chId){
92
+ try {
93
+ const r = await fetch(`/api/vtv/epg/${chId}`, {signal: AbortSignal.timeout(5000)});
94
+ if(r.ok){
95
+ const data = await r.json();
96
+ _epgData[chId] = data.schedule || [];
97
+ }
98
+ } catch(e) {
99
+ console.warn('EPG fetch failed:', e);
100
+ _epgData[chId] = [];
101
+ }
102
+ }
103
+
104
  async function loadAllStreams(){
105
  if(_loading) return;
106
  _loading = true;
 
115
  const info = data[ch.id];
116
  if(info && info.stream_url){
117
  let url = info.stream_url;
 
118
  if(NEEDS_PROXY.test(url)){
119
  url = '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(url);
120
  }
 
128
  console.warn('VTV API error:', e);
129
  }
130
 
 
131
  CHANNELS.forEach(ch => {
132
  const tab = document.getElementById('vtvt-'+ch.id);
133
  if(tab){
 
162
  return w;
163
  }
164
 
165
+ // ===== PIN BLOCK — called only once =====
166
  function pinBlock(){
167
  const h = document.getElementById('view-home');
168
  if(!h || document.getElementById('vtv-block')) return;
 
181
  if(_currentCh) window._vtvPlay(_currentCh);
182
  };
183
 
184
+ window._vtvPlay = async function(chId){
185
  const ch = CHANNELS.find(c => c.id === chId);
186
  if(!ch) return;
187
  _currentCh = chId;
 
208
  }
209
  return;
210
  }
211
+
212
+ // Load EPG for this channel
213
+ await loadEPG(chId);
214
+ const epgEl = document.getElementById('vtv-epg');
215
+ if(epgEl) epgEl.remove();
216
+ const frame = document.querySelector('.vtv-frame');
217
+ if(frame){
218
+ const epgDiv = document.createElement('div');
219
+ epgDiv.innerHTML = buildEPGHTML(chId);
220
+ frame.appendChild(epgDiv.firstElementChild);
221
+ }
222
+
223
  _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
224
  };
225
 
 
236
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
237
  const hls = new Hls({
238
  enableWorker: true,
239
+ lowLatencyMode: false,
240
  startLevel: -1,
241
  capLevelToPlayerSize: true,
242
+ maxBufferLength: 60,
243
+ backBufferLength: 30,
244
+ maxBufferHole: 0.5,
245
+ nudgeOffset: 0.1,
246
+ fragLoadingMaxRetry: 6,
247
  xhrSetup: function(xhr, url){
248
  if(url.includes('fptplay')){
249
  xhr.setRequestHeader('Referer', 'https://fptplay.vn/');
 
297
  }
298
  }
299
 
300
+ // ===== ONLY wrap loadHome — NO DOMContentLoaded listener =====
301
  const orig = window.loadHome;
302
  if(orig && !orig.__vtvWrapped){
303
  window.loadHome = async function(){
 
307
  };
308
  window.loadHome.__vtvWrapped = true;
309
  }
310
+ })();