bep40 commited on
Commit
0cefbd8
·
verified ·
1 Parent(s): 5dffe34

Update yt_live.js: handle unavailable channel status (vtv10/vtvprime show 🔒), fix loadAllStreams to use response data

Browse files
Files changed (1) hide show
  1. static/yt_live.js +837 -1
static/yt_live.js CHANGED
@@ -1 +1,837 @@
1
- placeholder
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ let d={};
763
+ try{const r=await fetch('/api/vtv/streams',{signal:AbortSignal.timeout(10000)}); if(r.ok){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){}
764
+ CHANNELS.forEach(ch=>{const t=document.getElementById('vtvt-'+ch.id); if(t){const data=d[ch.id]; if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){t.classList.remove('off'); t.textContent=ch.name;}else if(data&&data.status==='unavailable'){t.style.opacity='0.35'; t.textContent=ch.name+' 🔒';}else{t.style.opacity='0.35'; t.textContent=ch.name+' ✕';}}});
765
+ _loading=false; _streamsLoaded=true;
766
+ }
767
+
768
+ function buildBlock(){
769
+ const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
770
+ let tabs=''; CHANNELS.forEach(ch=>{tabs+='<button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button>';});
771
+ w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
772
+ '<div class="vtv-tabs">'+tabs+'</div>'+
773
+ '<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>'+
774
+ '<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>'+
775
+ '<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>';
776
+ return w;
777
+ }
778
+
779
+ async function loadEpg(chId){
780
+ const el=document.getElementById('vtv-epg-list'); if(!el) return;
781
+ el.innerHTML='<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
782
+ 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>';}
783
+ }
784
+
785
+ 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';}};
786
+ 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){}};
787
+
788
+ function pinBlock(){
789
+ const h=document.getElementById('view-home'); if(!h||document.getElementById('vtv-block')) return;
790
+ h.insertBefore(buildBlock(),h.firstChild); _blockInserted=true;
791
+ setTimeout(()=>{setupPinButton();buildInlineRecorder();},500);
792
+ if(!_streamsLoaded){loadAllStreams().then(()=>{setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL),300);});}
793
+ }
794
+
795
+ window._vtvRetry=function(){if(_currentCh)window._vtvPlay(_currentCh);};
796
+
797
+ window._vtvPlay=function(chId){
798
+ const ch=CHANNELS.find(c=>c.id===chId); if(!ch) return; _currentCh=chId;
799
+ document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
800
+ const tab=document.getElementById('vtvt-'+chId); if(tab) tab.classList.add('on');
801
+ 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');
802
+ video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
803
+ loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
804
+ if(_hls){_hls.destroy();_hls=null;}
805
+ const urls=STREAMS[chId]||[];
806
+ if(!urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=chId==='vtvprime'?'VTVPrime: Kênh trả phí/chưa hỗ trợ.':chId==='vtv10'?'VTV Cần Thơ: Chưa có luồng public.':ch.name+': Không có luồng.';return;}
807
+ _tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
808
+ loadEpg(chId);
809
+ 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(()=>{});}
810
+ };
811
+
812
+ function _tryPlay(video,urls,idx,name,loadEl,errEl,errMsg){
813
+ if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
814
+ const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
815
+ if(typeof Hls!=='undefined'&&Hls.isSupported()){
816
+ const hls=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
817
+ hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
818
+ 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);}}});
819
+ }else if(video.canPlayType('application/vnd.apple.mpegurl')){
820
+ video.src=src;
821
+ video.addEventListener('loadedmetadata',()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';},{once:true});
822
+ video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
823
+ }else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
824
+ }
825
+
826
+ // ===== NAV HOOKS =====
827
+ const _origShowView=window.showView;
828
+ 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);};
829
+ 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);};}
830
+ 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);};}
831
+
832
+ const _origLoadHome=window.loadHome;
833
+ if(_origLoadHome&&!_origLoadHome.__vtvWrapped){
834
+ 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;};
835
+ window.loadHome.__vtvWrapped=true;
836
+ }
837
+ })();