bep40 commited on
Commit
df310bf
·
verified ·
1 Parent(s): 08a2c5d

Fix vtv_init.js: add self-contained CSS, fix HLS loading, better error handling

Browse files
Files changed (1) hide show
  1. static/vtv_init.js +106 -56
static/vtv_init.js CHANGED
@@ -1,5 +1,4 @@
1
- // ===== VNEWS VTV Player v2 (self-contained) =====
2
- // Builds VTV player UI, fetches streams from backend API
3
  (function() {
4
  if (window._vtvInitLoaded) return;
5
  window._vtvInitLoaded = true;
@@ -16,92 +15,143 @@
16
  var _hlsInst = null;
17
  var _currentCh = null;
18
 
19
- function buildTabs() {
20
- var h = '';
21
- CHANNELS.forEach(function(ch) {
22
- h += '<div class="vtv-tab off" data-ch="'+ch.id+'" onclick="_vtvSwitch(\''+ch.id+'\')">'+ch.name+'</div>';
23
- });
24
- return h;
25
- }
26
-
27
- var HTML = '<div class="vtv-wrap" id="vtv-player-section">'
28
- + '<div class="vtv-head"><span class="vtv-title">📺 VTV Player</span><span class="vtv-badge">● LIVE</span></div>'
29
- + '<div class="vtv-tabs" id="vtv-tabs">'+buildTabs()+'</div>'
30
- + '<div class="vtv-frame" id="vtv-frame"><div class="vtv-load" id="vtv-loading"><div class="vtv-spinner"></div><span>Chọn kênh để xem</span></div>'
31
- + '<video id="vtv-video" style="display:none;width:100%;height:100%;object-fit:contain" controls autoplay playsinline></video></div>'
32
- +'</div>';
 
 
 
 
 
 
 
 
 
33
 
34
  window._vtvSwitch = function(chId) {
35
  if (_hlsInst) { _hlsInst.destroy(); _hlsInst = null; }
36
  _currentCh = chId;
37
 
38
- // Update tabs
39
- document.querySelectorAll('#vtv-tabs .vtv-tab').forEach(function(t) {
40
- t.classList.toggle('on', t.getAttribute('data-ch') === chId);
41
- t.classList.toggle('off', t.getAttribute('data-ch') !== chId);
42
- });
43
 
44
  var loadingEl = document.getElementById('vtv-loading');
45
  var videoEl = document.getElementById('vtv-video');
46
- if (!videoEl) return;
47
 
48
  loadingEl.style.display = 'flex';
 
49
  videoEl.style.display = 'none';
50
  videoEl.pause();
51
  videoEl.removeAttribute('src');
52
 
53
  // Fetch stream from backend
54
- fetch('/api/vtv/stream/' + chId)
55
- .then(function(r) { return r.json(); })
56
- .then(function(data) {
 
 
 
57
  var url = data.stream_url;
58
  if (!url) {
59
- loadingEl.innerHTML = '<span style="color:#e74c3c">⚠️ Kênh đang offline</span><br><button onclick="_vtvSwitch(\''+chId+'\')">Thử lại</button>';
60
  return;
61
  }
62
- // Check if need proxy
63
  if (url.indexOf('fptplay') !== -1 || url.indexOf('vtvgo') !== -1 || url.indexOf('mediacdn') !== -1) {
64
  url = '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(url);
65
  }
66
- if (typeof Hls !== 'undefined' && Hls.isSupported()) {
67
- var hls = new Hls();
68
- hls.loadSource(url);
69
- hls.attachMedia(videoEl);
70
- hls.on(Hls.Events.MANIFEST_PARSED, function() {
71
- loadingEl.style.display = 'none';
72
- videoEl.style.display = 'block';
73
- videoEl.play().catch(function(){});
74
- });
75
- hls.on(Hls.Events.ERROR, function(ev, data) {
76
- if (data.fatal) {
77
- hls.destroy();
78
- loadingEl.innerHTML = '<span style="color:#e74c3c">⚠️ Lỗi phát: '+data.type+'</span><br><button onclick="_vtvSwitch(\''+chId+'\')">Thử lại</button>';
79
- }
80
- });
81
- _hlsInst = hls;
82
- } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
83
- videoEl.src = url;
84
- videoEl.addEventListener('loadedmetadata', function() {
85
- loadingEl.style.display = 'none';
86
- videoEl.style.display = 'block';
87
- videoEl.play().catch(function(){});
88
- });
 
 
 
 
 
 
 
 
89
  }
90
- })
91
- .catch(function(err) {
92
- loadingEl.innerHTML = '<span style="color:#e74c3c">⚠️ Lỗi kết nối</span><br><button onclick="_vtvSwitch(\''+chId+'\')">Thử lại</button>';
93
  });
94
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  function inject() {
97
  var homeEl = document.getElementById('view-home');
98
  if (!homeEl || document.getElementById('vtv-player-section')) return;
99
 
100
  var featured = document.getElementById('home-featured-area');
101
- if (featured) {
102
  featured.insertAdjacentHTML('afterend', HTML);
103
- } else {
104
  homeEl.insertAdjacentHTML('afterbegin', HTML);
 
 
105
  }
106
  }
107
 
 
1
+ // ===== VNEWS VTV Player v3 (self-contained with CSS + backend stream fetch) =====
 
2
  (function() {
3
  if (window._vtvInitLoaded) return;
4
  window._vtvInitLoaded = true;
 
15
  var _hlsInst = null;
16
  var _currentCh = null;
17
 
18
+ // ===== Inject CSS (standalone, doesn't need yt_live.js) =====
19
+ (function injectCSS() {
20
+ var css = '.vtv-wrap{position:relative;margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}' +
21
+ '.vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}' +
22
+ '.vtv-title{font-size:13px;font-weight:800;color:#00ccff}' +
23
+ '.vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}' +
24
+ '@keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}' +
25
+ '.vtv-tabs{display:flex;flex-wrap:wrap;gap:3px;padding:6px 8px;overflow-x:auto;background:#0d1a2a}' +
26
+ '.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}' +
27
+ '.vtv-tab:hover{background:#0b4a7a;color:#fff}' +
28
+ '.vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}' +
29
+ '.vtv-tab.off{opacity:.35}' +
30
+ '.vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}' +
31
+ '.vtv-frame video{position:absolute;inset:0;width:100% !important;height:100% !important;object-fit:contain;max-width:100% !important;max-height:100% !important}' +
32
+ '.vtv-err{display:flex;align-items:center;justify-content:center;min-height:180px;color:#888;font-size:12px;text-align:center;padding:20px;flex-direction:column;gap:8px}' +
33
+ '.vtv-err button{background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer}' +
34
+ '.vtv-load{display:flex;align-items:center;justify-content:center;min-height:180px;color:#00ccff;font-size:12px;flex-direction:column;gap:8px}' +
35
+ '.vtv-spinner{width:24px;height:24px;border:2px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}' +
36
+ '@keyframes vtvspin{to{transform:rotate(360deg)}}';
37
+ var s = document.createElement('style');
38
+ s.textContent = css;
39
+ document.head.appendChild(s);
40
+ })();
41
 
42
  window._vtvSwitch = function(chId) {
43
  if (_hlsInst) { _hlsInst.destroy(); _hlsInst = null; }
44
  _currentCh = chId;
45
 
46
+ var tabs = document.querySelectorAll('#vtv-tabs .vtv-tab');
47
+ for (var i = 0; i < tabs.length; i++) {
48
+ tabs[i].classList.toggle('on', tabs[i].getAttribute('data-ch') === chId);
49
+ tabs[i].classList.toggle('off', tabs[i].getAttribute('data-ch') !== chId);
50
+ }
51
 
52
  var loadingEl = document.getElementById('vtv-loading');
53
  var videoEl = document.getElementById('vtv-video');
54
+ if (!videoEl || !loadingEl) return;
55
 
56
  loadingEl.style.display = 'flex';
57
+ loadingEl.innerHTML = '<div class="vtv-spinner"></div><span>Đang tải ' + chId.toUpperCase() + '...</span>';
58
  videoEl.style.display = 'none';
59
  videoEl.pause();
60
  videoEl.removeAttribute('src');
61
 
62
  // Fetch stream from backend
63
+ var xhr = new XMLHttpRequest();
64
+ xhr.open('GET', '/api/vtv/stream/' + chId, true);
65
+ xhr.timeout = 15000;
66
+ xhr.onload = function() {
67
+ try {
68
+ var data = JSON.parse(xhr.responseText);
69
  var url = data.stream_url;
70
  if (!url) {
71
+ loadingEl.innerHTML = '<div style="color:#e74c3c;text-align:center"><div style="font-size:24px;margin-bottom:8px">⚠️</div><span style="font-size:13px;font-weight:700">Đang bảo trì</span><br><span style="font-size:11px;color:#999">Kênh ' + chId.toUpperCase() + ' tạm ngắt</span><br><br><button onclick="_vtvSwitch(\''+chId+'\')" style="background:#0066cc;border:0;color:#fff;padding:8px 20px;border-radius:8px;cursor:pointer;font-size:12px">🔄 Thử lại</button></div>';
72
  return;
73
  }
74
+ // Proxy if needed
75
  if (url.indexOf('fptplay') !== -1 || url.indexOf('vtvgo') !== -1 || url.indexOf('mediacdn') !== -1) {
76
  url = '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(url);
77
  }
78
+ playStream(url, videoEl, loadingEl);
79
+ } catch(e) {
80
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">Lỗi dữ liệu<br><button onclick="_vtvSwitch(\''+chId+'\')" style="background:#0066cc;border:0;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;margin-top:8px">Thử lại</button></div>';
81
+ }
82
+ };
83
+ xhr.onerror = function() {
84
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">Không thể kết nối<br><button onclick="_vtvSwitch(\''+chId+'\')" style="background:#0066cc;border:0;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;margin-top:8px">Thử lại</button></div>';
85
+ };
86
+ xhr.ontimeout = function() {
87
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">Hết thời gian chờ<br><button onclick="_vtvSwitch(\''+chId+'\')" style="background:#0066cc;border:0;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;margin-top:8px">Thử lại</button></div>';
88
+ };
89
+ xhr.send();
90
+ };
91
+
92
+ function playStream(url, videoEl, loadingEl) {
93
+ if (typeof Hls !== 'undefined' && Hls.isSupported()) {
94
+ var hls = new Hls({ debug: false });
95
+ hls.loadSource(url);
96
+ hls.attachMedia(videoEl);
97
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
98
+ loadingEl.style.display = 'none';
99
+ videoEl.style.display = 'block';
100
+ videoEl.play().catch(function(e){ console.log('play error', e); });
101
+ });
102
+ hls.on(Hls.Events.ERROR, function(ev, data) {
103
+ console.log('HLS error', data.type, data.details);
104
+ if (data.fatal) {
105
+ hls.destroy();
106
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">⚠️ Lỗi phát (recover...)<br><button onclick="_vtvSwitch(\''+_currentCh+'\')" style="background:#0066cc;border:0;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;margin-top:8px">Thử lại</button></div>';
107
+ // Attempt recover
108
+ setTimeout(function() { if (_currentCh) window._vtvSwitch(_currentCh); }, 3000);
109
  }
 
 
 
110
  });
111
+ _hlsInst = hls;
112
+ } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
113
+ videoEl.src = url;
114
+ videoEl.addEventListener('loadedmetadata', function() {
115
+ loadingEl.style.display = 'none';
116
+ videoEl.style.display = 'block';
117
+ videoEl.play().catch(function(){});
118
+ });
119
+ videoEl.addEventListener('error', function() {
120
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">⚠️ Lỗi native HLS<br><button onclick="_vtvSwitch(\''+_currentCh+'\')" style="background:#0066cc;border:0;color:#fff;padding:6px 14px;border-radius:8px;cursor:pointer;margin-top:8px">Thử lại</button></div>';
121
+ });
122
+ } else {
123
+ loadingEl.innerHTML = '<div style="color:#e74c3c;font-size:12px">⚠️ Trình duyệt không hỗ trợ HLS</div>';
124
+ }
125
+ }
126
+
127
+ function buildTabs() {
128
+ var h = '';
129
+ for (var i = 0; i < CHANNELS.length; i++) {
130
+ var ch = CHANNELS[i];
131
+ h += '<div class="vtv-tab off" data-ch="'+ch.id+'" onclick="_vtvSwitch(\''+ch.id+'\')">'+ch.name+'</div>';
132
+ }
133
+ return h;
134
+ }
135
+
136
+ var HTML = '<div class="vtv-wrap" id="vtv-player-section">'
137
+ + '<div class="vtv-head"><span class="vtv-title">📺 VTV Player</span><span class="vtv-badge">● LIVE</span></div>'
138
+ + '<div class="vtv-tabs" id="vtv-tabs">'+buildTabs()+'</div>'
139
+ + '<div class="vtv-frame" id="vtv-frame">'
140
+ + '<div class="vtv-load" id="vtv-loading"><div class="vtv-spinner"></div><span>📺 Chọn kênh để xem</span></div>'
141
+ + '<video id="vtv-video" style="display:none" controls autoplay playsinline muted></video>'
142
+ + '</div></div>';
143
 
144
  function inject() {
145
  var homeEl = document.getElementById('view-home');
146
  if (!homeEl || document.getElementById('vtv-player-section')) return;
147
 
148
  var featured = document.getElementById('home-featured-area');
149
+ if (featured && featured.parentNode) {
150
  featured.insertAdjacentHTML('afterend', HTML);
151
+ } else if (homeEl.firstChild) {
152
  homeEl.insertAdjacentHTML('afterbegin', HTML);
153
+ } else {
154
+ homeEl.innerHTML = HTML + homeEl.innerHTML;
155
  }
156
  }
157