bep40 commited on
Commit
0cb177d
·
verified ·
1 Parent(s): a084598

Upload static/yt_live.js

Browse files
Files changed (1) hide show
  1. static/yt_live.js +253 -10
static/yt_live.js CHANGED
@@ -2,6 +2,7 @@
2
  // Uses backend /api/vtv/streams for stream URLs
3
  // VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
4
  // Default channel: VTV6
 
5
 
6
  (function(){
7
  if(window._ytLiveLoaded) return;
@@ -31,7 +32,10 @@
31
  let _loading = false;
32
  let _blockInserted = false;
33
  let _streamsLoaded = false;
 
 
34
 
 
35
  const s = document.createElement('style');
36
  s.textContent = `
37
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
@@ -68,9 +72,175 @@
68
  .vtv-epg-empty{color:#666;font-size:9px;padding:4px}
69
  .vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
70
  .vtv-epg-spinner{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  `;
72
  document.head.appendChild(s);
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  async function loadAllStreams(){
75
  if(_loading) return;
76
  _loading = true;
@@ -128,8 +298,16 @@
128
  '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>' +
129
  '<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>' +
130
  '</div>' +
 
 
 
 
 
 
 
 
131
  '<div class="vtv-epg" id="vtv-epg">' +
132
- '<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
133
  '<div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải EPG...</div></div>' +
134
  '</div>';
135
  return w;
@@ -138,32 +316,32 @@
138
  async function loadEpg(chId){
139
  const epgList = document.getElementById('vtv-epg-list');
140
  if(!epgList) return;
141
-
142
- // Show loading state
143
  epgList.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải lịch phát sóng...</div>';
144
-
145
  try {
146
  const r = await fetch('/api/vtv/epg/' + chId, {signal: AbortSignal.timeout(10000)});
147
  if(!r.ok) throw new Error('EPG fetch failed: ' + r.status);
148
  const data = await r.json();
149
  const programs = data.programs || [];
150
-
151
  if(programs.length === 0){
152
  epgList.innerHTML = '<div class="vtv-epg-empty">Chưa có lịch phát sóng cho kênh này</div>';
153
  return;
154
  }
155
-
156
  let h = '';
157
  programs.forEach(p => {
158
  const isNow = p.now ? ' now' : '';
159
  const desc = p.description ? ' title="' + escAttr(p.description) + '"' : '';
160
  h += '<div class="vtv-epg-item' + isNow + '"' + desc + '>';
161
  h += '<span class="epg-t">' + (p.time || '') + '</span> ';
 
162
  h += '<span class="epg-n">' + (p.title || '') + '</span>';
163
  h += '</div>';
164
  });
165
  epgList.innerHTML = h;
166
-
167
  // Scroll to current program
168
  const nowItem = epgList.querySelector('.vtv-epg-item.now');
169
  if(nowItem){
@@ -174,7 +352,7 @@
174
  if(epgList) epgList.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch phát sóng</div>';
175
  }
176
  }
177
-
178
  function escAttr(s){
179
  return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
180
  }
@@ -192,6 +370,27 @@
192
  }
193
  };
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  function pinBlock(){
196
  const h = document.getElementById('view-home');
197
  if(!h) return;
@@ -204,7 +403,6 @@
204
  _blockInserted = true;
205
  if(!_streamsLoaded){
206
  loadAllStreams().then(() => {
207
- // Auto-play VTV6 as default channel
208
  setTimeout(() => window._vtvPlay(DEFAULT_CHANNEL), 300);
209
  });
210
  }
@@ -243,6 +441,12 @@
243
  }
244
  _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
245
  loadEpg(chId);
 
 
 
 
 
 
246
  };
247
 
248
  function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
@@ -315,11 +519,50 @@
315
  }
316
  }
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  // Wrap loadHome to inject VTV block
319
  const orig = window.loadHome;
