bep40 commited on
Commit
367fa5d
·
verified ·
1 Parent(s): 79e6720

Upload static/yt_live.js

Browse files
Files changed (1) hide show
  1. static/yt_live.js +157 -203
static/yt_live.js CHANGED
@@ -1,238 +1,192 @@
1
- // === VNEWS — VTV1-VTV10+ LIVE CHANNELS pinned to TOP of home page ===
2
- // Fetches stream URLs from backend API (vtv_scraper.py which scrapes xemtv.net)
3
- // Uses HLS.js (already loaded in index_v2.html) for playback
4
 
5
  (function(){
6
- const CHANNEL_ORDER = ['VTV1','VTV2','VTV3','VTV4','VTV5','VTV6','VTV7','VTV8','VTV9','VTV10'];
7
- const DISPLAY_NAMES = {
8
- 'VTV1':'VTV1', 'VTV2':'VTV2', 'VTV3':'VTV3', 'VTV4':'VTV4', 'VTV5':'VTV5',
9
- 'VTV6':'VTV Cần Thơ', 'VTV7':'VTV7', 'VTV8':'VTV8', 'VTV9':'VTV9', 'VTV10':'VTV10'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  };
11
 
12
- let _vtvChannels = [];
13
- let _hlsInstance = null;
14
- let _currentChannelIdx = 0;
15
-
16
- // Inject styles once.
17
- if(!document.getElementById('vtv-channels-css')){
18
- const s=document.createElement('style');s.id='vtv-channels-css';
19
- s.textContent=`
20
- .vtv-channels-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden;box-shadow:0 0 0 1px rgba(0,102,204,.3)}
21
- .vtv-channels-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a);flex-wrap:wrap}
22
- .vtv-channels-title{font-size:13px;font-weight:800;color:#00ccff;letter-spacing:.5px}
23
- .vtv-channel-tabs{display:flex;gap:4px;flex:1;overflow-x:auto;scrollbar-width:none;margin-top:4px;padding-bottom:4px}
24
- .vtv-channel-tabs::-webkit-scrollbar{display:none}
25
- .vtv-channel-tab{padding:4px 10px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:12px;color:#8ab4d8;font-size:10px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
26
- .vtv-channel-tab:hover{background:#0b4a7a;color:#fff}
27
- .vtv-channel-tab.active{background:#0066cc;border-color:#0066cc;color:#fff;font-weight:700}
28
- .vtv-channel-tab:disabled{opacity:.35;cursor:not-allowed}
29
- .vtv-live-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtv-live-pulse 1.3s infinite;margin-left:4px}
30
- @keyframes vtv-live-pulse{0%,100%{opacity:1}50%{opacity:.3}}
31
- .vtv-channels-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:200px}
32
- .vtv-channels-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
33
- .vtv-channels-error{display:flex;align-items:center;justify-content:center;height:100%;color:#888;font-size:12px;text-align:center;padding:20px;min-height:200px;flex-direction:column;gap:8px}
34
- .vtv-channels-error a{color:#00ccff;text-decoration:none;font-size:11px}
35
- .vtv-loading{display:flex;align-items:center;justify-content:center;height:100%;color:#00ccff;font-size:12px;min-height:200px;flex-direction:column;gap:8px}
36
- .vtv-spinner{width:24px;height:24px;border:3px solid #1a3a5a;border-top-color:#00ccff;border-radius:50%;animation:vtv-spin .8s linear infinite}
37
- @keyframes vtv-spin{to{transform:rotate(360deg)}}
38
- .vtv-channel-info{font-size:11px;color:#aaa;min-width:80px;text-align:right}
39
- `;
40
- document.head.appendChild(s);
 
 
 
 
 
 
 
 
 
 
41
  }
42
 
43
- function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
44
-
45
- function buildChannelsBlock(){
46
- const wrap=document.createElement('div');
47
- wrap.className='vtv-channels-wrap';
48
- wrap.id='vtv-channels-block';
49
-
50
- const tabsHtml = CHANNEL_ORDER.map((name, i) => {
51
- const dn = DISPLAY_NAMES[name] || name;
52
- return `<button class="vtv-channel-tab${i===0?' active':''}" data-name="${name}" data-idx="${i}" onclick="window._switchVTV(${i})">${dn}</button>`;
53
- }).join('');
54
-
55
- wrap.innerHTML=
56
- '<div class="vtv-channels-head">'
57
- +'<span class="vtv-channels-title">📺 Kênh VTV Trực Tuyến</span>'
58
- +'<span class="vtv-live-badge">● LIVE</span>'
59
- +'</div>'
60
- +'<div class="vtv-channel-tabs" id="vtv-channel-tabs">' + tabsHtml + '</div>'
61
- +'<div class="vtv-channels-frame">'
62
- +'<div class="vtv-loading" id="vtv-loading"><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-channels-error" id="vtv-error" style="display:none">Không thể phát kênh</div>'
65
- +'</div>';
66
- return wrap;
67
  }
68
 
69
- function pinChannelsBlock(){
70
- const home=document.getElementById('view-home');
71
- if(!home) return;
72
- if(document.getElementById('vtv-channels-block')) return;
73
- home.insertBefore(buildChannelsBlock(), home.firstChild);
74
- initChannels();
75
- }
76
 
77
- async function initChannels(){
78
- const loadingEl = document.getElementById('vtv-loading');
79
- const video = document.getElementById('vtv-player');
80
- const errorEl = document.getElementById('vtv-error');
81
- const tabsEl = document.getElementById('vtv-channel-tabs');
82
 
83
- // Fetch channel streams from backend
84
- try{
85
- const r = await fetch('/api/vtv/channels');
86
- const data = await r.json();
87
- _vtvChannels = data.channels || [];
88
- }catch(e){
89
- console.warn('Failed to fetch VTV channels:', e);
90
- _vtvChannels = [];
91
- }
92
 
93
- loadingEl.style.display = 'none';
94
-
95
- // Disable tabs for channels without streams
96
- if(tabsEl){
97
- const foundNames = _vtvChannels.map(c => c.name);
98
- tabsEl.querySelectorAll('.vtv-channel-tab').forEach(tab => {
99
- const name = tab.dataset.name;
100
- if(!foundNames.includes(name)){
101
- tab.disabled = true;
102
- tab.title = DISPLAY_NAMES[name] + ': Chưa có luồng';
103
- }
104
- });
105
- }
106
 
107
- // Play first available channel
108
- if(_vtvChannels.length > 0){
109
- playChannel(0);
110
- } else {
111
- errorEl.style.display = 'flex';
112
- errorEl.innerHTML = 'Chưa có kênh nào khả dụng.<br><a href="https://hd.xemtv.net/kenh-vtv3-truc-tuyen-hot.html" target="_blank">Xem tại xemtv.net →</a>';
113
- video.style.display = 'none';
114
  }
115
- }
116
-
117
- function switchVTV(index){
118
- if(index >= _vtvChannels.length) return;
119
- const tabs = document.querySelectorAll('.vtv-channel-tab');
120
- tabs.forEach((tab, i) => {
121
- tab.classList.toggle('active', parseInt(tab.dataset.idx) === index);
122
- });
123
- playChannel(index);
124
- }
125
 
126
- // Expose globally
127
- window._switchVTV = switchVTV;
128
-
129
- function playChannel(index){
130
- if(index >= _vtvChannels.length) return;
131
-
132
- const ch = _vtvChannels[index];
133
- const video = document.getElementById('vtv-player');
134
- const errorEl = document.getElementById('vtv-error');
135
- const loadingEl = document.getElementById('vtv-loading');
136
-
137
- if(!video || !ch.url) {
138
- if(errorEl){
139
- errorEl.style.display = 'flex';
140
- errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Chưa có luồng.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
141
  }
142
- video.style.display = 'none';
 
 
 
 
 
143
  return;
144
  }
145
 
146
- _currentChannelIdx = index;
 
 
 
 
147
 
148
- // Clean up previous HLS instance
149
- if(_hlsInstance){
150
- _hlsInstance.destroy();
151
- _hlsInstance = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
 
 
153
 
154
- video.style.display = 'none';
155
- errorEl.style.display = 'none';
156
- loadingEl.style.display = 'flex';
157
- loadingEl.innerHTML = `<div class="vtv-spinner"></div>Đang kết nối ${esc(ch.displayName || ch.name)}...`;
158
-
159
- if(typeof Hls !== 'undefined' && Hls.isSupported()){
160
- const hls = new Hls({
161
- enableWorker: true,
162
- lowLatencyMode: true,
163
- startLevel: -1,
164
- capLevelToPlayerSize: true,
165
- maxBufferLength: 30,
166
- maxMaxBufferLength: 60,
167
- });
168
- _hlsInstance = hls;
169
- hls.loadSource(ch.url);
170
  hls.attachMedia(video);
171
- hls.on(Hls.Events.MANIFEST_PARSED, () => {
172
- video.play().catch(() => {});
173
- loadingEl.style.display = 'none';
174
- video.style.display = 'block';
175
  });
176
- hls.on(Hls.Events.ERROR, (event, data) => {
177
  if(data.fatal){
178
- switch(data.type){
179
- case Hls.ErrorTypes.NETWORK_ERROR:
180
- hls.startLoad();
181
- break;
182
- case Hls.ErrorTypes.MEDIA_ERROR:
183
- hls.recoverMediaError();
184
- break;
185
- default:
186
- hls.destroy();
187
- _hlsInstance = null;
188
- loadingEl.style.display = 'none';
189
- errorEl.style.display = 'flex';
190
- errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Lỗi phát video.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
191
- video.style.display = 'none';
192
- break;
193
- }
194
  }
195
  });
196
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
197
- video.src = ch.url;
198
- video.addEventListener('loadedmetadata', () => {
199
- video.play().catch(() => {});
200
- loadingEl.style.display = 'none';
201
- video.style.display = 'block';
202
- }, {once: true});
203
- video.addEventListener('error', () => {
204
- loadingEl.style.display = 'none';
205
- errorEl.style.display = 'flex';
206
- errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Không phát được.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
207
- video.style.display = 'none';
208
- }, {once: true});
209
  } else {
210
- loadingEl.style.display = 'none';
211
- errorEl.style.display = 'flex';
212
- errorEl.innerHTML = 'Trình duyệt không hỗ trợ HLS.<br><a href="https://hd.xemtv.net/" target="_blank">Xem tại xemtv.net →</a>';
213
- video.style.display = 'none';
214
  }
215
  }
216
 
217
- // Wrap loadHome to pin channels block after each render
218
- function install(){
219
- if(typeof window.loadHome!=='function'){return false;}
220
- if(window.loadHome.__vtvChannelsWrapped)return true;
221
- const orig=window.loadHome;
222
- const wrapped=async function(){
223
- const r=await orig.apply(this,arguments);
224
- try{pinChannelsBlock();}catch(e){}
225
  return r;
226
  };
227
- wrapped.__vtvChannelsWrapped=true;
228
- window.loadHome=wrapped;
229
- return true;
230
- }
231
-
232
- if(!install()){
233
- let tries=0;
234
- const t=setInterval(()=>{ if(install()||++tries>40)clearInterval(t); },100);
235
  }
236
- if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>setTimeout(pinChannelsBlock,300));
237
- else setTimeout(pinChannelsBlock,300);
238
  })();
 
