bep40 commited on
Commit
5dffe34
·
verified ·
1 Parent(s): 6aa0e76

Update yt_live.js: handle unavailable channel status (vtv10/vtvprime)

Browse files
Files changed (1) hide show
  1. static/yt_live.js +1 -836
static/yt_live.js CHANGED
@@ -1,836 +1 @@
1
- // === VNEWS — VTV LIVE + Inline Recorder v6 ===
2
- // Features: PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
3
- // FIX v6: audio from captureStream (unmute during record), video.ended for crop, AI title generation
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'},{id:'vtv2',name:'VTV2',badge:'Khoa học'},
11
- {id:'vtv3',name:'VTV3',badge:'Giải trí'},{id:'vtv4',name:'VTV4',badge:'Quốc tế'},
12
- {id:'vtv5',name:'VTV5',badge:'Miền Nam'},{id:'vtv6',name:'VTV6',badge:'Thanh niên'},
13
- {id:'vtv7',name:'VTV7',badge:'Giáo dục'},{id:'vtv8',name:'VTV8',badge:'Miền Trung'},
14
- {id:'vtv9',name:'VTV9',badge:'Miền Bắc'},{id:'vtv10',name:'VTV10',badge:'Cần Thơ'},
15
- {id:'vtvprime',name:'VTVPrime',badge:'Prime'},
16
- ];
17
- const DEFAULT_CHANNEL = 'vtv6';
18
- const NEEDS_PROXY = /fptplay\.net/;
19
- const STREAMS = {};
20
- let _currentCh = null, _hls = null, _loading = false, _blockInserted = false;
21
- let _streamsLoaded = false, _pipActive = false, _miniActive = false, _vtvPinned = false;
22
-
23
- // ===== RECORDER STATE =====
24
- const _rec = {
25
- active: false, startTime: null, endTime: null,
26
- isRecording: false, recorder: null, chunks: [], blob: null,
27
- ratio: 'original', _dragMarker: null, _recTimer: null,
28
- };
29
-
30
- // ===== STYLES =====
31
- const style = document.createElement('style');
32
- style.textContent = `
33
- .vtv-wrap{position:relative;margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden}
34
- .vtv-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a)}
35
- .vtv-title{font-size:13px;font-weight:800;color:#00ccff}
36
- .vtv-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtvp 1.3s infinite}
37
- @keyframes vtvp{0%,100%{opacity:1}50%{opacity:.3}}
38
- .vtv-tabs{display:flex;gap:3px;padding:6px 8px;overflow-x:auto;scrollbar-width:none;background:#0d1a2a}
39
- .vtv-tabs::-webkit-scrollbar{display:none}
40
- .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}
41
- .vtv-tab:hover{background:#0b4a7a;color:#fff}
42
- .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
43
- .vtv-tab.off{opacity:.35;pointer-events:none}
44
- .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
45
- .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
46
- .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}
47
- .vtv-err button{background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer}
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
- .vtv-controls{display:flex;align-items:center;gap:4px;padding:4px 8px;background:#0d1a2a;border-top:1px solid #1a2a3a}
52
- .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}
53
- .vtv-pip-btn:hover{background:#0b4a7a;color:#fff}
54
- .vtv-pip-btn.on{background:#0066cc;border-color:#00ccff;color:#fff}
55
- .vtv-pip-btn svg{width:12px;height:12px;fill:currentColor}
56
- .vtv-epg{margin:0;padding:6px 10px;background:#0a1628;border-top:1px solid #1a2a3a}
57
- .vtv-epg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}
58
- .vtv-epg-title{font-size:10px;font-weight:700;color:#00ccff}
59
- .vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
60
- .vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
61
- .vtv-epg-list::-webkit-scrollbar{display:none}
62
- .vtv-epg-item{flex:0 0 auto;padding:3px 6px;background:#1a2a3a;border-radius:4px;font-size:8px;color:#8ab4d8;white-space:nowrap;cursor:pointer}
63
- .vtv-epg-item:hover{background:#2a4a6a}
64
- .vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
65
- .vtv-epg-item .t{font-size:7px;color:#6a8aaa}
66
- .vtv-epg-item.now .t{color:#aaccee}
67
- .vtv-epg-item .n{color:#ccc;font-size:8px}
68
- .vtv-epg-item.now .n{color:#fff}
69
- .vtv-epg-empty{color:#666;font-size:9px;padding:4px}
70
- .vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
71
- .vtv-epg-sp{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
72
- .vtv-pin-btn{position:absolute;top:6px;right:8px;z-index:5;background:rgba(0,0,0,.5);border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 6px;border-radius:4px;cursor:pointer;font-weight:700;display:flex;align-items:center;gap:3px}
73
- .vtv-pin-btn:hover{background:#0b4a7a;color:#fff}
74
- .vtv-pin-btn.pinned{background:#0066cc;border-color:#00ccff;color:#fff}
75
- .vtv-wrap.vtv-sticky{position:sticky;top:0;z-index:48;transition:all .25s ease;box-shadow:0 4px 24px rgba(0,102,204,.35)}
76
- .vtv-wrap.vtv-sticky .vtv-epg{display:none}
77
- .vtv-wrap.vtv-sticky .vtv-controls{display:none}
78
- .vtv-wrap.vtv-sticky .vtv-tabs{padding:3px 8px}
79
- .vtv-wrap.vtv-sticky .vtv-tab{padding:3px 7px;font-size:8px}
80
- .vtv-wrap.vtv-sticky .vtv-frame{max-height:140px;min-height:100px}
81
- .vtv-wrap.vtv-sticky .vtv-frame video{max-height:140px}
82
- .vtv-wrap.vtv-sticky .vtv-load{height:100px}
83
- .vtv-wrap.vtv-sticky .vtv-err{height:100px}
84
- .vtv-wrap.vtv-sticky .vtv-head{padding:5px 10px}
85
- .vtv-wrap.vtv-sticky .vtv-title{font-size:11px}
86
- .vtv-wrap.vtv-sticky .vtv-badge{font-size:8px}
87
- .vtv-wrap.vtv-sticky .vtv-pin-btn{top:4px;right:6px}
88
- .vtv-rec-btn{background:#660000;border:1px solid #990000;color:#ff6666;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:4px;font-weight:700}
89
- .vtv-rec-btn:hover{background:#990000;color:#fff}
90
- .vtv-rec-btn.recording{background:#cc0000;border-color:#ff0000;color:#fff;animation:vtv-rec-pulse 1s infinite}
91
- @keyframes vtv-rec-pulse{0%,100%{opacity:1}50%{opacity:.5}}
92
- .vtv-rec-btn svg{width:12px;height:12px;fill:currentColor}
93
- .vtv-inline-rec{display:none;padding:8px 10px;background:#0a0a1a;border-top:1px solid #1a1a3a}
94
- .vtv-inline-rec.show{display:block}
95
- .vtv-inline-rec .rec-bar{position:relative;height:32px;background:#000;border-radius:4px;overflow:hidden;cursor:pointer;border:1px solid #2a2a4a;margin-bottom:6px;user-select:none}
96
- .vtv-inline-rec .rec-bar .rec-progress{position:absolute;top:0;bottom:0;background:rgba(155,89,182,.3);left:0;width:0%;pointer-events:none}
97
- .vtv-inline-rec .rec-bar .rec-marker{position:absolute;top:0;bottom:0;width:4px;z-index:2;border-radius:2px}
98
- .vtv-inline-rec .rec-bar .rec-marker.s{background:#2ecc71}
99
- .vtv-inline-rec .rec-bar .rec-marker.e{background:#e74c3c}
100
- .vtv-inline-rec .rec-time{display:flex;justify-content:space-between;font-size:9px;color:#888;margin-bottom:6px}
101
- .vtv-inline-rec .rec-controls{display:flex;gap:4px}
102
- .vtv-inline-rec .rec-controls button{flex:1;padding:6px;border:none;border-radius:6px;font-size:10px;font-weight:700;cursor:pointer}
103
- .vtv-inline-rec .rec-controls .rec-set-start{background:#1a3a1a;border:1px solid #2d6a2d;color:#5cb87a}
104
- .vtv-inline-rec .rec-controls .rec-set-end{background:#3a1a1a;border:1px solid #6a2d2d;color:#e74c3c}
105
- .vtv-inline-rec .rec-controls .rec-go{background:#1a0a3a;border:1px solid #3a2a6a;color:#9b59b6}
106
- .vtv-inline-rec .rec-controls .rec-reset{background:#222;border:1px solid #333;color:#888}
107
- .vtv-inline-rec .rec-hint{font-size:9px;color:#666;text-align:center;margin-top:4px}
108
- .vtv-inline-rec .rec-status{font-size:10px;color:#888;text-align:center;padding:4px;margin-top:4px;background:#111;border-radius:4px}
109
- .vtv-inline-rec .rec-status.ok{color:#2ecc71}
110
- .vtv-inline-rec .rec-status.err{color:#e74c3c}
111
- .vtv-inline-rec .rec-status.recording{color:#e74c3c;animation:vtv-rec-pulse 1s infinite}
112
- .vtv-rec-panel{display:none;padding:10px;background:#0d0d20;border-top:1px solid #2a1a4a}
113
- .vtv-rec-panel.show{display:block}
114
- .vtv-rec-panel .rec-panel-title{font-size:11px;font-weight:700;color:#9b59b6;margin-bottom:8px}
115
- .vtv-rec-panel .rec-preview-wrap{margin-bottom:8px;text-align:center}
116
- .vtv-rec-panel .rec-preview-wrap video{max-width:100%;max-height:180px;border-radius:6px;background:#000}
117
- .vtv-rec-panel .rec-ratio-row{display:flex;gap:4px;margin-bottom:8px}
118
- .vtv-rec-panel .rec-ratio-row button{flex:1;padding:6px;background:#1a1a2e;border:1px solid #2a2a4a;border-radius:6px;color:#888;font-size:10px;cursor:pointer}
119
- .vtv-rec-panel .rec-ratio-row button.active{border-color:#9b59b6;color:#9b59b6;background:#2a1a4a}
120
- .vtv-rec-panel .rec-actions{display:flex;gap:4px}
121
- .vtv-rec-panel .rec-actions button{flex:1;padding:8px;border:none;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer}
122
- .vtv-rec-panel .rec-actions .rec-download{background:#2d8659;color:#fff}
123
- .vtv-rec-panel .rec-actions .rec-share{background:#9b59b6;color:#fff}
124
- .vtv-rec-panel .rec-proc{text-align:center;padding:12px;color:#9b59b6;font-size:12px;display:none}
125
- .vtv-rec-panel .rec-title-input{width:100%;background:#1a1a2e;border:1px solid #2a2a4a;border-radius:6px;padding:8px;color:#ccc;font-size:11px;margin-bottom:8px;box-sizing:border-box}
126
- .vtv-rec-panel .rec-title-input:focus{border-color:#9b59b6;outline:none}
127
- .vtv-rec-panel .rec-ai-title-row{display:flex;gap:4px;margin-bottom:8px}
128
- .vtv-rec-panel .rec-ai-title-row button{flex:1;padding:6px;background:#1a1a2e;border:1px solid #2a2a4a;border-radius:6px;color:#888;font-size:10px;cursor:pointer}
129
- .vtv-rec-panel .rec-ai-title-row button:hover{border-color:#9b59b6;color:#9b59b6}
130
- .vtv-rec-panel .rec-ai-title-row button:disabled{opacity:.4;cursor:not-allowed}
131
- .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,.4);display:none;transition:transform .3s ease}
132
- .vtv-mini.show{display:block}
133
- .vtv-mini.hidden{transform:translateY(-88%)}
134
- .vtv-mini-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;max-height:180px}
135
- .vtv-mini-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
136
- .vtv-mini-bar{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;background:linear-gradient(90deg,#003366,#1a1a1a)}
137
- .vtv-mini-ch{font-size:10px;font-weight:700;color:#00ccff}
138
- .vtv-mini-epg{font-size:9px;color:#8ab4d8;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-left:8px}
139
- .vtv-mini-btns{display:flex;gap:3px}
140
- .vtv-mini-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 6px;border-radius:4px;cursor:pointer}
141
- .vtv-mini-btn:hover{background:#0b4a7a;color:#fff}
142
- .vtv-mini-btn.x{background:#600;border-color:#900;color:#f66}
143
- .vtv-mini-btn.x:hover{background:#900;color:#fff}
144
- .vtv-mini-peek{position:absolute;bottom:-18px;right:10px;background:#0066cc;color:#fff;font-size:9px;padding:2px 8px;border-radius:0 0 6px 6px;cursor:pointer;display:none}
145
- .vtv-mini.hidden .vtv-mini-peek{display:block}
146
- `;
147
- document.head.appendChild(style);
148
-
149
- // ===== HELPERS =====
150
- function fmtSec(s){ return String(Math.floor(s/60)).padStart(2,'0')+':'+String(Math.floor(s%60)).padStart(2,'0'); }
151
- function escH(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
152
-
153
- function recStatus(msg, type) {
154
- const el = document.getElementById('vtv-rec-status');
155
- if (el) { el.textContent = msg; el.className = 'rec-status' + (type ? ' ' + type : ''); }
156
- }
157
-
158
- function downloadBlob(blob, filename) {
159
- const a = document.createElement('a');
160
- a.href = URL.createObjectURL(blob); a.download = filename; a.click();
161
- URL.revokeObjectURL(a.href);
162
- recStatus('✅ Đã tải: ' + filename, 'ok');
163
- }
164
-
165
- function showProc(text) {
166
- const el = document.getElementById('vtv-rec-proc');
167
- if (el) { el.style.display = 'block'; document.getElementById('vtv-rec-proc-text').textContent = text; }
168
- }
169
- function hideProc() {
170
- const el = document.getElementById('vtv-rec-proc');
171
- if (el) el.style.display = 'none';
172
- }
173
-
174
- // ===== UPLOAD TO WALL (Short AI) =====
175
- function uploadToWall(blob, title, source) {
176
- const formData = new FormData();
177
- formData.append('video', blob, `vtv-${_currentCh}-${Date.now()}.webm`);
178
- formData.append('title', title || `Record LIVE ${_currentCh?.toUpperCase()} - ${new Date().toLocaleString('vi-VN')}`);
179
- formData.append('source', source || 'vtv_inline_recorder');
180
- showProc('⏳ Đang upload lên Short AI...');
181
- return fetch('/api/wall', { method: 'POST', body: formData })
182
- .then(r => r.json())
183
- .then(data => {
184
- hideProc();
185
- if (data.ok) {
186
- recStatus('✅ Đã tạo Short AI! Xem trong mục Tường AI.', 'ok');
187
- return data;
188
- } else {
189
- throw new Error(data.error || 'Upload lỗi');
190
- }
191
- })
192
- .catch(e => {
193
- hideProc();
194
- recStatus('⚠️ ' + e.message + '. Đang tải về...', 'err');
195
- setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
196
- throw e;
197
- });
198
- }
199
-
200
- // ===== AI TITLE GENERATION =====
201
- async function generateAITitle(blob, channelName) {
202
- // Get video info for AI context
203
- return new Promise((resolve) => {
204
- const video = document.createElement('video');
205
- video.src = URL.createObjectURL(blob);
206
- video.muted = true;
207
- video.onloadedmetadata = function() {
208
- URL.revokeObjectURL(video.src);
209
- const dur = Math.round(video.duration || 0);
210
- const w = video.videoWidth, h = video.videoHeight;
211
- // Generate title based on channel + duration + timestamp
212
- const now = new Date();
213
- const timeStr = now.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
214
- const dateStr = now.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit'});
215
- const titles = [
216
- `🔴 LIVE ${channelName} - ${timeStr} ${dateStr}`,
217
- `📺 ${channelName} - Video ngắn ${dur}s`,
218
- `🎬 ${channelName} Record - ${timeStr}`,
219
- `⚡ ${channelName} Highlights - ${dateStr}`,
220
- `📱 ${channelName} Short - ${timeStr} ${dateStr}`,
221
- ];
222
- // Pick one based on duration
223
- const idx = dur < 15 ? 4 : dur < 30 ? 1 : dur < 45 ? 2 : 0;
224
- resolve(titles[idx % titles.length]);
225
- };
226
- video.onerror = function() {
227
- URL.revokeObjectURL(video.src);
228
- resolve(`📺 Record ${channelName} - ${new Date().toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'})}`);
229
- };
230
- });
231
- }
232
-
233
- // ===== BUILD INLINE RECORDER =====
234
- function buildInlineRecorder() {
235
- const controls = document.querySelector('.vtv-controls');
236
- if (!controls || document.getElementById('vtv-rec-btn')) return;
237
-
238
- const recBtn = document.createElement('button');
239
- recBtn.id = 'vtv-rec-btn';
240
- recBtn.className = 'vtv-rec-btn';
241
- recBtn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
242
- recBtn.title = 'Record video ngắn từ LIVE (max 60s)';
243
- recBtn.onclick = toggleRecPanel;
244
- controls.appendChild(recBtn);
245
-
246
- const inlineRec = document.createElement('div');
247
- inlineRec.id = 'vtv-inline-rec';
248
- inlineRec.className = 'vtv-inline-rec';
249
- inlineRec.innerHTML = `
250
- <div class="rec-time">
251
- <span id="vtv-rec-time-start">00:00</span>
252
- <span id="vtv-rec-time-dur">Duration: 0s</span>
253
- <span id="vtv-rec-time-end">01:00</span>
254
- </div>
255
- <div class="rec-bar" id="vtv-rec-bar">
256
- <div class="rec-progress" id="vtv-rec-progress"></div>
257
- <div class="rec-marker s" id="vtv-rec-mk-start" style="display:none"></div>
258
- <div class="rec-marker e" id="vtv-rec-mk-end" style="display:none"></div>
259
- </div>
260
- <div class="rec-controls">
261
- <button class="rec-set-start" id="vtv-rec-set-start" onclick="window._vtvRecSetStart()">🔵 Điểm đầu</button>
262
- <button class="rec-set-end" id="vtv-rec-set-end" onclick="window._vtvRecSetEnd()" disabled>🔴 Điểm cuối</button>
263
- <button class="rec-go" id="vtv-rec-go" onclick="window._vtvRecGo()" disabled>⏺ Record!</button>
264
- <button class="rec-reset" onclick="window._vtvRecReset()">↺ Reset</button>
265
- </div>
266
- <div class="rec-hint" id="vtv-rec-hint">Chọn điểm bắt đầu → kết thúc (tối đa 60s)</div>
267
- <div class="rec-status" id="vtv-rec-status">Sẵn sàng</div>
268
- `;
269
- controls.parentNode.insertBefore(inlineRec, controls.nextSibling);
270
-
271
- const recPanel = document.createElement('div');
272
- recPanel.id = 'vtv-rec-panel';
273
- recPanel.className = 'vtv-rec-panel';
274
- recPanel.innerHTML = `
275
- <div class="rec-panel-title">🎬 Đã record xong!</div>
276
- <div class="rec-preview-wrap" id="vtv-rec-preview-wrap" style="display:none">
277
- <video id="vtv-rec-preview-vid" controls style="max-width:100%;max-height:180px;border-radius:6px;background:#000"></video>
278
- </div>
279
- <input type="text" class="rec-title-input" id="vtv-rec-title-input" placeholder="Tiêu đề video (để trống = AI tự tạo)">
280
- <div class="rec-ai-title-row">
281
- <button onclick="window._vtvRecGenAITitle()" id="vtv-rec-ai-btn">🤖 AI tạo tiêu đề</button>
282
- </div>
283
- <div class="rec-ratio-row">
284
- <button class="active" data-ratio="original" onclick="window._vtvRecSetRatio('original')">📺 Gốc</button>
285
- <button data-ratio="1:1" onclick="window._vtvRecSetRatio('1:1')">⬜ 1:1</button>
286
- <button data-ratio="9:16" onclick="window._vtvRecSetRatio('9:16')">📱 9:16</button>
287
- </div>
288
- <div class="rec-actions">
289
- <button class="rec-download" onclick="window._vtvRecDownload()">💾 Lưu về máy</button>
290
- <button class="rec-share" onclick="window._vtvRecShare()">🎬 Tạo Short AI</button>
291
- </div>
292
- <div class="rec-proc" id="vtv-rec-proc">
293
- <div class="vtv-spinner" style="width:20px;height:20px;margin:0 auto 6px"></div>
294
- <span id="vtv-rec-proc-text">Đang xử lý...</span>
295
- </div>
296
- `;
297
- inlineRec.parentNode.insertBefore(recPanel, inlineRec.nextSibling);
298
-
299
- document.getElementById('vtv-rec-bar').addEventListener('click', function(e) {
300
- if (!_rec.isRecording) handleRecBarClick(e);
301
- });
302
-
303
- document.getElementById('vtv-rec-mk-start').addEventListener('mousedown', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'start'; });
304
- document.getElementById('vtv-rec-mk-start').addEventListener('touchstart', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'start'; }, {passive:false});
305
- document.getElementById('vtv-rec-mk-end').addEventListener('mousedown', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'end'; });
306
- document.getElementById('vtv-rec-mk-end').addEventListener('touchstart', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'end'; }, {passive:false});
307
- }
308
-
309
- // Global drag
310
- document.addEventListener('mousemove', function(e) { if (_rec._dragMarker) dragMarker(e.clientX); });
311
- document.addEventListener('touchmove', function(e) { if (_rec._dragMarker) dragMarker(e.touches[0].clientX); }, {passive:false});
312
- document.addEventListener('mouseup', function(){ _rec._dragMarker = null; });
313
- document.addEventListener('touchend', function(){ _rec._dragMarker = null; });
314
-
315
- function dragMarker(clientX) {
316
- const bar = document.getElementById('vtv-rec-bar');
317
- if (!bar) return;
318
- const rect = bar.getBoundingClientRect();
319
- const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
320
- const time = pct * 60;
321
- if (_rec._dragMarker === 'start') {
322
- if (_rec.endTime !== null && time >= _rec.endTime - 1) return;
323
- _rec.startTime = time;
324
- const mk = document.getElementById('vtv-rec-mk-start');
325
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
326
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
327
- } else {
328
- if (_rec.startTime !== null && time <= _rec.startTime + 1) return;
329
- _rec.endTime = time;
330
- const mk = document.getElementById('vtv-rec-mk-end');
331
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
332
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
333
- }
334
- updateRecBar();
335
- }
336
-
337
- function handleRecBarClick(e) {
338
- const video = document.getElementById('vtv-player');
339
- if (!video || !video.src) { recStatus('⚠️ Chưa có stream!'); return; }
340
- const bar = document.getElementById('vtv-rec-bar');
341
- const rect = bar.getBoundingClientRect();
342
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
343
- const time = pct * 60;
344
- if (_rec.startTime === null) {
345
- _rec.startTime = time;
346
- const mk = document.getElementById('vtv-rec-mk-start');
347
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
348
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
349
- document.getElementById('vtv-rec-set-end').disabled = false;
350
- recStatus('Điểm đầu: ' + fmtSec(time) + '. Chọn điểm cuối.', '');
351
- } else if (_rec.endTime === null) {
352
- if (time <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
353
- const dur = time - _rec.startTime;
354
- if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
355
- _rec.endTime = time;
356
- const mk = document.getElementById('vtv-rec-mk-end');
357
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
358
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
359
- updateRecBar();
360
- recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
361
- document.getElementById('vtv-rec-go').disabled = false;
362
- }
363
- }
364
-
365
- function updateRecBar() {
366
- const s = _rec.startTime || 0, e = _rec.endTime || 60;
367
- const dur = e - s;
368
- document.getElementById('vtv-rec-progress').style.left = (s/60*100) + '%';
369
- document.getElementById('vtv-rec-progress').style.width = (dur/60*100) + '%';
370
- document.getElementById('vtv-rec-time-dur').textContent = 'Duration: ' + Math.round(dur) + 's';
371
- }
372
-
373
- // ===== RECORDER CONTROLS =====
374
- window._vtvRecSetStart = function() {
375
- const video = document.getElementById('vtv-player');
376
- if (!video || !video.src) { recStatus('⚠️ Chưa có stream!', 'err'); return; }
377
- _rec.startTime = video.currentTime || 0;
378
- const pct = _rec.startTime / 60 * 100;
379
- const mk = document.getElementById('vtv-rec-mk-start');
380
- mk.style.display = 'block'; mk.style.left = pct + '%';
381
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(_rec.startTime);
382
- document.getElementById('vtv-rec-set-end').disabled = false;
383
- recStatus('Điểm đầu: ' + fmtSec(_rec.startTime), '');
384
- };
385
-
386
- window._vtvRecSetEnd = function() {
387
- if (_rec.startTime === null) { recStatus('⚠️ Chọn điểm đầu trước!', 'err'); return; }
388
- const video = document.getElementById('vtv-player');
389
- if (!video) return;
390
- const cur = video.currentTime || 0;
391
- if (cur <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
392
- const dur = cur - _rec.startTime;
393
- if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
394
- _rec.endTime = cur;
395
- const pct = cur / 60 * 100;
396
- const mk = document.getElementById('vtv-rec-mk-end');
397
- mk.style.display = 'block'; mk.style.left = pct + '%';
398
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(cur);
399
- updateRecBar();
400
- recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
401
- document.getElementById('vtv-rec-go').disabled = false;
402
- };
403
-
404
- window._vtvRecReset = function() {
405
- _rec.startTime = null; _rec.endTime = null; _rec.isRecording = false; _rec.blob = null;
406
- if (_rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
407
- clearTimeout(_rec._recTimer);
408
- document.getElementById('vtv-rec-mk-start').style.display = 'none';
409
- document.getElementById('vtv-rec-mk-end').style.display = 'none';
410
- document.getElementById('vtv-rec-progress').style.width = '0%';
411
- document.getElementById('vtv-rec-time-start').textContent = '00:00';
412
- document.getElementById('vtv-rec-time-end').textContent = '01:00';
413
- document.getElementById('vtv-rec-time-dur').textContent = 'Duration: 0s';
414
- document.getElementById('vtv-rec-set-start').disabled = false;
415
- document.getElementById('vtv-rec-set-end').disabled = true;
416
- document.getElementById('vtv-rec-go').disabled = true;
417
- document.getElementById('vtv-rec-panel').classList.remove('show');
418
- document.getElementById('vtv-rec-title-input').value = '';
419
- recStatus('Đã reset.', '');
420
- const btn = document.getElementById('vtv-rec-btn');
421
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
422
- };
423
-
424
- window._vtvRecSetRatio = function(ratio) {
425
- _rec.ratio = ratio;
426
- document.querySelectorAll('#vtv-rec-panel .rec-ratio-row button').forEach(b => b.classList.toggle('active', b.dataset.ratio === ratio));
427
- };
428
-
429
- // ===== RECORD — captureStream with audio (unmute during record) =====
430
- window._vtvRecGo = function() {
431
- if (_rec.startTime === null || _rec.endTime === null) return;
432
- if (_rec.isRecording) {
433
- if (_rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
434
- _rec.isRecording = false;
435
- clearTimeout(_rec._recTimer);
436
- const btn = document.getElementById('vtv-rec-btn');
437
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
438
- return;
439
- }
440
-
441
- const video = document.getElementById('vtv-player');
442
- if (!video) { recStatus('⚠️ Không tìm thấy video!', 'err'); return; }
443
-
444
- _rec.chunks = [];
445
- _rec.isRecording = true;
446
-
447
- // CRITICAL: Unmute video so captureStream includes audio tracks
448
- const wasMuted = video.muted;
449
- video.muted = false;
450
- // Also set volume low so user doesn't hear double audio
451
- const wasVolume = video.volume;
452
- video.volume = 0.3;
453
-
454
- let stream;
455
- try {
456
- stream = video.captureStream ? video.captureStream() : video.mozCaptureStream();
457
- if (!stream) throw new Error('captureStream not supported');
458
- } catch(e) {
459
- video.muted = wasMuted; video.volume = wasVolume;
460
- recStatus('❌ ' + e.message, 'err');
461
- _rec.isRecording = false;
462
- return;
463
- }
464
-
465
- const audioTracks = stream.getAudioTracks();
466
- console.log('[VTV Rec] Audio tracks:', audioTracks.length, 'Video tracks:', stream.getVideoTracks().length);
467
-
468
- let mimeType = 'video/webm;codecs=vp9,opus';
469
- if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm;codecs=vp8,opus';
470
- if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm';
471
-
472
- try { _rec.recorder = new MediaRecorder(stream, {mimeType}); }
473
- catch(e) {
474
- video.muted = wasMuted; video.volume = wasVolume;
475
- recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err');
476
- _rec.isRecording = false;
477
- return;
478
- }
479
-
480
- _rec.recorder.ondataavailable = e => { if(e.data && e.data.size > 0) _rec.chunks.push(e.data); };
481
- _rec.recorder.onstop = function() {
482
- _rec.isRecording = false;
483
- video.muted = wasMuted;
484
- video.volume = wasVolume;
485
- _rec.blob = new Blob(_rec.chunks, {type: 'video/webm'});
486
- const btn = document.getElementById('vtv-rec-btn');
487
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
488
- document.getElementById('vtv-rec-panel').classList.add('show');
489
- const pv = document.getElementById('vtv-rec-preview-vid');
490
- pv.src = URL.createObjectURL(_rec.blob);
491
- document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
492
- const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
493
- const hasAudio = _rec.blob.size > 50000; // rough heuristic
494
- recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
495
- };
496
- _rec.recorder.onerror = e => {
497
- _rec.isRecording = false;
498
- video.muted = wasMuted; video.volume = wasVolume;
499
- recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
500
- };
501
-
502
- _rec.recorder.start(500); // 500ms timeslice
503
- video.currentTime = _rec.startTime;
504
-
505
- const btn = document.getElementById('vtv-rec-btn');
506
- if (btn) { btn.classList.add('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> Dừng'; }
507
- recStatus('🔴 ĐANG RECORD...' + (audioTracks.length > 0 ? ' (có audio)' : ''), 'recording');
508
-
509
- const dur = (_rec.endTime - _rec.startTime) * 1000;
510
- _rec._recTimer = setTimeout(() => {
511
- if (_rec.isRecording && _rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
512
- }, dur + 500);
513
- };
514
-
515
- // ===== AI TITLE =====
516
- window._vtvRecGenAITitle = async function() {
517
- if (!_rec.blob) { recStatus('⚠️ Chưa record!', 'err'); return; }
518
- const btn = document.getElementById('vtv-rec-ai-btn');
519
- const input = document.getElementById('vtv-rec-title-input');
520
- if (!btn || !input) return;
521
-
522
- btn.disabled = true;
523
- btn.textContent = '⏳ Đang tạo...';
524
-
525
- try {
526
- const ch = CHANNELS.find(c => c.id === _currentCh);
527
- const channelName = ch ? ch.name : (_currentCh || 'VTV');
528
- const title = await generateAITitle(_rec.blob, channelName);
529
- input.value = title;
530
- recStatus('✅ Đã tạo tiêu đề AI: ' + title, 'ok');
531
- } catch(e) {
532
- recStatus('⚠️ Lỗi AI: ' + e.message, 'err');
533
- } finally {
534
- btn.disabled = false;
535
- btn.textContent = '🤖 AI tạo tiêu đề';
536
- }
537
- };
538
-
539
- // ===== DOWNLOAD =====
540
- window._vtvRecDownload = function() {
541
- if (!_rec.blob) return;
542
- const title = document.getElementById('vtv-rec-title-input')?.value || '';
543
- if (_rec.ratio === 'original') {
544
- downloadBlob(_rec.blob, `vtv-${_currentCh}-${Date.now()}.webm`);
545
- } else {
546
- processRatio(_rec.blob, _rec.ratio, 'download', title);
547
- }
548
- };
549
-
550
- // ===== SHARE TO SHORT AI =====
551
- window._vtvRecShare = function() {
552
- if (!_rec.blob) return;
553
- const title = document.getElementById('vtv-rec-title-input')?.value || '';
554
- if (_rec.ratio !== 'original') {
555
- processRatio(_rec.blob, _rec.ratio, 'upload', title);
556
- } else {
557
- uploadToWall(_rec.blob, title);
558
- }
559
- };
560
-
561
- // ===== RATIO CROP — v6: reliable render =====
562
- // Key fixes:
563
- // 1. video.volume=0 (not muted) to avoid autoplay block
564
- // 2. video.ended event to stop (NOT setTimeout with duration)
565
- // 3. 200ms timeslice for frequent data collection
566
- // 4. rec.requestData() flush before stop
567
- // 5. Parallel audioVideo element to capture audio from blob
568
- function processRatio(blob, ratio, action, title) {
569
- showProc(`🔄 Đang xử lý ${ratio}...`);
570
-
571
- const video = document.createElement('video');
572
- video.src = URL.createObjectURL(blob);
573
- video.volume = 0;
574
- video.preload = 'auto';
575
- video.playsInline = true;
576
-
577
- function onReady() {
578
- video.removeEventListener('loadedmetadata', onReady);
579
- video.removeEventListener('canplay', onReady);
580
-
581
- const vw = video.videoWidth, vh = video.videoHeight;
582
- if (!vw || !vh) {
583
- hideProc(); recStatus('❌ Video không có kích thước hợp lệ.', 'err');
584
- URL.revokeObjectURL(video.src); return;
585
- }
586
-
587
- let cw, ch, sx, sy;
588
- if (ratio === '1:1') {
589
- const size = Math.min(vw, vh); cw = ch = size;
590
- sx = Math.floor((vw - size) / 2); sy = Math.floor((vh - size) / 2);
591
- } else if (ratio === '9:16') {
592
- ch = vh; cw = Math.min(vw, Math.round(vh * 9 / 16));
593
- sx = Math.floor((vw - cw) / 2); sy = 0;
594
- } else { cw = vw; ch = vh; sx = sy = 0; }
595
-
596
- showProc(`🔄 Render ${cw}x${ch}...`);
597
-
598
- const canvas = document.createElement('canvas');
599
- canvas.width = cw; canvas.height = ch;
600
- const ctx = canvas.getContext('2d');
601
- const canvasStream = canvas.captureStream(30);
602
-
603
- // Parallel video to capture audio from blob
604
- let recordStream = canvasStream;
605
- const audioVideo = document.createElement('video');
606
- audioVideo.src = video.src;
607
- audioVideo.muted = true;
608
- audioVideo.volume = 0;
609
- audioVideo.preload = 'auto';
610
- audioVideo.playsInline = true;
611
-
612
- audioVideo.onloadedmetadata = function() {
613
- try {
614
- const aStr = audioVideo.captureStream ? audioVideo.captureStream() : null;
615
- if (aStr) {
616
- const tracks = aStr.getAudioTracks();
617
- if (tracks.length > 0) {
618
- recordStream = new MediaStream([...canvasStream.getVideoTracks(), ...tracks]);
619
- console.log('[VTV Crop] Audio merged:', tracks.length, 'tracks');
620
- }
621
- }
622
- } catch(e) { console.warn('[VTV Crop] Audio merge failed:', e); }
623
- };
624
-
625
- const mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')
626
- ? 'video/webm;codecs=vp9,opus'
627
- : MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus')
628
- ? 'video/webm;codecs=vp8,opus' : 'video/webm';
629
-
630
- let rec;
631
- try { rec = new MediaRecorder(recordStream, {mimeType: mime}); }
632
- catch(e) { hideProc(); recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err'); URL.revokeObjectURL(video.src); return; }
633
-
634
- const chunks = [];
635
- rec.ondataavailable = e => { if(e.data && e.data.size > 0) chunks.push(e.data); };
636
- rec.onstop = () => {
637
- hideProc(); URL.revokeObjectURL(video.src);
638
- if (chunks.length === 0) {
639
- recStatus('❌ Render thất bại. Thử lại hoặc chọn "Gốc".', 'err');
640
- return;
641
- }
642
- const out = new Blob(chunks, {type: 'video/webm'});
643
- if (action === 'download') {
644
- downloadBlob(out, `vtv-${_currentCh}-${ratio.replace(':','x')}-${Date.now()}.webm`);
645
- } else {
646
- uploadToWall(out, title);
647
- }
648
- };
649
- rec.onerror = e => { hideProc(); recStatus('❌ Lỗi render: ' + (e.error?.message || '?'), 'err'); URL.revokeObjectURL(video.src); };
650
-
651
- rec.start(200);
652
-
653
- audioVideo.play().catch(() => {});
654
-
655
- video.play().then(() => {
656
- let finished = false;
657
- function finish() {
658
- if (finished) return;
659
- finished = true;
660
- try { rec.requestData(); } catch(e) {}
661
- setTimeout(() => {
662
- if (rec.state !== 'inactive') rec.stop();
663
- try { audioVideo.pause(); } catch(e) {}
664
- }, 300);
665
- }
666
- video.onended = finish;
667
- audioVideo.onended = finish;
668
-
669
- function draw() {
670
- if (video.paused || video.ended) return;
671
- ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch);
672
- requestAnimationFrame(draw);
673
- }
674
- draw();
675
- }).catch(e => {
676
- hideProc(); recStatus('❌ Lỗi phát video: ' + e.message, 'err');
677
- URL.revokeObjectURL(video.src);
678
- });
679
- }
680
-
681
- video.onerror = function() {
682
- hideProc(); recStatus('❌ Không đọc được video.', 'err');
683
- URL.revokeObjectURL(video.src);
684
- };
685
-
686
- video.addEventListener('loadedmetadata', onReady);
687
- video.addEventListener('canplay', onReady);
688
- }
689
-
690
- // ===== TOGGLE REC PANEL =====
691
- function toggleRecPanel() {
692
- const panel = document.getElementById('vtv-inline-rec');
693
- if (!panel) { buildInlineRecorder(); setTimeout(toggleRecPanel, 200); return; }
694
- _rec.active = !_rec.active;
695
- panel.classList.toggle('show', _rec.active);
696
- const btn = document.getElementById('vtv-rec-btn');
697
- if (btn) {
698
- if (_rec.active) {
699
- btn.style.background = '#3a1a1a';
700
- btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> Đóng';
701
- } else {
702
- btn.style.background = '';
703
- btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
704
- document.getElementById('vtv-rec-panel').classList.remove('show');
705
- }
706
- }
707
- }
708
-
709
- // ===== PIN BUTTON =====
710
- function setupPinButton(){
711
- const block = document.getElementById('vtv-block');
712
- if(!block) return;
713
- const header = block.querySelector('.vtv-head');
714
- if(!header || document.getElementById('vtv-pin-btn')) return;
715
- const btn = document.createElement('button');
716
- btn.id = 'vtv-pin-btn'; btn.className = 'vtv-pin-btn';
717
- btn.innerHTML = '📌 Ghim';
718
- btn.title = 'Ghim VTV cố định trên đầu trang';
719
- btn.onclick = function(e){
720
- e.stopPropagation(); _vtvPinned = !_vtvPinned;
721
- if(_vtvPinned){ block.classList.add('vtv-sticky'); btn.classList.add('pinned'); btn.innerHTML = '📌 Đã ghim'; }
722
- else { block.classList.remove('vtv-sticky'); btn.classList.remove('pinned'); btn.innerHTML = '📌 Ghim'; }
723
- };
724
- header.style.position = 'relative';
725
- header.appendChild(btn);
726
- }
727
-
728
- // ===== MINI PLAYER =====
729
- function createMiniPlayer(){
730
- if(document.getElementById('vtv-mini')) return;
731
- const mini = document.createElement('div');
732
- mini.className = 'vtv-mini'; mini.id = 'vtv-mini';
733
- mini.innerHTML = `<div class="vtv-mini-frame"><video id="vtv-mini-vid" playsinline muted preload="auto"></video></div>
734
- <div class="vtv-mini-bar"><div style="display:flex;align-items:center;flex:1;min-width:0"><span class="vtv-mini-ch" id="vtv-mini-ch">VTV</span><span class="vtv-mini-epg" id="vtv-mini-epg"></span></div>
735
- <div class="vtv-mini-btns"><button class="vtv-mini-btn" id="vtv-mini-pip">📺</button><button class="vtv-mini-btn" id="vtv-mini-expand">⬆️</button><button class="vtv-mini-btn" id="vtv-mini-hide">⬇️</button><button class="vtv-mini-btn x" id="vtv-mini-close">✕</button></div></div>
736
- <div class="vtv-mini-peek" id="vtv-mini-peek">📺 Xem</div>`;
737
- document.body.appendChild(mini);
738
- document.getElementById('vtv-mini-pip').onclick = async function(){
739
- const v = document.getElementById('vtv-mini-vid');
740
- try{ if(document.pictureInPictureElement === v) await document.exitPictureInPicture(); else await v.requestPictureInPicture(); }catch(e){}
741
- };
742
- document.getElementById('vtv-mini-expand').onclick = function(){ closeMiniPlayer(); if(typeof switchCat==='function') switchCat('home'); };
743
- document.getElementById('vtv-mini-hide').onclick = ()=>{ mini.classList.add('hidden'); };
744
- document.getElementById('vtv-mini-close').onclick = closeMiniPlayer;
745
- document.getElementById('vtv-mini-peek').onclick = ()=>{ mini.classList.remove('hidden'); };
746
- }
747
- function closeMiniPlayer(){ const m=document.getElementById('vtv-mini'); if(m){const v=document.getElementById('vtv-mini-vid'); if(v){v.pause();v.src='';} m.remove();} _miniActive=false; }
748
- function activateMiniPlayer(){
749
- if(!_currentCh) return; createMiniPlayer(); _miniActive=true;
750
- const mini=document.getElementById('vtv-mini'); const mv=document.getElementById('vtv-mini-vid'); const v=document.getElementById('vtv-player');
751
- if(_hls){const h=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:10}); h.loadSource(STREAMS[_currentCh][0]); h.attachMedia(mv); mv.play().catch(()=>{});}
752
- else if(v&&v.src){mv.src=v.src; mv.play().catch(()=>{});}
753
- const ch=CHANNELS.find(c=>c.id===_currentCh); if(ch) document.getElementById('vtv-mini-ch').textContent=ch.name;
754
- fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[]; const n=p.find(x=>x.now); document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});
755
- mini.classList.add('show'); mini.classList.remove('hidden');
756
- }
757
-
758
- // ===== MAIN PLAYER =====
759
- async function loadAllStreams(){
760
- if(_loading) return; _loading=true;
761
- const el=document.getElementById('vtv-load'); if(el) el.innerHTML='<div class="vtv-spinner"></div>Đang tải kênh...';
762
- try{const r=await fetch('/api/vtv/streams',{signal:AbortSignal.timeout(10000)}); if(r.ok){const d=await r.json(); CHANNELS.forEach(ch=>{const i=d[ch.id]; if(i&&i.stream_url){let u=i.stream_url; if(NEEDS_PROXY.test(u)) u='/api/proxy/m3u8/vtv?url='+encodeURIComponent(u); STREAMS[ch.id]=[u];} else STREAMS[ch.id]=[];});}}catch(e){}
763
- CHANNELS.forEach(ch=>{const t=document.getElementById('vtvt-'+ch.id); if(t){if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){t.classList.remove('off'); t.textContent=ch.name;}else{t.style.opacity='0.35'; t.textContent=ch.name+' ✕';}}});
764
- _loading=false; _streamsLoaded=true;
765
- }
766
-
767
- function buildBlock(){
768
- const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
769
- let tabs=''; CHANNELS.forEach(ch=>{tabs+='<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';});
770
- w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
771
- '<div class="vtv-tabs">'+tabs+'</div>'+
772
- '<div class="vtv-frame"><div class="vtv-load" id="vtv-load"><div class="vtv-spinner"></div>Đang tải kênh...</div><video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video><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></div>'+
773
- '<div class="vtv-controls"><button class="vtv-pip-btn" id="vtv-pip-btn" onclick="window._vtvTogglePiP()"><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>PiP</button><span style="flex:1"></span><span style="font-size:9px;color:#666" id="vtv-status"></span></div>'+
774
- '<div class="vtv-epg" id="vtv-epg"><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><div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải...</div></div></div>';
775
- return w;
776
- }
777
-
778
- async function loadEpg(chId){
779
- const el=document.getElementById('vtv-epg-list'); if(!el) return;
780
- el.innerHTML='<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
781
- try{const r=await fetch('/api/vtv/epg/'+chId,{signal:AbortSignal.timeout(10000)}); if(!r.ok) throw new Error(); const d=await r.json(); const p=d.programs||[]; if(!p.length){el.innerHTML='<div class="vtv-epg-empty">Chưa có lịch</div>';return;} let h=''; p.forEach(x=>{const n=x.now?' now':''; h+='<div class="vtv-epg-item'+n+'"><span class="t">'+(x.time||'')+(x.end_time?'-'+x.end_time:'')+'</span> <span class="n">'+(x.title||'')+'</span></div>';}); el.innerHTML=h; const ni=el.querySelector('.vtv-epg-item.now'); if(ni) ni.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});}catch(e){el.innerHTML='<div class="vtv-epg-empty">Không tải được lịch</div>';}
782
- }
783
-
784
- window._vtvToggleEpg=function(){const l=document.getElementById('vtv-epg-list'),b=document.getElementById('vtv-epg-toggle'); if(!l||!b)return; if(l.style.display==='none'){l.style.display='flex';b.textContent='Ẩn';}else{l.style.display='none';b.textContent='Hiện';}};
785
- window._vtvTogglePiP=async function(){const v=document.getElementById('vtv-player'); const b=document.getElementById('vtv-pip-btn'); if(!v)return; try{if(document.pictureInPictureElement===v){await document.exitPictureInPicture();_pipActive=false;if(b)b.classList.remove('on');}else{await v.requestPictureInPicture();_pipActive=true;if(b)b.classList.add('on');}}catch(e){}};
786
-
787
- function pinBlock(){
788
- const h=document.getElementById('view-home'); if(!h||document.getElementById('vtv-block')) return;
789
- h.insertBefore(buildBlock(),h.firstChild); _blockInserted=true;
790
- setTimeout(()=>{setupPinButton();buildInlineRecorder();},500);
791
- if(!_streamsLoaded){loadAllStreams().then(()=>{setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL),300);});}
792
- }
793
-
794
- window._vtvRetry=function(){if(_currentCh)window._vtvPlay(_currentCh);};
795
-
796
- window._vtvPlay=function(chId){
797
- const ch=CHANNELS.find(c=>c.id===chId); if(!ch) return; _currentCh=chId;
798
- document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
799
- const tab=document.getElementById('vtvt-'+chId); if(tab) tab.classList.add('on');
800
- const video=document.getElementById('vtv-player'); const errEl=document.getElementById('vtv-err'); const loadEl=document.getElementById('vtv-load'); const errMsg=document.getElementById('vtv-err-msg');
801
- video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
802
- loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
803
- if(_hls){_hls.destroy();_hls=null;}
804
- const urls=STREAMS[chId]||[];
805
- if(!urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=chId==='vtvprime'?'VTVPrime: Kênh trả phí.':ch.name+': Không có luồng.';return;}
806
- _tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
807
- loadEpg(chId);
808
- if(_miniActive){document.getElementById('vtv-mini-ch').textContent=ch.name;fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[];const n=p.find(x=>x.now);document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});}
809
- };
810
-
811
- function _tryPlay(video,urls,idx,name,loadEl,errEl,errMsg){
812
- if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
813
- const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
814
- if(typeof Hls!=='undefined'&&Hls.isSupported()){
815
- const hls=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
816
- hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
817
- let rec=0; hls.on(Hls.Events.ERROR,(ev,data)=>{if(data.fatal){if(data.type===Hls.ErrorTypes.NETWORK_ERROR){rec++;if(rec<=3)setTimeout(()=>hls.startLoad(),2000);else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){try{hls.recoverMediaError();}catch(e){}}else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}});
818
- }else if(video.canPlayType('application/vnd.apple.mpegurl')){
819
- video.src=src;
820
- video.addEventListener('loadedmetadata',()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';},{once:true});
821
- video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
822
- }else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
823
- }
824
-
825
- // ===== NAV HOOKS =====
826
- const _origShowView=window.showView;
827
- window.showView=function(id){if(id==='view-home'&&_miniActive)closeMiniPlayer();else if(id!=='view-home'&&_currentCh&&STREAMS[_currentCh]&&STREAMS[_currentCh].length>0&&!_miniActive)activateMiniPlayer();return _origShowView.apply(this,arguments);};
828
- const _origReadArticle=window.readArticle; if(_origReadArticle){window.readArticle=function(url){if(_currentCh&&STREAMS[_currentCh]&&STREAMS[_currentCh].length>0&&!_miniActive)activateMiniPlayer();return _origReadArticle.apply(this,arguments);};}
829
- const _origSwitchCat=window.switchCat; if(_origSwitchCat){window.switchCat=function(id){if(id==='home'){if(_miniActive)closeMiniPlayer();}else if(_currentCh&&STREAMS[_currentCh]&&STREAMS[_currentCh].length>0&&!_miniActive)activateMiniPlayer();return _origSwitchCat.apply(this,arguments);};}
830
-
831
- const _origLoadHome=window.loadHome;
832
- if(_origLoadHome&&!_origLoadHome.__vtvWrapped){
833
- window.loadHome=async function(){const old=document.getElementById('vtv-block');if(old)old.remove();_blockInserted=false;const r=await _origLoadHome.apply(this,arguments);try{pinBlock();}catch(e){}return r;};
834
- window.loadHome.__vtvWrapped=true;
835
- }
836
- })();
 
1
+ placeholder