320
  if(orig && !orig.__vtvWrapped){
321
  window.loadHome = async function(){
322
- // Remove old VTV block before loadHome replaces innerHTML
323
  const oldBlock = document.getElementById('vtv-block');
324
  if(oldBlock) oldBlock.remove();
325
  _blockInserted = false;
 
2
  // Uses backend /api/vtv/streams for stream URLs
3
  // VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
4
  // Default channel: VTV6
5
+ // Features: PiP button, floating mini-player when reading articles
6
 
7
  (function(){
8
  if(window._ytLiveLoaded) return;
 
32
  let _loading = false;
33
  let _blockInserted = false;
34
  let _streamsLoaded = false;
35
+ let _pipActive = false;
36
+ let _miniPlayerActive = false;
37
 
38
+ // ===== CSS =====
39
  const s = document.createElement('style');
40
  s.textContent = `
41
  .vtv-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
 
72
  .vtv-epg-empty{color:#666;font-size:9px;padding:4px}
73
  .vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
74
  .vtv-epg-spinner{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
75
+ .vtv-controls{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#0d1a2a;border-top:1px solid #1a2a3a}
76
+ .vtv-pip-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px}
77
+ .vtv-pip-btn:hover{background:#0b4a7a;color:#fff}
78
+ .vtv-pip-btn.active{background:#0066cc;border-color:#00ccff;color:#fff}
79
+ .vtv-pip-btn svg{width:12px;height:12px;fill:currentColor}
80
+
81
+ /* ===== MINI PLAYER (floating when reading articles) ===== */
82
+ .vtv-mini{position:fixed;top:0;left:0;right:0;z-index:99990;background:#000;border-bottom:2px solid #0066cc;box-shadow:0 4px 20px rgba(0,102,204,.3);display:none;transition:transform .3s ease}
83
+ .vtv-mini.show{display:block}
84
+ .vtv-mini.vtv-mini-hidden{transform:translateY(-85%)}
85
+ .vtv-mini-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;max-height:200px}
86
+ .vtv-mini-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
87
+ .vtv-mini-bar{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;background:linear-gradient(90deg,#003366,#1a1a1a)}
88
+ .vtv-mini-info{display:flex;align-items:center;gap:6px}
89
+ .vtv-mini-ch{font-size:10px;font-weight:700;color:#00ccff}
90
+ .vtv-mini-epg{font-size:9px;color:#8ab4d8;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
91
+ .vtv-mini-btns{display:flex;gap:4px}
92
+ .vtv-mini-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 6px;border-radius:4px;cursor:pointer}
93
+ .vtv-mini-btn:hover{background:#0b4a7a;color:#fff}
94
+ .vtv-mini-btn.close{background:#660000;border-color:#990000;color:#ff6666}
95
+ .vtv-mini-btn.close:hover{background:#990000;color:#fff}
96
+ .vtv-mini-peek{position:absolute;bottom:-20px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
97
+ .vtv-mini.vtv-mini-hidden .vtv-mini-peek{display:block}
98
  `;
99
  document.head.appendChild(s);
100
 
101
+ // ===== MINI PLAYER =====
102
+ function createMiniPlayer(){
103
+ if(document.getElementById('vtv-mini')) return;
104
+ const mini = document.createElement('div');
105
+ mini.className = 'vtv-mini';
106
+ mini.id = 'vtv-mini';
107
+ mini.innerHTML = `
108
+ <div class="vtv-mini-frame">
109
+ <video id="vtv-mini-video" playsinline muted preload="auto"></video>
110
+ </div>
111
+ <div class="vtv-mini-bar">
112
+ <div class="vtv-mini-info">
113
+ <span class="vtv-mini-ch" id="vtv-mini-ch">VTV</span>
114
+ <span class="vtv-mini-epg" id="vtv-mini-epg"></span>
115
+ </div>
116
+ <div class="vtv-mini-btns">
117
+ <button class="vtv-mini-btn" id="vtv-mini-pip" title="PiP">📺 PiP</button>
118
+ <button class="vtv-mini-btn" id="vtv-mini-expand" title="Mở rộng">⬆️</button>
119
+ <button class="vtv-mini-btn" id="vtv-mini-hide" title="Thu nhỏ">⬇️</button>
120
+ <button class="vtv-mini-btn close" id="vtv-mini-close" title="Đóng">✕</button>
121
+ </div>
122
+ </div>
123
+ <div class="vtv-mini-peek" id="vtv-mini-peek">📺 Xem</div>
124
+ `;
125
+ document.body.appendChild(mini);
126
+
127
+ // Event listeners
128
+ document.getElementById('vtv-mini-pip').onclick = toggleMiniPiP;
129
+ document.getElementById('vtv-mini-expand').onclick = expandFromMini;
130
+ document.getElementById('vtv-mini-hide').onclick = hideMiniPlayer;
131
+ document.getElementById('vtv-mini-close').onclick = closeMiniPlayer;
132
+ document.getElementById('vtv-mini-peek').onclick = showMiniPlayer;
133
+ document.getElementById('vtv-mini').onclick = function(e){
134
+ if(e.target === this || e.target.classList.contains('vtv-mini-frame')){
135
+ expandFromMini();
136
+ }
137
+ };
138
+ }
139
+
140
+ function showMiniPlayer(){
141
+ const mini = document.getElementById('vtv-mini');
142
+ if(mini){
143
+ mini.classList.remove('vtv-mini-hidden');
144
+ mini.classList.add('show');
145
+ }
146
+ }
147
+
148
+ function hideMiniPlayer(){
149
+ const mini = document.getElementById('vtv-mini');
150
+ if(mini){
151
+ mini.classList.add('vtv-mini-hidden');
152
+ }
153
+ }
154
+
155
+ function closeMiniPlayer(){
156
+ const mini = document.getElementById('vtv-mini');
157
+ if(mini){
158
+ const video = document.getElementById('vtv-mini-video');
159
+ if(video){
160
+ video.pause();
161
+ video.src = '';
162
+ }
163
+ mini.remove();
164
+ }
165
+ _miniPlayerActive = false;
166
+ }
167
+
168
+ function expandFromMini(){
169
+ // Go back to home view to show full player
170
+ if(typeof switchCat === 'function'){
171
+ switchCat('home');
172
+ }
173
+ showMiniPlayer();
174
+ }
175
+
176
+ function activateMiniPlayer(){
177
+ if(!_currentCh) return;
178
+ createMiniPlayer();
179
+ _miniPlayerActive = true;
180
+
181
+ const mini = document.getElementById('vtv-mini');
182
+ const miniVideo = document.getElementById('vtv-mini-video');
183
+ const mainVideo = document.getElementById('vtv-player');
184
+
185
+ // Copy video source from main player
186
+ if(mainVideo && mainVideo.src){
187
+ miniVideo.src = mainVideo.src;
188
+ miniVideo.play().catch(()=>{});
189
+ } else if(mainVideo && mainVideo.querySelector('source')){
190
+ const source = mainVideo.querySelector('source');
191
+ if(source && source.src){
192
+ miniVideo.src = source.src;
193
+ miniVideo.play().catch(()=>{});
194
+ }
195
+ }
196
+
197
+ // Update channel info
198
+ const ch = CHANNELS.find(c => c.id === _currentCh);
199
+ if(ch){
200
+ document.getElementById('vtv-mini-ch').textContent = ch.name;
201
+ }
202
+
203
+ // Update EPG info
204
+ updateMiniEpg();
205
+
206
+ mini.classList.add('show');
207
+ mini.classList.remove('vtv-mini-hidden');
208
+ }
209
+
210
+ function updateMiniEpg(){
211
+ if(!_currentCh) return;
212
+ fetch('/api/vtv/epg/' + _currentCh)
213
+ .then(r => r.json())
214
+ .then(data => {
215
+ const programs = data.programs || [];
216
+ const nowProg = programs.find(p => p.now);
217
+ if(nowProg){
218
+ document.getElementById('vtv-mini-epg').textContent = nowProg.time + ' - ' + nowProg.title;
219
+ } else if(programs.length > 0){
220
+ document.getElementById('vtv-mini-epg').textContent = programs[0].time + ' - ' + programs[0].title;
221
+ }
222
+ }).catch(()=>{});
223
+ }
224
+
225
+ async function toggleMiniPiP(){
226
+ const miniVideo = document.getElementById('vtv-mini-video');
227
+ if(!miniVideo) return;
228
+
229
+ try{
230
+ if(document.pictureInPictureElement === miniVideo){
231
+ await document.exitPictureInPicture();
232
+ _pipActive = false;
233
+ } else {
234
+ await miniVideo.requestPictureInPicture();
235
+ _pipActive = true;
236
+ }
237
+ } catch(e){
238
+ console.warn('PiP error:', e);
239
+ alert('Trình duyệt không hỗ trợ PiP hoặc video chưa sẵn sàng');
240
+ }
241
+ }
242
+
243
+ // ===== MAIN PLAYER =====
244
  async function loadAllStreams(){
245
  if(_loading) return;
246
  _loading = true;
 
298
  '<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>' +
299
  '<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>' +
300
  '</div>' +
301
+ '<div class="vtv-controls">' +
302
+ '<button class="vtv-pip-btn" id="vtv-pip-btn" onclick="window._vtvTogglePiP()">' +
303
+ '<svg viewBox="0 0 24 24"><rect x="2" y="4" width="20" height="16" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><rect x="12" y="10" width="8" height="6" rx="1" fill="currentColor"/></svg>' +
304
+ 'PiP' +
305
+ '</button>' +
306
+ '<span style="flex:1"></span>' +
307
+ '<span style="font-size:9px;color:#666" id="vtv-status"></span>' +
308
+ '</div>' +
309
  '<div class="vtv-epg" id="vtv-epg">' +
310
+ '<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng (vtv.vn)</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
311
  '<div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải EPG...</div></div>' +
312
  '</div>';
313
  return w;
 
316
  async function loadEpg(chId){
317
  const epgList = document.getElementById('vtv-epg-list');
318
  if(!epgList) return;
319
+
 
320
  epgList.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải lịch phát sóng...</div>';
321
+
322
  try {
323
  const r = await fetch('/api/vtv/epg/' + chId, {signal: AbortSignal.timeout(10000)});
324
  if(!r.ok) throw new Error('EPG fetch failed: ' + r.status);
325
  const data = await r.json();
326
  const programs = data.programs || [];
327
+
328
  if(programs.length === 0){
329
  epgList.innerHTML = '<div class="vtv-epg-empty">Chưa có lịch phát sóng cho kênh này</div>';
330
  return;
331
  }
332
+
333
  let h = '';
334
  programs.forEach(p => {
335
  const isNow = p.now ? ' now' : '';
336
  const desc = p.description ? ' title="' + escAttr(p.description) + '"' : '';
337
  h += '<div class="vtv-epg-item' + isNow + '"' + desc + '>';
338
  h += '<span class="epg-t">' + (p.time || '') + '</span> ';
339
+ if(p.end_time) h += '<span class="epg-t">-' + p.end_time + '</span> ';
340
  h += '<span class="epg-n">' + (p.title || '') + '</span>';
341
  h += '</div>';
342
  });
343
  epgList.innerHTML = h;
344
+
345
  // Scroll to current program
346
  const nowItem = epgList.querySelector('.vtv-epg-item.now');
347
  if(nowItem){
 
352
  if(epgList) epgList.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch phát sóng</div>';
353
  }
354
  }
355
+
356
  function escAttr(s){
357
  return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
358
  }
 
370
  }
371
  };
372
 
373
+ window._vtvTogglePiP = async function(){
374
+ const video = document.getElementById('vtv-player');
375
+ const btn = document.getElementById('vtv-pip-btn');
376
+ if(!video) return;
377
+
378
+ try{
379
+ if(document.pictureInPictureElement === video){
380
+ await document.exitPictureInPicture();
381
+ _pipActive = false;
382
+ if(btn) btn.classList.remove('active');
383
+ } else {
384
+ await video.requestPictureInPicture();
385
+ _pipActive = true;
386
+ if(btn) btn.classList.add('active');
387
+ }
388
+ } catch(e){
389
+ console.warn('PiP error:', e);
390
+ alert('Trình duyệt không hỗ trợ PiP hoặc video chưa sẵn sàng');
391
+ }
392
+ };
393
+
394
  function pinBlock(){
395
  const h = document.getElementById('view-home');
396
  if(!h) return;
 
403
  _blockInserted = true;
404
  if(!_streamsLoaded){
405
  loadAllStreams().then(() => {
 
406
  setTimeout(() => window._vtvPlay(DEFAULT_CHANNEL), 300);
407
  });
408
  }
 
441
  }
442
  _tryPlay(video, urls, 0, ch.name, loadEl, errEl, errMsg);
443
  loadEpg(chId);
444
+
445
+ // Update mini player if active
446
+ if(_miniPlayerActive){
447
+ document.getElementById('vtv-mini-ch').textContent = ch.name;
448
+ updateMiniEpg();
449
+ }
450
  };
451
 
452
  function _tryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
 
519
  }
520
  }
521
 
522
+ // ===== KEEP PLAYER WHEN NAVIGATING =====
523
+ // Override showView to activate mini player when leaving home
524
+ const origShowView = window.showView;
525
+ window.showView = function(id){
526
+ // If leaving home/article view and VTV is playing, show mini player
527
+ if(id !== 'view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
528
+ if(!_miniPlayerActive){
529
+ activateMiniPlayer();
530
+ }
531
+ }
532
+ return origShowView.apply(this, arguments);
533
+ };
534
+
535
+ // Override readArticle to keep player
536
+ const origReadArticle = window.readArticle;
537
+ if(origReadArticle){
538
+ window.readArticle = async function(url){
539
+ // Activate mini player before navigating
540
+ if(_currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
541
+ if(!_miniPlayerActive){
542
+ activateMiniPlayer();
543
+ }
544
+ }
545
+ return origReadArticle.apply(this, arguments);
546
+ };
547
+ }
548
+
549
+ // Override switchCat to keep player
550
+ const origSwitchCat = window.switchCat;
551
+ if(origSwitchCat){
552
+ window.switchCat = async function(id){
553
+ if(id !== 'view-home' && _currentCh && STREAMS[_currentCh] && STREAMS[_currentCh].length > 0){
554
+ if(!_miniPlayerActive){
555
+ activateMiniPlayer();
556
+ }
557
+ }
558
+ return origSwitchCat.apply(this, arguments);
559
+ };
560
+ }
561
+
562
  // Wrap loadHome to inject VTV block
563
  const orig = window.loadHome;
564
  if(orig && !orig.__vtvWrapped){
565
  window.loadHome = async function(){
 
566
  const oldBlock = document.getElementById('vtv-block');
567
  if(oldBlock) oldBlock.remove();
568
  _blockInserted = false;