1
+ // === VNEWS — VTV1-VTV10 LIVE CHANNELS ===
2
+ // Scrapes m3u8 streams from hd.xemtv.net + fallback CDN
3
+ // VTV6 = VTV Can Tho
4
 
5
  (function(){
6
+ const CHANNELS = [
7
+ {id:'vtv1', name:'VTV1', url:'https://hd.xemtv.net/kenh-vtv1-truc-tuyen-hot.html', icon:'🔴'},
8
+ {id:'vtv2', name:'VTV2', url:'https://hd.xemtv.net/kenh-vtv2-truc-tuyen-hot.html', icon:'🟠'},
9
+ {id:'vtv3', name:'VTV3', url:'https://hd.xemtv.net/kenh-vtv3-truc-tuyen-hot.html', icon:'🟡'},
10
+ {id:'vtv4', name:'VTV4', url:'https://hd.xemtv.net/kenh-vtv4-truc-tuyen-hot.html', icon:'🟢'},
11
+ {id:'vtv5', name:'VTV5', url:'https://hd.xemtv.net/kenh-vtv5-truc-tuyen-hot.html', icon:'🔵'},
12
+ {id:'vtvct', name:'VTV Cần Thơ', url:'https://hd.xemtv.net/kenh-vtv-can-tho-truc-tuyen-hot.html', icon:'🟣'},
13
+ {id:'vtv7', name:'VTV7', url:'https://hd.xemtv.net/kenh-vtv7-truc-tuyen-hot.html', icon:'⚪'},
14
+ {id:'vtv8', name:'VTV8', url:'https://hd.xemtv.net/kenh-vtv8-truc-tuyen-hot.html', icon:'🟤'},
15
+ {id:'vtv9', name:'VTV9', url:'https://hd.xemtv.net/kenh-vtv9-truc-tuyen-hot.html', icon:'🔴'},
16
+ {id:'vtv10', name:'VTV10', url:'https://hd.xemtv.net/kenh-vtv10-truc-tuyen-hot.html', icon:'🟠'},
17
+ ];
18
+
19
+ // HoichainTV CDN fallbacks (working VTV streams)
20
+ const CDN_FALLBACKS = {
21
+ vtv1: 'https://vips-livecdn.hoichanhtv.com/vtv1/index.m3u8',
22
+ vtv2: 'https://vips-livecdn.hoichanhtv.com/vtv2/index.m3u8',
23
+ vtv3: 'https://vips-livecdn.hoichanhtv.com/vtv3/index.m3u8',
24
+ vtv4: 'https://vips-livecdn.hoichanhtv.com/vtv4/index.m3u8',
25
+ vtv5: 'https://vips-livecdn.hoichanhtv.com/vtv5/index.m3u8',
26
+ vtvct:'https://vips-livecdn.hoichanhtv.com/vtv6/index.m3u8',
27
+ vtv7: 'https://vips-livecdn.hoichanhtv.com/vtv7/index.m3u8',
28
+ vtv8: 'https://vips-livecdn.hoichanhtv.com/vtv8/index.m3u8',
29
+ vtv9: 'https://vips-livecdn.hoichanhtv.com/vtv9/index.m3u8',
30
+ vtv10:'https://vips-livecdn.hoichanhtv.com/vtv10/index.m3u8',
31
  };
32
 
33
+ let _vtvStreams = {};
34
+ let _currentCh = null;
35
+ let _hls = null;
36
+
37
+ if(document.getElementById('vtv-css')) return;
38
+ const s=document.createElement('style'); s.id='vtv-css';
39
+ s.textContent=`
40
+ .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
41
+ .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
42
+ .vtv-title{font-size:13px;font-weight:800;color:#00ccff}
43
+ .vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}
44
+ @keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}
45
+ .vtv-tabs{display:flex;gap:3px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;background:#0d1a2a}
46
+ .vtv-tabs::-webkit-scrollbar{display:none}
47
+ .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}
48
+ .vtv-tab:hover{background:#0b4a7a;color:#fff}
49
+ .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
50
+ .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
51
+ .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
52
+ .vtv-err{display:flex;align-items:center;justify-content:center;height:180px;color:#888;font-size:12px;text-align:center;padding:20px}
53
+ .vtv-load{display:flex;align-items:center;justify-content:center;height:180px;color:#00ccff;font-size:12px}
54
+ `;
55
+ document.head.appendChild(s);
56
+
57
+ function buildBlock(){
58
+ const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
59
+ let tabs='';
60
+ CHANNELS.forEach(ch=>{
61
+ tabs+=`<button class="vtv-tab" id="vtvt-${ch.id}" onclick="window._vtvPlay('${ch.id}')">${ch.icon} ${ch.name}</button>`;
62
+ });
63
+ w.innerHTML=
64
+ '<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
65
+ '<div class="vtv-tabs">'+tabs+'</div>'+
66
+ '<div class="vtv-frame">'+
67
+ '<div class="vtv-load" id="vtv-load">Đang tải kênh...</div>'+
68
+ '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>'+
69
+ '<div class="vtv-err" id="vtv-err" style="display:none">Không thể tải kênh</div>'+
70
+ '</div>';
71
+ return w;
72
  }
73
 
74
+ function pinBlock(){
75
+ const h=document.getElementById('view-home');
76
+ if(!h||document.getElementById('vtv-block')) return;
77
+ h.insertBefore(buildBlock(), h.firstChild);
78
+ setTimeout(()=>window._vtvPlay('vtv3'), 800);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
80
 
81
+ window._vtvPlay = async function(chId){
82
+ const ch = CHANNELS.find(c=>c.id===chId);
83
+ if(!ch) return;
84
+ _currentCh = ch;
 
 
 
85
 
86
+ document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
87
+ const tab=document.getElementById('vtvt-'+chId);
88
+ if(tab) tab.classList.add('on');
 
 
89
 
90
+ const video=document.getElementById('vtv-player');
91
+ const errEl=document.getElementById('vtv-err');
92
+ const loadEl=document.getElementById('vtv-load');
93
+ video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
94
+ loadEl.textContent='Đang kết nối '+ch.name+'...';
 
 
 
 
95
 
96
+ if(_hls){_hls.destroy(); _hls=null;}
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ // Check cache (5 min)
99
+ if(_vtvStreams[chId] && Date.now()-_vtvStreams[chId]._ts<300000){
100
+ _playStream(video, _vtvStreams[chId].url, ch.name, loadEl, errEl);
101
+ return;
 
 
 
102
  }
 
 
 
 
 
 
 
 
 
 
103
 
104
+ // Step 1: Try scraping from xemtv.net
105
+ try{
106
+ const html = await _fetchPage(ch.url);
107
+ const m3u8 = _extractM3u8(html);
108
+ if(m3u8){
109
+ _vtvStreams[chId] = {url:m3u8, _ts:Date.now()};
110
+ _playStream(video, m3u8, ch.name, loadEl, errEl);
111
+ return;
 
 
 
 
 
 
 
112
  }
113
+ }catch(e){}
114
+
115
+ // Step 2: Try CDN fallback
116
+ if(CDN_FALLBACKS[chId]){
117
+ _vtvStreams[chId] = {url:CDN_FALLBACKS[chId], _ts:Date.now()};
118
+ _playStream(video, CDN_FALLBACKS[chId], ch.name, loadEl, errEl);
119
  return;
120
  }
121
 
122
+ // Step 3: Error
123
+ loadEl.style.display='none';
124
+ errEl.style.display='flex';
125
+ errEl.textContent=ch.name+': Không có luồng. Thử lại sau.';
126
+ };
127
 
128
+ async function _fetchPage(url){
129
+ // Use our proxy endpoint
130
+ try{
131
+ const r = await fetch('/api/proxy/page?url='+encodeURIComponent(url));
132
+ if(r.ok) return await r.text();
133
+ }catch(e){}
134
+ return '';
135
+ }
136
+
137
+ function _extractM3u8(html){
138
+ if(!html) return null;
139
+ const patterns = [
140
+ /(https?:\/\/[^"'<>\s]+\.m3u8[^"'<>\s]*)/gi,
141
+ /file\s*[:=]\s*["']([^"']*\.m3u8[^"']*)/gi,
142
+ /source\s*[:=]\s*["']([^"']*\.m3u8[^"']*)/gi,
143
+ /src\s*[:=]\s*["']([^"']*\.m3u8[^"']*)/gi,
144
+ /url\s*[:=]\s*["']([^"']*\.m3u8[^"']*)/gi,
145
+ /hls\.loadSource\(["']([^"']*\.m3u8[^"']*)/gi,
146
+ ];
147
+ for(const pat of patterns){
148
+ const m = pat.exec(html);
149
+ if(m) return m[1].trim();
150
  }
151
+ return null;
152
+ }
153
 
154
+ function _playStream(video, src, name, loadEl, errEl){
155
+ if(typeof Hls!=='undefined' && Hls.isSupported()){
156
+ const hls = new Hls({enableWorker:true, lowLatencyMode:true, startLevel:-1, capLevelToPlayerSize:true, maxBufferLength:20});
157
+ _hls = hls;
158
+ hls.loadSource(src);
 
 
 
 
 
 
 
 
 
 
 
159
  hls.attachMedia(video);
160
+ hls.on(Hls.Events.MANIFEST_PARSED, ()=>{
161
+ video.play().catch(()=>{});
162
+ loadEl.style.display='none'; video.style.display='block';
 
163
  });
164
+ hls.on(Hls.Events.ERROR, (ev, data)=>{
165
  if(data.fatal){
166
+ if(data.type===Hls.ErrorTypes.NETWORK_ERROR){ hls.startLoad(); }
167
+ else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){ hls.recoverMediaError(); }
168
+ else { hls.destroy(); _hls=null; loadEl.style.display='none'; errEl.style.display='flex'; errEl.textContent=name+': Luồng bị gián đoạn'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
  });
171
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
172
+ video.src = src;
173
+ video.addEventListener('loadedmetadata',()=>{ video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; },{once:true});
174
+ video.addEventListener('error',()=>{ loadEl.style.display='none'; errEl.style.display='flex'; errEl.textContent=name+': Không phát được'; },{once:true});
 
 
 
 
 
 
 
 
 
175
  } else {
176
+ loadEl.style.display='none'; errEl.style.display='flex'; errEl.textContent='Trình duyệt không hỗ trợ HLS';
 
 
 
177
  }
178
  }
179
 
180
+ // Wrap loadHome
181
+ const orig = window.loadHome;
182
+ if(orig && !orig.__vtvWrapped){
183
+ window.loadHome = async function(){
184
+ const r = await orig.apply(this, arguments);
185
+ try{ pinBlock(); }catch(e){}
 
 
186
  return r;
187
  };
188
+ window.loadHome.__vtvWrapped = true;
 
 
 
 
 
 
 
189
  }
190
+ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',()=>setTimeout(pinBlock,500));
191
+ else setTimeout(pinBlock,500);
192
  })();