bep40 commited on
Commit
7eee336
·
verified ·
1 Parent(s): ba8a348

Rewrite VTV player: parse M3U from 3 IPTV sources with HLS.js failover

Browse files
Files changed (1) hide show
  1. static/yt_live.js +187 -140
static/yt_live.js CHANGED
@@ -1,32 +1,39 @@
1
- // === VNEWS — VTV1-VTV10 + VTV Cần Thơ LIVE CHANNELS ===
2
- // Fetches stream URLs from backend API (which scrapes xemtv.net PHP endpoints)
3
- // Backend proxy adds proper Referer/Origin headers for fptplay CDN
4
- // VTV6 = su-kien-03 (event channel), VTV10 = VTV Cần Thơ
 
 
5
 
6
  (function(){
7
- // Channel definitions — stream URLs fetched from backend
 
 
8
  const CHANNELS = [
9
- {id:'vtv1', name:'VTV1', icon:'🔴'},
10
- {id:'vtv2', name:'VTV2', icon:'🟠'},
11
- {id:'vtv3', name:'VTV3', icon:'🟡'},
12
- {id:'vtv4', name:'VTV4', icon:'🟢'},
13
- {id:'vtv5', name:'VTV5', icon:'🔵'},
14
- {id:'vtv6', name:'VTV6', icon:'🟣'},
15
- {id:'vtv7', name:'VTV7', icon:''},
16
- {id:'vtv8', name:'VTV8', icon:'🟤'},
17
- {id:'vtv9', name:'VTV9', icon:'🔴'},
18
- {id:'vtv10', name:'VTV Cần Thơ', icon:'🟡'},
19
  ];
20
 
21
- // Fetched stream URLs (populated by _fetchStreams)
22
- const STREAMS = {};
 
 
 
23
 
 
24
  let _currentCh = null;
25
  let _hls = null;
26
- let _fetching = false;
27
 
28
- if(document.getElementById('vtv-css')) return;
29
- const s=document.createElement('style'); s.id='vtv-css';
30
  s.textContent=`
31
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
32
  .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
@@ -38,7 +45,7 @@
38
  .vtv-tab{padding:4px 8px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:10px;color:#8ab4d8;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
39
  .vtv-tab:hover{background:#0b4a7a;color:#fff}
40
  .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
41
- .vtv-tab.off{opacity:.4;pointer-events:none}
42
  .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
43
  .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
44
  .vtv-err{display:flex;align-items:center;justify-content:center;height:180px;color:#888;font-size:12px;text-align:center;padding:20px;flex-direction:column;gap:8px}
@@ -49,69 +56,109 @@
49
  `;
50
  document.head.appendChild(s);
51
 
52
- function buildBlock(){
53
- const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
54
- let tabs='';
55
- CHANNELS.forEach(ch=>{
56
- tabs+=`<button class="vtv-tab off" id="vtvt-${ch.id}" onclick="window._vtvPlay('${ch.id}')">${ch.icon} ${ch.name}</button>`;
57
- });
58
- w.innerHTML=
59
- '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
60
- '<div class="vtv-tabs">'+tabs+'</div>'+
61
- '<div class="vtv-frame">'+
62
- '<div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải danh sách kênh...</div>'+
63
- '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>'+
64
- '<div class="vtv-err" id="vtv-err" style="display:none"><span id="vtv-err-msg">Không thể tải kênh</span><button onclick="window._vtvRetry()">Thử lại</button></div>'+
65
- '</div>';
66
- return w;
 
 
 
 
67
  }
68
 
69
- function pinBlock(){
70
- const h=document.getElementById('view-home');
71
- if(!h||document.getElementById('vtv-block')) return;
72
- h.insertBefore(buildBlock(), h.firstChild);
73
- _fetchStreams();
 
 
 
 
74
  }
75
 
76
- // Fetch stream URLs from backend
77
- window._fetchVtvStreams = async function(){
78
- if(_fetching) return;
79
- _fetching = true;
80
  try{
81
- const r = await fetch('/api/vtv/streams');
82
- if(r.ok){
83
- const data = await r.json();
84
- if(data.channels){
85
- data.channels.forEach(ch=>{
86
- if(ch.stream_url) STREAMS[ch.id] = ch.stream_url;
87
- });
88
- // Enable tabs for channels that have streams
89
- CHANNELS.forEach(ch=>{
90
- const tab = document.getElementById('vtvt-'+ch.id);
91
- if(tab){
92
- if(STREAMS[ch.id]){
93
- tab.classList.remove('off');
94
- } else {
95
- tab.textContent = ch.icon + ' ' + ch.name + ' ✕';
96
- tab.style.opacity = '0.4';
 
 
 
97
  }
 
98
  }
99
- });
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
  }
102
- }catch(e){
103
- console.warn('VTV streams fetch failed:', e);
104
- }
105
- _fetching = false;
106
- };
107
 
108
- function _fetchStreams(){
109
- window._fetchVtvStreams().then(()=>{
110
- // Auto-play VTV3 if available
111
- if(STREAMS['vtv3']){
112
- setTimeout(()=>window._vtvPlay('vtv3'), 500);
113
- } else if(STREAMS['vtv1']){
114
- setTimeout(()=>window._vtvPlay('vtv1'), 500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  }
116
  });
117
  }
@@ -120,98 +167,98 @@
120
  if(_currentCh) window._vtvPlay(_currentCh);
121
  };
122
 
123
- window._vtvPlay = async function(chId){
124
- const ch = CHANNELS.find(c=>c.id===chId);
125
  if(!ch) return;
126
  _currentCh = chId;
127
-
128
- document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
129
- const tab=document.getElementById('vtvt-'+chId);
130
  if(tab) tab.classList.add('on');
131
-
132
- const video=document.getElementById('vtv-player');
133
- const errEl=document.getElementById('vtv-err');
134
- const loadEl=document.getElementById('vtv-load');
135
- const errMsg=document.getElementById('vtv-err-msg');
136
- video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
137
- loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
138
-
139
- if(_hls){_hls.destroy(); _hls=null;}
140
-
141
- // Get stream URL — fetch fresh if not cached
142
- let streamUrl = STREAMS[chId];
143
- if(!streamUrl){
144
- try{
145
- const r = await fetch('/api/vtv/stream/'+encodeURIComponent(ch.name));
146
- if(r.ok){
147
- const d = await r.json();
148
- streamUrl = d.stream_url;
149
- if(streamUrl) STREAMS[chId] = streamUrl;
150
- }
151
- }catch(e){}
152
- }
153
-
154
- if(!streamUrl){
155
- loadEl.style.display='none'; errEl.style.display='flex';
156
  errMsg.textContent = ch.name + ': Không tìm thấy luồng. Thử lại sau.';
157
  return;
158
  }
159
-
160
- // Use our proxy endpoint to fetch stream with proper Referer
161
- const proxyUrl = '/api/proxy/hls?url='+encodeURIComponent(streamUrl);
162
- _playStream(video, proxyUrl, ch.name, loadEl, errEl, errMsg);
163
  };
164
 
165
- function _playStream(video, src, name, loadEl, errEl, errMsg){
166
- if(typeof Hls!=='undefined' && Hls.isSupported()){
 
 
 
 
 
 
 
 
 
167
  const hls = new Hls({
168
- enableWorker:true,
169
- lowLatencyMode:true,
170
- startLevel:-1,
171
- capLevelToPlayerSize:true,
172
- maxBufferLength:20,
173
  xhrSetup: function(xhr, url){
174
- xhr.setRequestHeader('Referer', 'https://fptplay.vn/');
175
- xhr.setRequestHeader('Origin', 'https://fptplay.vn');
 
 
176
  }
177
  });
178
  _hls = hls;
179
  hls.loadSource(src);
180
  hls.attachMedia(video);
181
- hls.on(Hls.Events.MANIFEST_PARSED, ()=>{
182
- video.play().catch(()=>{});
183
- loadEl.style.display='none'; video.style.display='block';
 
184
  });
185
- hls.on(Hls.Events.ERROR, (ev, data)=>{
186
  if(data.fatal){
187
- if(data.type===Hls.ErrorTypes.NETWORK_ERROR){
188
- setTimeout(()=>hls.startLoad(), 2000);
189
- } else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){
 
 
 
 
190
  hls.recoverMediaError();
191
  } else {
192
- hls.destroy(); _hls=null;
193
- loadEl.style.display='none'; errEl.style.display='flex';
194
- errMsg.textContent=name+': Luồng bị gián đoạn. Thử lại sau.';
195
  }
196
  }
197
  });
198
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
199
  video.src = src;
200
- video.addEventListener('loadedmetadata',()=>{
201
- video.play().catch(()=>{});
202
- loadEl.style.display='none'; video.style.display='block';
203
- },{once:true});
204
- video.addEventListener('error',()=>{
205
- loadEl.style.display='none'; errEl.style.display='flex';
206
- errMsg.textContent=name+': Không phát được';
207
- },{once:true});
208
  } else {
209
- loadEl.style.display='none'; errEl.style.display='flex';
210
- errMsg.textContent='Trình duyệt không hỗ trợ HLS';
 
211
  }
212
  }
213
 
214
- // Wrap loadHome
215
  const orig = window.loadHome;
216
  if(orig && !orig.__vtvWrapped){
217
  window.loadHome = async function(){
@@ -221,6 +268,6 @@
221
  };
222
  window.loadHome.__vtvWrapped = true;
223
  }
224
- if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',()=>setTimeout(pinBlock,500));
225
- else setTimeout(pinBlock,500);
226
  })();
 
1
+ // === VNEWS — VTV1-VTV10 LIVE CHANNELS ===
2
+ // Parses M3U playlists from 3 public IPTV sources:
3
+ // 1. iptv-org/iptv (streams/vn.m3u) primary, community-maintained
4
+ // 2. Love4vn/Test (VTV.m3u) VTV-specific backup
5
+ // 3. Ykcitus/IPTV (vietnam.m3u) — general backup
6
+ // Uses HLS.js (loaded in index_v2.html) with auto-failover between sources
7
 
8
  (function(){
9
+ if(window._ytLiveLoaded) return;
10
+ window._ytLiveLoaded = true;
11
+
12
  const CHANNELS = [
13
+ {id:'vtv1', name:'VTV1', patterns:['VTV1']},
14
+ {id:'vtv2', name:'VTV2', patterns:['VTV2']},
15
+ {id:'vtv3', name:'VTV3', patterns:['VTV3']},
16
+ {id:'vtv4', name:'VTV4', patterns:['VTV4']},
17
+ {id:'vtv5', name:'VTV5', patterns:['VTV5','VTV5HD']},
18
+ {id:'vtv6', name:'VTV6', patterns:['VTV6']},
19
+ {id:'vtv7', name:'VTV7', patterns:['VTV7']},
20
+ {id:'vtv8', name:'VTV8', patterns:['VTV8']},
21
+ {id:'vtv9', name:'VTV9', patterns:['VTV9']},
22
+ {id:'vtv10', name:'VTV Cần Thơ', patterns:['VTV Cần Thơ','VTV Can Tho','VTVCT','vtvcantho','Cần Thơ']},
23
  ];
24
 
25
+ const M3U_SOURCES = [
26
+ {name:'iptv-org', url:'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/vn.m3u'},
27
+ {name:'Love4vn', url:'https://raw.githubusercontent.com/Love4vn/Test/master/VTV.m3u'},
28
+ {name:'Ykcitus', url:'https://raw.githubusercontent.com/Ykcitus/IPTV/master/vietnam.m3u'},
29
+ ];
30
 
31
+ const STREAMS = {};
32
  let _currentCh = null;
33
  let _hls = null;
34
+ let _loading = false;
35
 
36
+ const s=document.createElement('style');
 
37
  s.textContent=`
38
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
39
  .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
 
45
  .vtv-tab{padding:4px 8px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:10px;color:#8ab4d8;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
46
  .vtv-tab:hover{background:#0b4a7a;color:#fff}
47
  .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
48
+ .vtv-tab.off{opacity:.35;pointer-events:none}
49
  .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
50
  .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
51
  .vtv-err{display:flex;align-items:center;justify-content:center;height:180px;color:#888;font-size:12px;text-align:center;padding:20px;flex-direction:column;gap:8px}
 
56
  `;
57
  document.head.appendChild(s);
58
 
59
+ function parseM3U(text){
60
+ const lines = text.split('\n');
61
+ const entries = [];
62
+ let cur = null;
63
+ for(let i=0; i<lines.length; i++){
64
+ const line = lines[i].trim();
65
+ if(line.startsWith('#EXTINF:')){
66
+ cur = {info: line, url: ''};
67
+ const nameMatch = line.match(/,(.+)$/);
68
+ cur.name = nameMatch ? nameMatch[1].trim() : '';
69
+ const tvgMatch = line.match(/tvg-id="([^"]*)"/);
70
+ cur.tvgId = tvgMatch ? tvgMatch[1] : '';
71
+ } else if(line && !line.startsWith('#') && cur){
72
+ cur.url = line;
73
+ entries.push(cur);
74
+ cur = null;
75
+ }
76
+ }
77
+ return entries;
78
  }
79
 
80
+ function matchChannel(entry, channel){
81
+ const searchText = (entry.name + ' ' + entry.tvgId + ' ' + entry.info).toLowerCase();
82
+ for(const pat of channel.patterns){
83
+ if(searchText.includes(pat.toLowerCase())){
84
+ if(channel.id === 'vtv5' && (searchText.includes('tây') || searchText.includes('tay'))) continue;
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
  }
90
 
91
+ async function fetchM3U(url){
 
 
 
92
  try{
93
+ const r = await fetch(url, {signal: AbortSignal.timeout(10000)});
94
+ if(r.ok) return await r.text();
95
+ }catch(e){}
96
+ return null;
97
+ }
98
+
99
+ async function loadAllStreams(){
100
+ if(_loading) return;
101
+ _loading = true;
102
+ const results = await Promise.allSettled(M3U_SOURCES.map(src => fetchM3U(src.url)));
103
+ CHANNELS.forEach(ch => { STREAMS[ch.id] = []; });
104
+ results.forEach((result) => {
105
+ if(result.status === 'fulfilled' && result.value){
106
+ const entries = parseM3U(result.value);
107
+ CHANNELS.forEach(ch => {
108
+ for(const entry of entries){
109
+ if(matchChannel(entry, ch)){
110
+ if(entry.url && !STREAMS[ch.id].includes(entry.url)){
111
+ STREAMS[ch.id].push(entry.url);
112
  }
113
+ break;
114
  }
115
+ }
116
+ });
117
+ }
118
+ });
119
+ CHANNELS.forEach(ch => {
120
+ const tab = document.getElementById('vtvt-'+ch.id);
121
+ if(tab){
122
+ if(STREAMS[ch.id].length > 0){
123
+ tab.classList.remove('off');
124
+ } else {
125
+ tab.style.opacity = '0.35';
126
+ tab.textContent = ch.name + ' ✕';
127
  }
128
  }
129
+ });
130
+ _loading = false;
131
+ }
 
 
132
 
133
+ function buildBlock(){
134
+ const w = document.createElement('div');
135
+ w.className = 'vtv-wrap';
136
+ w.id = 'vtv-block';
137
+ let tabs = '';
138
+ CHANNELS.forEach(ch => {
139
+ tabs += '<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';
140
+ });
141
+ w.innerHTML =
142
+ '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>' +
143
+ '<div class="vtv-tabs">' + tabs + '</div>' +
144
+ '<div class="vtv-frame">' +
145
+ '<div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải danh sách kênh...</div>' +
146
+ '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>' +
147
+ '<div class="vtv-err" id="vtv-err" style="display:none"><span id="vtv-err-msg">Không thể tải kênh</span><button onclick="window._vtvRetry()">Thử lại</button></div>' +
148
+ '</div>';
149
+ return w;
150
+ }
151
+
152
+ function pinBlock(){
153
+ const h = document.getElementById('view-home');
154
+ if(!h || document.getElementById('vtv-block')) return;
155
+ h.insertBefore(buildBlock(), h.firstChild);
156
+ loadAllStreams().then(() => {
157
+ for(const ch of CHANNELS){
158
+ if(STREAMS[ch.id] && STREAMS[ch.id].length > 0){
159
+ setTimeout(() => window._vtvPlay(ch.id), 300);
160
+ break;
161
+ }
162
  }
163
  });
164
  }
 
167
  if(_currentCh) window._vtvPlay(_currentCh);
168
  };
169
 
170
+ window._vtvPlay = function(chId){
171
+ const ch = CHANNELS.find(c => c.id === chId);
172
  if(!ch) return;
173
  _currentCh = chId;
174
+ document.querySelectorAll('.vtv-tab').forEach(t => t.classList.remove('on'));
175
+ const tab = document.getElementById('vtvt-'+chId);
 
176
  if(tab) tab.classList.add('on');
177
+ const video = document.getElementById('vtv-player');
178
+ const errEl = document.getElementById('vtv-err');
179
+ const loadEl = document.getElementById('vtv-load');
180
+ const errMsg = document.getElementById('vtv-err-msg');
181
+ video.style.display = 'none';
182
+ errEl.style.display = 'none';
183
+ loadEl.style.display = 'flex';
184
+ loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + ch.name + '...';
185
+ if(_hls){ _hls.destroy(); _hls = null; }
186
+ const urls = STREAMS[chId] || [];
187
+ if(urls.length === 0){
188
+ loadEl.style.display = 'none';
189
+ errEl.style.display = 'flex';
 
 
 
 
 
 
 
 
 
 
 
 
190
  errMsg.textContent = ch.name + ': Không tìm thấy luồng. Thử lại sau.';
191
  return;
192
  }
193
+ _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
 
 
 
194
  };
195
 
196
+ function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
197
+ if(idx >= urls.length){
198
+ loadEl.style.display = 'none';
199
+ errEl.style.display = 'flex';
200
+ errMsg.textContent = name + ': Tất cả nguồn đều lỗi. Thử lại sau.';
201
+ return;
202
+ }
203
+ const src = urls[idx];
204
+ const sourceLabel = ' (' + (idx+1) + '/' + urls.length + ')';
205
+ loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + name + sourceLabel + '...';
206
+ if(typeof Hls !== 'undefined' && Hls.isSupported()){
207
  const hls = new Hls({
208
+ enableWorker: true,
209
+ lowLatencyMode: true,
210
+ startLevel: -1,
211
+ capLevelToPlayerSize: true,
212
+ maxBufferLength: 20,
213
  xhrSetup: function(xhr, url){
214
+ if(url.includes('fptplay')){
215
+ xhr.setRequestHeader('Referer', 'https://fptplay.vn/');
216
+ xhr.setRequestHeader('Origin', 'https://fptplay.vn');
217
+ }
218
  }
219
  });
220
  _hls = hls;
221
  hls.loadSource(src);
222
  hls.attachMedia(video);
223
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
224
+ video.play().catch(() => {});
225
+ loadEl.style.display = 'none';
226
+ video.style.display = 'block';
227
  });
228
+ hls.on(Hls.Events.ERROR, (ev, data) => {
229
  if(data.fatal){
230
+ if(data.type === Hls.ErrorTypes.NETWORK_ERROR){
231
+ setTimeout(() => {
232
+ hls.destroy();
233
+ _hls = null;
234
+ _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
235
+ }, 1500);
236
+ } else if(data.type === Hls.ErrorTypes.MEDIA_ERROR){
237
  hls.recoverMediaError();
238
  } else {
239
+ hls.destroy();
240
+ _hls = null;
241
+ _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
242
  }
243
  }
244
  });
245
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
246
  video.src = src;
247
+ video.addEventListener('loadedmetadata', () => {
248
+ video.play().catch(() => {});
249
+ loadEl.style.display = 'none';
250
+ video.style.display = 'block';
251
+ }, {once: true});
252
+ video.addEventListener('error', () => {
253
+ _tryPlay(video, urls, idx + 1, name, loadEl, errEl, errMsg);
254
+ }, {once: true});
255
  } else {
256
+ loadEl.style.display = 'none';
257
+ errEl.style.display = 'flex';
258
+ errMsg.textContent = 'Trình duyệt không hỗ trợ HLS';
259
  }
260
  }
261
 
 
262
  const orig = window.loadHome;
263
  if(orig && !orig.__vtvWrapped){
264
  window.loadHome = async function(){
 
268
  };
269
  window.loadHome.__vtvWrapped = true;
270
  }
271
+ if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(pinBlock, 500));
272
+ else setTimeout(pinBlock, 500);
273
  })();