Fix VTV6 stream + fix click tabs vtv2/vtv3/vtv9

#34
by bep40 - opened
Files changed (2) hide show
  1. static/yt_live.js +3 -843
  2. vtv_api.py +118 -8
static/yt_live.js CHANGED
@@ -1,6 +1,7 @@
1
  // === VNEWS — VTV LIVE + Inline Recorder v9 ===
2
  // Features: PiP, mini-player, INLINE RECORDER, VTVGO POPUP players for each channel
3
  // FIX v10: Correct VTVGO popup URLs for all channels + World Cup 2026 TS stream links
 
4
 
5
  (function(){
6
  if(window._ytLiveLoaded) return;
@@ -55,7 +56,7 @@
55
  .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}
56
  .vtv-tab:hover{background:#0b4a7a;color:#fff}
57
  .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
58
- .vtv-tab.off{opacity:.35;pointer-events:none}
59
  .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
60
  .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
61
  .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}
@@ -171,845 +172,4 @@
171
  .vtv-popup-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;margin-left:4px}
172
  .vtv-popup-btn:hover{background:#0b4a7a;color:#fff}
173
  `;
174
- document.head.appendChild(style);
175
-
176
- // ===== HELPERS =====
177
- function fmtSec(s){ return String(Math.floor(s/60)).padStart(2,'0')+':'+String(Math.floor(s%60)).padStart(2,'0'); }
178
- function escH(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
179
-
180
- function recStatus(msg, type) {
181
- const el = document.getElementById('vtv-rec-status');
182
- if (el) { el.textContent = msg; el.className = 'rec-status' + (type ? ' ' + type : ''); }
183
- }
184
-
185
- function downloadBlob(blob, filename) {
186
- const a = document.createElement('a');
187
- a.href = URL.createObjectURL(blob); a.download = filename; a.click();
188
- URL.revokeObjectURL(a.href);
189
- recStatus('✅ Đã tải: ' + filename, 'ok');
190
- }
191
-
192
- function showProc(text) {
193
- const el = document.getElementById('vtv-rec-proc');
194
- if (el) { el.style.display = 'block'; document.getElementById('vtv-rec-proc-text').textContent = text; }
195
- }
196
- function hideProc() {
197
- const el = document.getElementById('vtv-rec-proc');
198
- if (el) el.style.display = 'none';
199
- }
200
-
201
- // ===== UPLOAD TO WALL (Short AI) — v7: robust JSON parse =====
202
- function uploadToWall(blob, title, source) {
203
- const formData = new FormData();
204
- formData.append('video', blob, `vtv-${_currentCh}-${Date.now()}.webm`);
205
- formData.append('title', title || `Record LIVE ${_currentCh?.toUpperCase()} - ${new Date().toLocaleString('vi-VN')}`);
206
- formData.append('source', source || 'vtv_inline_recorder');
207
- showProc('⏳ Đang upload lên Short AI...');
208
- return fetch('/api/wall', { method: 'POST', body: formData })
209
- .then(r => {
210
- console.log('[VTV Upload] HTTP status:', r.status, r.statusText);
211
- return r.text().then(text => {
212
- console.log('[VTV Upload] Response text:', text.substring(0, 200));
213
- let data;
214
- try { data = JSON.parse(text); } catch(e) {
215
- console.error('[VTV Upload] JSON parse failed:', e, 'Raw:', text.substring(0, 500));
216
- throw new Error('Server trả về lỗi (không phải JSON). Status: ' + r.status);
217
- }
218
- return data;
219
- });
220
- })
221
- .then(data => {
222
- hideProc();
223
- console.log('[VTV Upload] Parsed data:', data);
224
- if (data && data.ok === true) {
225
- recStatus('✅ Đã tạo Short AI! Xem trong mục Tường AI.', 'ok');
226
- return data;
227
- } else {
228
- throw new Error(data && data.error ? data.error : 'Upload thất bại (ok=false)');
229
- }
230
- })
231
- .catch(e => {
232
- hideProc();
233
- console.error('[VTV Upload] Error:', e);
234
- recStatus('⚠️ ' + e.message + '. Đang tải về...', 'err');
235
- setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
236
- throw e;
237
- });
238
- }
239
-
240
- // ===== AI TITLE GENERATION =====
241
- async function generateAITitle(blob, channelName) {
242
- return new Promise((resolve) => {
243
- const video = document.createElement('video');
244
- video.src = URL.createObjectURL(blob);
245
- video.muted = true;
246
- video.onloadedmetadata = function() {
247
- URL.revokeObjectURL(video.src);
248
- const dur = Math.round(video.duration || 0);
249
- const now = new Date();
250
- const timeStr = now.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
251
- const dateStr = now.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit'});
252
- const titles = [
253
- `🔴 LIVE ${channelName} - ${timeStr} ${dateStr}`,
254
- `📺 ${channelName} - Video ngắn ${dur}s`,
255
- `🎬 ${channelName} Record - ${timeStr}`,
256
- `⚡ ${channelName} Highlights - ${dateStr}`,
257
- `📱 ${channelName} Short - ${timeStr} ${dateStr}`,
258
- ];
259
- const idx = dur < 15 ? 4 : dur < 30 ? 1 : dur < 45 ? 2 : 0;
260
- resolve(titles[idx % titles.length]);
261
- };
262
- video.onerror = function() {
263
- URL.revokeObjectURL(video.src);
264
- resolve(`📺 Record ${channelName} - ${new Date().toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'})}`);
265
- };
266
- });
267
- }
268
-
269
- // ===== BUILD INLINE RECORDER =====
270
- function buildInlineRecorder() {
271
- const controls = document.querySelector('.vtv-controls');
272
- if (!controls || document.getElementById('vtv-rec-btn')) return;
273
-
274
- const recBtn = document.createElement('button');
275
- recBtn.id = 'vtv-rec-btn';
276
- recBtn.className = 'vtv-rec-btn';
277
- recBtn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
278
- recBtn.title = 'Record video ngắn từ LIVE (max 60s)';
279
- recBtn.onclick = toggleRecPanel;
280
- controls.appendChild(recBtn);
281
-
282
- const inlineRec = document.createElement('div');
283
- inlineRec.id = 'vtv-inline-rec';
284
- inlineRec.className = 'vtv-inline-rec';
285
- inlineRec.innerHTML = `
286
- <div class="rec-time">
287
- <span id="vtv-rec-time-start">00:00</span>
288
- <span id="vtv-rec-time-dur">Duration: 0s</span>
289
- <span id="vtv-rec-time-end">01:00</span>
290
- </div>
291
- <div class="rec-bar" id="vtv-rec-bar">
292
- <div class="rec-progress" id="vtv-rec-progress"></div>
293
- <div class="rec-marker s" id="vtv-rec-mk-start" style="display:none"></div>
294
- <div class="rec-marker e" id="vtv-rec-mk-end" style="display:none"></div>
295
- </div>
296
- <div class="rec-controls">
297
- <button class="rec-set-start" id="vtv-rec-set-start" onclick="window._vtvRecSetStart()">🔵 Điểm đầu</button>
298
- <button class="rec-set-end" id="vtv-rec-set-end" onclick="window._vtvRecSetEnd()" disabled>🔴 Điểm cuối</button>
299
- <button class="rec-go" id="vtv-rec-go" onclick="window._vtvRecGo()" disabled>⏺ Record!</button>
300
- <button class="rec-reset" onclick="window._vtvRecReset()">↺ Reset</button>
301
- </div>
302
- <div class="rec-hint" id="vtv-rec-hint">Chọn điểm bắt đầu → kết thúc (tối đa 60s)</div>
303
- <div class="rec-status" id="vtv-rec-status">Sẵn sàng</div>
304
- `;
305
- controls.parentNode.insertBefore(inlineRec, controls.nextSibling);
306
-
307
- const recPanel = document.createElement('div');
308
- recPanel.id = 'vtv-rec-panel';
309
- recPanel.className = 'vtv-rec-panel';
310
- recPanel.innerHTML = `
311
- <div class="rec-panel-title">🎬 Đã record xong!</div>
312
- <div class="rec-preview-wrap" id="vtv-rec-preview-wrap" style="display:none">
313
- <video id="vtv-rec-preview-vid" controls style="max-width:100%;max-height:180px;border-radius:6px;background:#000"></video>
314
- </div>
315
- <input type="text" class="rec-title-input" id="vtv-rec-title-input" placeholder="Tiêu đề video (để trống = AI tự tạo)">
316
- <div class="rec-ai-title-row">
317
- <button onclick="window._vtvRecGenAITitle()" id="vtv-rec-ai-btn">🤖 AI tạo tiêu đề</button>
318
- </div>
319
- <div class="rec-ratio-row">
320
- <button class="active" data-ratio="original" onclick="window._vtvRecSetRatio('original')">📺 Gốc</button>
321
- <button data-ratio="1:1" onclick="window._vtvRecSetRatio('1:1')">⬜ 1:1</button>
322
- <button data-ratio="9:16" onclick="window._vtvRecSetRatio('9:16')">📱 9:16</button>
323
- </div>
324
- <div class="rec-actions">
325
- <button class="rec-download" onclick="window._vtvRecDownload()">💾 Lưu về máy</button>
326
- <button class="rec-share" onclick="window._vtvRecShare()">🎬 Tạo Short AI</button>
327
- </div>
328
- <div class="rec-proc" id="vtv-rec-proc">
329
- <div class="vtv-spinner" style="width:20px;height:20px;margin:0 auto 6px"></div>
330
- <span id="vtv-rec-proc-text">Đang xử lý...</span>
331
- </div>
332
- `;
333
- inlineRec.parentNode.insertBefore(recPanel, inlineRec.nextSibling);
334
-
335
- document.getElementById('vtv-rec-bar').addEventListener('click', function(e) {
336
- if (!_rec.isRecording) handleRecBarClick(e);
337
- });
338
-
339
- document.getElementById('vtv-rec-mk-start').addEventListener('mousedown', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'start'; });
340
- document.getElementById('vtv-rec-mk-start').addEventListener('touchstart', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'start'; }, {passive:false});
341
- document.getElementById('vtv-rec-mk-end').addEventListener('mousedown', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'end'; });
342
- document.getElementById('vtv-rec-mk-end').addEventListener('touchstart', function(e){ e.preventDefault(); e.stopPropagation(); _rec._dragMarker = 'end'; }, {passive:false});
343
- }
344
-
345
- // Global drag
346
- document.addEventListener('mousemove', function(e) { if (_rec._dragMarker) dragMarker(e.clientX); });
347
- document.addEventListener('touchmove', function(e) { if (_rec._dragMarker) dragMarker(e.touches[0].clientX); }, {passive:false});
348
- document.addEventListener('mouseup', function(){ _rec._dragMarker = null; });
349
- document.addEventListener('touchend', function(){ _rec._dragMarker = null; });
350
-
351
- function dragMarker(clientX) {
352
- const bar = document.getElementById('vtv-rec-bar');
353
- if (!bar) return;
354
- const rect = bar.getBoundingClientRect();
355
- const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
356
- const time = pct * 60;
357
- if (_rec._dragMarker === 'start') {
358
- if (_rec.endTime !== null && time >= _rec.endTime - 1) return;
359
- _rec.startTime = time;
360
- const mk = document.getElementById('vtv-rec-mk-start');
361
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
362
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
363
- } else {
364
- if (_rec.startTime !== null && time <= _rec.startTime + 1) return;
365
- _rec.endTime = time;
366
- const mk = document.getElementById('vtv-rec-mk-end');
367
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
368
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
369
- }
370
- updateRecBar();
371
- }
372
-
373
- function handleRecBarClick(e) {
374
- const video = document.getElementById('vtv-player');
375
- if (!video || !video.src) { recStatus('⚠️ Chưa có stream!'); return; }
376
- const bar = document.getElementById('vtv-rec-bar');
377
- const rect = bar.getBoundingClientRect();
378
- const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
379
- const time = pct * 60;
380
- if (_rec.startTime === null) {
381
- _rec.startTime = time;
382
- const mk = document.getElementById('vtv-rec-mk-start');
383
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
384
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(time);
385
- document.getElementById('vtv-rec-set-end').disabled = false;
386
- recStatus('Điểm đầu: ' + fmtSec(time) + '. Chọn điểm cuối.', '');
387
- } else if (_rec.endTime === null) {
388
- if (time <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
389
- const dur = time - _rec.startTime;
390
- if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
391
- _rec.endTime = time;
392
- const mk = document.getElementById('vtv-rec-mk-end');
393
- mk.style.display = 'block'; mk.style.left = (pct * 100) + '%';
394
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(time);
395
- updateRecBar();
396
- recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
397
- document.getElementById('vtv-rec-go').disabled = false;
398
- }
399
- }
400
-
401
- function updateRecBar() {
402
- const s = _rec.startTime || 0, e = _rec.endTime || 60;
403
- const dur = e - s;
404
- document.getElementById('vtv-rec-progress').style.left = (s/60*100) + '%';
405
- document.getElementById('vtv-rec-progress').style.width = (dur/60*100) + '%';
406
- document.getElementById('vtv-rec-time-dur').textContent = 'Duration: ' + Math.round(dur) + 's';
407
- }
408
-
409
- // ===== RECORDER CONTROLS =====
410
- window._vtvRecSetStart = function() {
411
- const video = document.getElementById('vtv-player');
412
- if (!video || !video.src) { recStatus('⚠️ Chưa có stream!', 'err'); return; }
413
- _rec.startTime = video.currentTime || 0;
414
- const pct = _rec.startTime / 60 * 100;
415
- const mk = document.getElementById('vtv-rec-mk-start');
416
- mk.style.display = 'block'; mk.style.left = pct + '%';
417
- document.getElementById('vtv-rec-time-start').textContent = fmtSec(_rec.startTime);
418
- document.getElementById('vtv-rec-set-end').disabled = false;
419
- recStatus('Điểm đầu: ' + fmtSec(_rec.startTime), '');
420
- };
421
-
422
- window._vtvRecSetEnd = function() {
423
- if (_rec.startTime === null) { recStatus('⚠️ Chọn điểm đầu trước!', 'err'); return; }
424
- const video = document.getElementById('vtv-player');
425
- if (!video) return;
426
- const cur = video.currentTime || 0;
427
- if (cur <= _rec.startTime) { recStatus('⚠️ Điểm cuối phải sau điểm đầu!', 'err'); return; }
428
- const dur = cur - _rec.startTime;
429
- if (dur > 60) { recStatus('⚠️ Tối đa 60s!', 'err'); return; }
430
- _rec.endTime = cur;
431
- const pct = cur / 60 * 100;
432
- const mk = document.getElementById('vtv-rec-mk-end');
433
- mk.style.display = 'block'; mk.style.left = pct + '%';
434
- document.getElementById('vtv-rec-time-end').textContent = fmtSec(cur);
435
- updateRecBar();
436
- recStatus('✅ ' + Math.round(dur) + 's. Nhấn Record!', 'ok');
437
- document.getElementById('vtv-rec-go').disabled = false;
438
- };
439
-
440
- window._vtvRecReset = function() {
441
- _rec.startTime = null; _rec.endTime = null; _rec.isRecording = false; _rec.blob = null;
442
- if (_rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
443
- clearTimeout(_rec._recTimer);
444
- document.getElementById('vtv-rec-mk-start').style.display = 'none';
445
- document.getElementById('vtv-rec-mk-end').style.display = 'none';
446
- document.getElementById('vtv-rec-progress').style.width = '0%';
447
- document.getElementById('vtv-rec-time-start').textContent = '00:00';
448
- document.getElementById('vtv-rec-time-end').textContent = '01:00';
449
- document.getElementById('vtv-rec-time-dur').textContent = 'Duration: 0s';
450
- document.getElementById('vtv-rec-set-start').disabled = false;
451
- document.getElementById('vtv-rec-set-end').disabled = true;
452
- document.getElementById('vtv-rec-go').disabled = true;
453
- document.getElementById('vtv-rec-panel').classList.remove('show');
454
- document.getElementById('vtv-rec-title-input').value = '';
455
- recStatus('Đã reset.', '');
456
- const btn = document.getElementById('vtv-rec-btn');
457
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
458
- };
459
-
460
- window._vtvRecSetRatio = function(ratio) {
461
- _rec.ratio = ratio;
462
- document.querySelectorAll('#vtv-rec-panel .rec-ratio-row button').forEach(b => b.classList.toggle('active', b.dataset.ratio === ratio));
463
- };
464
-
465
- // ===== RECORD — captureStream with audio (unmute during record) =====
466
- window._vtvRecGo = function() {
467
- if (_rec.startTime === null || _rec.endTime === null) return;
468
- if (_rec.isRecording) {
469
- if (_rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
470
- _rec.isRecording = false;
471
- clearTimeout(_rec._recTimer);
472
- const btn = document.getElementById('vtv-rec-btn');
473
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
474
- return;
475
- }
476
-
477
- const video = document.getElementById('vtv-player');
478
- if (!video) { recStatus('⚠️ Không tìm thấy video!', 'err'); return; }
479
-
480
- _rec.chunks = [];
481
- _rec.isRecording = true;
482
-
483
- const wasMuted = video.muted;
484
- video.muted = false;
485
- const wasVolume = video.volume;
486
- video.volume = 0.3;
487
-
488
- let stream;
489
- try {
490
- stream = video.captureStream ? video.captureStream() : video.mozCaptureStream();
491
- if (!stream) throw new Error('captureStream not supported');
492
- } catch(e) {
493
- video.muted = wasMuted; video.volume = wasVolume;
494
- recStatus('❌ ' + e.message, 'err');
495
- _rec.isRecording = false;
496
- return;
497
- }
498
-
499
- const audioTracks = stream.getAudioTracks();
500
- console.log('[VTV Rec] Audio tracks:', audioTracks.length, 'Video tracks:', stream.getVideoTracks().length);
501
-
502
- let mimeType = 'video/webm;codecs=vp9,opus';
503
- if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm;codecs=vp8,opus';
504
- if (!MediaRecorder.isTypeSupported(mimeType)) mimeType = 'video/webm';
505
-
506
- try { _rec.recorder = new MediaRecorder(stream, {mimeType}); }
507
- catch(e) {
508
- video.muted = wasMuted; video.volume = wasVolume;
509
- recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err');
510
- _rec.isRecording = false;
511
- return;
512
- }
513
-
514
- _rec.recorder.ondataavailable = e => { if(e.data && e.data.size > 0) _rec.chunks.push(e.data); };
515
- _rec.recorder.onstop = function() {
516
- _rec.isRecording = false;
517
- video.muted = wasMuted;
518
- video.volume = wasVolume;
519
- _rec.blob = new Blob(_rec.chunks, {type: 'video/webm'});
520
- const btn = document.getElementById('vtv-rec-btn');
521
- if (btn) { btn.classList.remove('recording'); btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record'; }
522
- document.getElementById('vtv-rec-panel').classList.add('show');
523
- const pv = document.getElementById('vtv-rec-preview-vid');
524
- pv.src = URL.createObjectURL(_rec.blob);
525
- document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
526
- const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
527
- recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
528
- };
529
- _rec.recorder.onerror = e => {
530
- _rec.isRecording = false;
531
- video.muted = wasMuted; video.volume = wasVolume;
532
- recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
533
- };
534
-
535
- _rec.recorder.start(500);
536
- video.currentTime = _rec.startTime;
537
-
538
- const btn = document.getElementById('vtv-rec-btn');
539
- 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'; }
540
- recStatus('🔴 ĐANG RECORD...' + (audioTracks.length > 0 ? ' (có audio)' : ''), 'recording');
541
-
542
- const dur = (_rec.endTime - _rec.startTime) * 1000;
543
- _rec._recTimer = setTimeout(() => {
544
- if (_rec.isRecording && _rec.recorder && _rec.recorder.state !== 'inactive') _rec.recorder.stop();
545
- }, dur + 500);
546
- };
547
-
548
- // ===== AI TITLE =====
549
- window._vtvRecGenAITitle = async function() {
550
- if (!_rec.blob) { recStatus('⚠️ Chưa record!', 'err'); return; }
551
- const btn = document.getElementById('vtv-rec-ai-btn');
552
- const input = document.getElementById('vtv-rec-title-input');
553
- if (!btn || !input) return;
554
-
555
- btn.disabled = true;
556
- btn.textContent = '⏳ Đang tạo...';
557
-
558
- try {
559
- const ch = CHANNELS.find(c => c.id === _currentCh);
560
- const channelName = ch ? ch.name : (_currentCh || 'VTV');
561
- const title = await generateAITitle(_rec.blob, channelName);
562
- input.value = title;
563
- recStatus('✅ Đã tạo tiêu đề AI: ' + title, 'ok');
564
- } catch(e) {
565
- recStatus('⚠️ Lỗi AI: ' + e.message, 'err');
566
- } finally {
567
- btn.disabled = false;
568
- btn.textContent = '🤖 AI tạo tiêu đề';
569
- }
570
- };
571
-
572
- // ===== DOWNLOAD =====
573
- window._vtvRecDownload = function() {
574
- if (!_rec.blob) return;
575
- const title = document.getElementById('vtv-rec-title-input')?.value || '';
576
- if (_rec.ratio === 'original') {
577
- downloadBlob(_rec.blob, `vtv-${_currentCh}-${Date.now()}.webm`);
578
- } else {
579
- processRatio(_rec.blob, _rec.ratio, 'download', title);
580
- }
581
- };
582
-
583
- // ===== SHARE TO SHORT AI =====
584
- window._vtvRecShare = function() {
585
- if (!_rec.blob) return;
586
- const title = document.getElementById('vtv-rec-title-input')?.value || '';
587
- if (_rec.ratio !== 'original') {
588
- processRatio(_rec.blob, _rec.ratio, 'upload', title);
589
- } else {
590
- uploadToWall(_rec.blob, title);
591
- }
592
- };
593
-
594
- // ===== RATIO CROP — v7: reliable render with timeout fallback =====
595
- function processRatio(blob, ratio, action, title) {
596
- showProc(`🔄 Đang xử lý ${ratio}...`);
597
-
598
- const blobUrl = URL.createObjectURL(blob);
599
- const video = document.createElement('video');
600
- video.src = blobUrl;
601
- video.volume = 0;
602
- video.preload = 'auto';
603
- video.playsInline = true;
604
-
605
- function onReady() {
606
- video.removeEventListener('loadedmetadata', onReady);
607
- video.removeEventListener('canplay', onReady);
608
-
609
- const vw = video.videoWidth, vh = video.videoHeight;
610
- if (!vw || !vh) {
611
- hideProc(); recStatus('❌ Video không có kích thước hợp lệ.', 'err');
612
- URL.revokeObjectURL(blobUrl); return;
613
- }
614
-
615
- let cw, ch, sx, sy;
616
- if (ratio === '1:1') {
617
- const size = Math.min(vw, vh); cw = ch = size;
618
- sx = Math.floor((vw - size) / 2); sy = Math.floor((vh - size) / 2);
619
- } else if (ratio === '9:16') {
620
- ch = vh; cw = Math.min(vw, Math.round(vh * 9 / 16));
621
- sx = Math.floor((vw - cw) / 2); sy = 0;
622
- } else { cw = vw; ch = vh; sx = sy = 0; }
623
-
624
- showProc(`🔄 Render ${cw}x${ch}...`);
625
-
626
- const canvas = document.createElement('canvas');
627
- canvas.width = cw; canvas.height = ch;
628
- const ctx = canvas.getContext('2d');
629
- const canvasStream = canvas.captureStream(30);
630
-
631
- let recordStream = canvasStream;
632
- const audioVideo = document.createElement('video');
633
- audioVideo.src = blobUrl;
634
- audioVideo.muted = true;
635
- audioVideo.volume = 0;
636
- audioVideo.preload = 'auto';
637
- audioVideo.playsInline = true;
638
-
639
- audioVideo.onloadedmetadata = function() {
640
- try {
641
- const aStr = audioVideo.captureStream ? audioVideo.captureStream() : null;
642
- if (aStr) {
643
- const tracks = aStr.getAudioTracks();
644
- if (tracks.length > 0) {
645
- recordStream = new MediaStream([...canvasStream.getVideoTracks(), ...tracks]);
646
- console.log('[VTV Crop] Audio merged:', tracks.length, 'tracks');
647
- }
648
- }
649
- } catch(e) { console.warn('[VTV Crop] Audio merge failed:', e); }
650
- };
651
-
652
- const mime = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus')
653
- ? 'video/webm;codecs=vp9,opus'
654
- : MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus')
655
- ? 'video/webm;codecs=vp8,opus' : 'video/webm';
656
-
657
- let rec;
658
- try { rec = new MediaRecorder(recordStream, {mimeType: mime}); }
659
- catch(e) { hideProc(); recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err'); URL.revokeObjectURL(blobUrl); return; }
660
-
661
- const chunks = [];
662
- rec.ondataavailable = e => { if(e.data && e.data.size > 0) chunks.push(e.data); };
663
- rec.onstop = () => {
664
- hideProc();
665
- URL.revokeObjectURL(blobUrl);
666
- if (chunks.length === 0) {
667
- recStatus('❌ Render thất bại (không có data). Thử lại hoặc chọn "Gốc".', 'err');
668
- return;
669
- }
670
- const out = new Blob(chunks, {type: 'video/webm'});
671
- if (action === 'download') {
672
- downloadBlob(out, `vtv-${_currentCh}-${ratio.replace(':','x')}-${Date.now()}.webm`);
673
- } else {
674
- uploadToWall(out, title);
675
- }
676
- };
677
- rec.onerror = e => { hideProc(); recStatus('❌ Lỗi render: ' + (e.error?.message || '?'), 'err'); URL.revokeObjectURL(blobUrl); };
678
-
679
- rec.start(200);
680
-
681
- audioVideo.play().catch(() => {});
682
-
683
- video.play().then(() => {
684
- let finished = false;
685
- function finish() {
686
- if (finished) return;
687
- finished = true;
688
- console.log('[VTV Crop] finish() called');
689
- try { rec.requestData(); } catch(e) {}
690
- setTimeout(() => {
691
- if (rec.state !== 'inactive') rec.stop();
692
- try { audioVideo.pause(); } catch(e) {}
693
- }, 300);
694
- }
695
- video.onended = finish;
696
- audioVideo.onended = finish;
697
-
698
- const expectedDur = (video.duration || 0) * 1000;
699
- if (expectedDur > 0 && isFinite(expectedDur)) {
700
- setTimeout(() => {
701
- if (!finished) {
702
- console.warn('[VTV Crop] Timeout fallback after', expectedDur + 3000, 'ms');
703
- finish();
704
- }
705
- }, expectedDur + 3000);
706
- } else {
707
- console.warn('[VTV Crop] Duration unknown, using 60s timeout');
708
- setTimeout(() => {
709
- if (!finished) {
710
- console.warn('[VTV Crop] 60s timeout fallback');
711
- finish();
712
- }
713
- }, 60000);
714
- }
715
-
716
- function draw() {
717
- if (video.paused || video.ended) return;
718
- ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch);
719
- requestAnimationFrame(draw);
720
- }
721
- draw();
722
- }).catch(e => {
723
- hideProc(); recStatus('❌ Lỗi phát video: ' + e.message, 'err');
724
- URL.revokeObjectURL(blobUrl);
725
- });
726
- }
727
-
728
- video.onerror = function() {
729
- hideProc(); recStatus('❌ Không đọc được video.', 'err');
730
- URL.revokeObjectURL(blobUrl);
731
- };
732
-
733
- video.addEventListener('loadedmetadata', onReady);
734
- video.addEventListener('canplay', onReady);
735
- }
736
-
737
- // ===== TOGGLE REC PANEL =====
738
- function toggleRecPanel() {
739
- const panel = document.getElementById('vtv-inline-rec');
740
- if (!panel) { buildInlineRecorder(); setTimeout(toggleRecPanel, 200); return; }
741
- _rec.active = !_rec.active;
742
- panel.classList.toggle('show', _rec.active);
743
- const btn = document.getElementById('vtv-rec-btn');
744
- if (btn) {
745
- if (_rec.active) {
746
- btn.style.background = '#3a1a1a';
747
- btn.innerHTML = '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="2"/></svg> Đóng';
748
- } else {
749
- btn.style.background = '';
750
- btn.innerHTML = '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg> Record';
751
- document.getElementById('vtv-rec-panel').classList.remove('show');
752
- }
753
- }
754
- }
755
-
756
- // ===== PIN BUTTON =====
757
- function setupPinButton(){
758
- const block = document.getElementById('vtv-block');
759
- if(!block) return;
760
- const header = block.querySelector('.vtv-head');
761
- if(!header || document.getElementById('vtv-pin-btn')) return;
762
- const btn = document.createElement('button');
763
- btn.id = 'vtv-pin-btn'; btn.className = 'vtv-pin-btn';
764
- btn.innerHTML = '📌 Ghim';
765
- btn.title = 'Ghim VTV cố định trên đầu trang';
766
- btn.onclick = function(e){
767
- e.stopPropagation(); _vtvPinned = !_vtvPinned;
768
- if(_vtvPinned){ block.classList.add('vtv-sticky'); btn.classList.add('pinned'); btn.innerHTML = '📌 Đã ghim'; }
769
- else { block.classList.remove('vtv-sticky'); btn.classList.remove('pinned'); btn.innerHTML = '📌 Ghim'; }
770
- };
771
- header.style.position = 'relative';
772
- header.appendChild(btn);
773
- }
774
-
775
- // ===== MINI PLAYER =====
776
- function createMiniPlayer(){
777
- if(document.getElementById('vtv-mini')) return;
778
- const mini = document.createElement('div');
779
- mini.className = 'vtv-mini'; mini.id = 'vtv-mini';
780
- mini.innerHTML = `<div class="vtv-mini-frame"><video id="vtv-mini-vid" playsinline muted preload="auto"></video></div>
781
- <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>
782
- <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>
783
- <div class="vtv-mini-peek" id="vtv-mini-peek">📺 Xem</div>`;
784
- document.body.appendChild(mini);
785
- document.getElementById('vtv-mini-pip').onclick = async function(){
786
- const v = document.getElementById('vtv-mini-vid');
787
- try{ if(document.pictureInPictureElement === v) await document.exitPictureInPicture(); else await v.requestPictureInPicture(); }catch(e){}
788
- };
789
- document.getElementById('vtv-mini-expand').onclick = function(){ closeMiniPlayer(); if(typeof switchCat==='function') switchCat('home'); };
790
- document.getElementById('vtv-mini-hide').onclick = ()=>{ mini.classList.add('hidden'); };
791
- document.getElementById('vtv-mini-close').onclick = closeMiniPlayer;
792
- document.getElementById('vtv-mini-peek').onclick = ()=>{ mini.classList.remove('hidden'); };
793
- }
794
- 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; }
795
- function activateMiniPlayer(){
796
- if(!_currentCh) return; createMiniPlayer(); _miniActive=true;
797
- const mini=document.getElementById('vtv-mini'); const mv=document.getElementById('vtv-mini-vid'); const v=document.getElementById('vtv-player');
798
- if(_hls){const h=new Hls({enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,maxBufferLength:4,fragLoadingTimeOut:8000,manifestLoadingTimeOut:8000}); h.loadSource(STREAMS[_currentCh][0]); h.attachMedia(mv); mv.play().catch(()=>{});}
799
- else if(v&&v.src){mv.src=v.src; mv.play().catch(()=>{});}
800
- const ch=CHANNELS.find(c=>c.id===_currentCh); if(ch) document.getElementById('vtv-mini-ch').textContent=ch.name;
801
- 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(()=>{});
802
- mini.classList.add('show'); mini.classList.remove('hidden');
803
- }
804
-
805
- // ===== VTVGO POPUP PLAYER =====
806
- function openVtvPopup(chId) {
807
- const popupUrl = VTV_POPUP_URLS[chId];
808
- if (!popupUrl) return;
809
- const ch = CHANNELS.find(c => c.id === chId);
810
- const channelName = ch ? ch.name : chId;
811
- openVtvPopupWithUrl(popupUrl, '📺 ' + channelName);
812
- }
813
-
814
- function closeVtvPopup() {
815
- const popup = document.getElementById('vtv-popup');
816
- if (popup) {
817
- popup.classList.remove('show');
818
- const iframe = popup.querySelector('iframe');
819
- if (iframe) iframe.src = '';
820
- }
821
- _vtvPopupActive = false;
822
- }
823
-
824
- // ===== WORLD CUP 2026 REPLAY SECTION =====
825
- const WC2026_LINKS = [
826
- {id:'wc-vtv3', name:'⚽ WC2026 - VTV3', popupUrl:'https://package.vtvgo.vn/channel/vtv3-1,3.html', badge:'VTV3'},
827
- {id:'wc-vtv6', name:'⚽ WC2026 - VTV6', popupUrl:'https://package.vtvgo.vn/channel/vtv6-1,13.html', badge:'VTV6'},
828
- ];
829
-
830
- // TS token links for World Cup replay (user can add more)
831
- const WC2026_TS_LINKS = [
832
- {id:'wc-ts-1', name:'⚽ WC2026 - Xem lại (TS Token)', tsUrl:'https://package.vtvgo.vn/ts/62FPb0qUU6-20260626-328751', badge:'Replay'},
833
- ];
834
-
835
- function buildWc2026Section(){
836
- const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-wc2026';
837
- w.style.borderColor='#e6b800';
838
- let tabs='';
839
- WC2026_LINKS.forEach(ch=>{
840
- tabs+='<div style="display:flex;align-items:center"><button class="vtv-tab" id="vtvt-wc-'+ch.id+'" onclick="window._vtvOpenUrlPopup(\''+ch.popupUrl+'\')" style="background:#1a2a1a;border-color:#2d4a2d;color:#8ad88a">'+ch.name+'</button></div>';
841
- });
842
- WC2026_TS_LINKS.forEach(ch=>{
843
- tabs+='<div style="display:flex;align-items:center"><button class="vtv-tab" id="vtvt-wc-ts-'+ch.id+'" onclick="window._vtvOpenTsPopup(\''+ch.tsUrl+'\')" style="background:#2a1a1a;border-color:#4a2d2d;color:#d88a8a">'+ch.name+'</button></div>';
844
- });
845
- w.innerHTML='<div class="vtv-head" style="background:linear-gradient(90deg,#4a3a00,#1a1a1a)"><span class="vtv-title" style="color:#ffcc00">⚽ World Cup 2026</span><span class="vtv-badge" style="color:#ffcc00">FIFA</span></div>'+
846
- '<div class="vtv-tabs" style="flex-wrap:wrap">'+tabs+'</div>'+
847
- '<div class="vtv-wc-info" style="padding:6px 10px;background:#0a1628;border-top:1px solid #2a2a1a;font-size:9px;color:#888;text-align:center">Nhấn nút để mở player VTVGO xem trực tiếp / xem lại World Cup 2026</div>';
848
- return w;
849
- }
850
-
851
- // Open any URL in popup (for WC2026 direct links)
852
- window._vtvOpenUrlPopup=function(popupUrl) {
853
- if (!popupUrl) return;
854
- openVtvPopupWithUrl(popupUrl, '', true);
855
- };
856
-
857
- window._vtvOpenPopup=function(chId) {
858
- openVtvPopup(chId);
859
- };
860
-
861
- function openVtvPopupWithUrl(popupUrl, title, hideHeader) {
862
- let popup = document.getElementById('vtv-popup');
863
- if (!popup) {
864
- popup = document.createElement('div');
865
- popup.id = 'vtv-popup';
866
- popup.className = 'vtv-popup';
867
- popup.innerHTML = `
868
- <div class="vtv-popup-header" id="vtv-popup-hdr">
869
- <span class="vtv-popup-title" id="vtv-popup-title">📺 VTV Player</span>
870
- <button class="vtv-popup-close" id="vtv-popup-close">✕</button>
871
- </div>
872
- <div class="vtv-popup-frame" id="vtv-popup-frame"></div>
873
- `;
874
- document.body.appendChild(popup);
875
- document.getElementById('vtv-popup-close').onclick = closeVtvPopup;
876
- }
877
-
878
- _vtvPopupActive = true;
879
- const hdr = document.getElementById('vtv-popup-hdr');
880
- if (hdr) hdr.style.display = hideHeader ? 'none' : 'flex';
881
- document.getElementById('vtv-popup-title').textContent = title || '📺 VTV Player';
882
- const frame = document.getElementById('vtv-popup-frame');
883
-
884
- // Show loader
885
- frame.innerHTML = '<div class="vtv-popup-loader" id="vtv-popup-loader"><div class="vtv-spinner"></div>Đang tải player...</div>';
886
-
887
- // Use iframe but handle blocked embeds gracefully
888
- const iframe = document.createElement('iframe');
889
- iframe.id = 'vtv-popup-iframe';
890
- iframe.setAttribute('allowfullscreen', 'true');
891
- iframe.setAttribute('webkitallowfullscreen', 'true');
892
- iframe.setAttribute('mozallowfullscreen', 'true');
893
- iframe.setAttribute('allow', 'autoplay; encrypted-media; fullscreen');
894
- iframe.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none;display:none';
895
-
896
- // Timeout to detect blocked iframe (onload fires even for blocked, so use timeout)
897
- let loaded = false;
898
- iframe.onload = () => { loaded = true; const l = document.getElementById('vtv-popup-loader'); if(l) l.style.display='none'; iframe.style.display = 'block'; };
899
- frame.appendChild(iframe);
900
- popup.classList.add('show');
901
-
902
- // Set src after append to ensure events fire
903
- setTimeout(() => { iframe.src = popupUrl; }, 50);
904
-
905
- // If still not loaded after 4s, fallback to new window
906
- setTimeout(() => {
907
- if (!loaded && popup.classList.contains('show')) {
908
- window.open(popupUrl, '_blank');
909
- closeVtvPopup();
910
- }
911
- }, 4000);
912
- }
913
-
914
- // Open TS token URL in popup
915
- window._vtvOpenTsPopup=function(tsUrl) {
916
- openVtvPopupWithUrl(tsUrl, '', true);
917
- };
918
-
919
- window._vtvClosePopup=function() { closeVtvPopup(); };
920
-
921
- // ===== MAIN PLAYER =====
922
- async function loadAllStreams(){
923
- if(_loading) return; _loading=true;
924
- const el=document.getElementById('vtv-load'); if(el) el.innerHTML='<div class="vtv-spinner"></div>Đang tải kênh...';
925
- try{const r=await fetch('/api/vtv/streams'); 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){}
926
- CHANNELS.forEach(ch=>{
927
- const t=document.querySelector('#vtvt-'+ch.id);
928
- if(t){
929
- if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){
930
- t.classList.remove('off');
931
- }else{
932
- t.style.opacity='0.35';
933
- }
934
- }
935
- });
936
- _loading=false; _streamsLoaded=true;
937
- }
938
-
939
- function buildBlock(){
940
- const w=document.createElement('div'); w.className='vtv-wrap'; w.id='vtv-block';
941
- let tabs=''; CHANNELS.forEach(ch=>{
942
- tabs+='<div style="display:flex;align-items:center"><button class="vtv-tab off" id="vtvt-'+ch.id+'" onclick="window._vtvPlay(\''+ch.id+'\')">'+ch.name+'</button><button class="vtv-popup-btn" onclick="window._vtvOpenPopup(\''+ch.id+'\')" title="Mở player VTVGO">📺</button></div>';
943
- });
944
- w.innerHTML='<div class="vtv-head"><span class="vtv-title">📺 VTV Trực Tuyến</span><span class="vtv-badge">● LIVE</span></div>'+
945
- '<div class="vtv-tabs" style="flex-wrap:wrap">'+tabs+'</div>'+
946
- '<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>'+
947
- '<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>'+
948
- '<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>';
949
- return w;
950
- }
951
-
952
- async function loadEpg(chId){
953
- const el=document.getElementById('vtv-epg-list'); if(!el) return;
954
- el.innerHTML='<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
955
- try{const r=await fetch('/api/vtv/epg/'+chId); 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>';}
956
- }
957
-
958
- 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';}};
959
- 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){}};
960
-
961
- function pinBlock(){
962
- const h=document.getElementById('view-home'); if(!h||document.getElementById('vtv-block')) return;
963
- h.insertBefore(buildBlock(),h.firstChild); _blockInserted=true;
964
- // Add World Cup 2026 section below VTV block
965
- if(!document.getElementById('vtv-wc2026')){
966
- const wcBlock = buildWc2026Section();
967
- h.insertBefore(wcBlock, document.getElementById('vtv-block').nextSibling);
968
- }
969
- setTimeout(()=>{setupPinButton();buildInlineRecorder();},500);
970
- if(!_streamsLoaded){loadAllStreams().then(()=>{setTimeout(()=>window._vtvPlay(DEFAULT_CHANNEL),300);});}
971
- }
972
-
973
- window._vtvRetry=function(){if(_currentCh)window._vtvPlay(_currentCh);};
974
-
975
- window._vtvPlay=function(chId){
976
- const ch=CHANNELS.find(c=>c.id===chId); if(!ch) return; _currentCh=chId;
977
- document.querySelectorAll('.vtv-tab').forEach(t=>t.classList.remove('on'));
978
- const tab=document.getElementById('vtvt-'+chId); if(tab) tab.classList.add('on');
979
- 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');
980
- video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
981
- loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
982
- if(_hls){_hls.destroy();_hls=null;}
983
- const urls=STREAMS[chId]||[];
984
- 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;}
985
- _tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
986
- loadEpg(chId);
987
- 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(()=>{});}
988
- };
989
-
990
- function _tryPlay(video,urls,idx,name,loadEl,errEl,errMsg){
991
- if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
992
- const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
993
- if(typeof Hls!=='undefined'&&Hls.isSupported()){
994
- const hls=new Hls({enableWorker:true,lowLatencyMode:false,startLevel:0,capLevelToPlayerSize:true,maxBufferLength:6,maxMaxBufferLength:10,liveSyncDurationCount:2,liveMaxLatencyDurationCount:3,fragLoadingTimeOut:10000,manifestLoadingTimeOut:10000}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
995
- hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
996
- 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);}}});
997
- }else if(video.canPlayType('application/vnd.apple.mpegurl')){
998
- video.src=src;
999
- video.addEventListener('loadedmetadata',()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';},{once:true});
1000
- video.addEventListener('error',()=>{_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);},{once:true});
1001
- }else{loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent='Không hỗ trợ HLS';}
1002
- }
1003
-
1004
- // ===== NAV HOOKS =====
1005
- const _origShowView=window.showView;
1006
- 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);};
1007
- 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);};}
1008
- 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);};}
1009
-
1010
- const _origLoadHome=window.loadHome;
1011
- if(_origLoadHome&&!_origLoadHome.__vtvWrapped){
1012
- window.loadHome=async function(){const old=document.getElementById('vtv-block');if(old)old.remove();const wc=document.getElementById('vtv-wc2026');if(wc)wc.remove();_blockInserted=false;const r=await _origLoadHome.apply(this,arguments);try{pinBlock();}catch(e){}return r;};
1013
- window.loadHome.__vtvWrapped=true;
1014
- }
1015
- })();
 
1
  // === VNEWS — VTV LIVE + Inline Recorder v9 ===
2
  // Features: PiP, mini-player, INLINE RECORDER, VTVGO POPUP players for each channel
3
  // FIX v10: Correct VTVGO popup URLs for all channels + World Cup 2026 TS stream links
4
+ // FIX v11: Allow clicking off tabs (always clickable), better stream status handling
5
 
6
  (function(){
7
  if(window._ytLiveLoaded) return;
 
56
  .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}
57
  .vtv-tab:hover{background:#0b4a7a;color:#fff}
58
  .vtv-tab.on{background:#0066cc;border-color:#00ccff;color:#fff;font-weight:700}
59
+ .vtv-tab.off{opacity:.35}
60
  .vtv-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:180px}
61
  .vtv-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
62
  .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}
 
172
  .vtv-popup-btn{background:#1a2a3a;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:3px 8px;border-radius:6px;cursor:pointer;margin-left:4px}
173
  .vtv-popup-btn:hover{background:#0b4a7a;color:#fff}
174
  `;
175
+ document.head.appendChild(style);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vtv_api.py CHANGED
@@ -5,8 +5,9 @@ Fetches stream URLs from multiple CDN sources with aggressive extraction and fas
5
  FIX 2026-07-06: Force sv2.xemtivitop.com as PRIMARY source for VTV2, VTV3, VTV6, VTV9.
6
  VTV6 MUST use ONLY sv2.xemtivitop.com/live/hot/vtv6.php - no other sources.
7
  Added dedicated extraction for sv2 PHP endpoints with JS parse, redirect chain capture.
 
8
  """
9
- import re, time, threading, json
10
  import requests
11
  from fastapi import APIRouter, Query
12
  from fastapi.responses import JSONResponse, Response
@@ -154,7 +155,7 @@ _CACHE_TTL = 180 # 3 minutes
154
 
155
  FAST_TIMEOUT = 5
156
  NORMAL_TIMEOUT = 12
157
- LONG_TIMEOUT = 20
158
 
159
  def _cached(key):
160
  with _vtv_lock:
@@ -166,6 +167,34 @@ def _set_cache(key, data):
166
  with _vtv_lock:
167
  _vtv_cache[key] = {'t': time.time(), 'd': data}
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def extract_m3u8_aggressive(html):
170
  """Aggressively extract m3u8 URL from HTML using ALL possible patterns."""
171
  if not html:
@@ -211,6 +240,22 @@ def extract_m3u8_aggressive(html):
211
  r"data-[a-z]+\s*=\s*['\"](https?://[^'\"]+)['\"]",
212
  # Pattern 18: direct m3u8 URL with CDN domain
213
  r"(https?://[a-zA-Z0-9.-]+\.(?:cdn|cloudfront|akamaized|vtvgo|fptplay|mediacdn|ssaimh)[^\"'<>\\s]*(?:\.m3u8)[^\"'<>\\s]*)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  ]
215
 
216
  for pat in patterns:
@@ -229,6 +274,7 @@ def fetch_sv2_stream_direct(sv2_php_url, referer="https://sv2.xemtivitop.com/"):
229
  """
230
  Dedicated fetcher for sv2.xemtivitop.com PHP endpoints.
231
  Uses browser-like session with cookies, follows all redirects aggressively.
 
232
  """
233
  session = requests.Session()
234
  session.headers.update({
@@ -267,6 +313,11 @@ def fetch_sv2_stream_direct(sv2_php_url, referer="https://sv2.xemtivitop.com/"):
267
  if not html or len(html) < 50:
268
  return None, "empty_response"
269
 
 
 
 
 
 
270
  # Try aggressive extraction on PHP response
271
  m3u8 = extract_m3u8_aggressive(html)
272
  if m3u8:
@@ -287,17 +338,62 @@ def fetch_sv2_stream_direct(sv2_php_url, referer="https://sv2.xemtivitop.com/"):
287
  try:
288
  r2 = session.get(src, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
289
  if r2.status_code == 200:
 
 
 
 
290
  m3u8 = extract_m3u8_aggressive(r2.text)
291
  if m3u8:
292
  return m3u8, "iframe_extracted"
293
  except:
294
  continue
295
 
296
- # Step 3: Try fetch with different referer (some PHP pages check for specific referer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  alt_referers = [
298
  "https://sv2.xemtivitop.com/",
299
  "https://www.xemtivitop.com/",
300
  "https://xemtivitop.com/",
 
301
  ]
302
  for alt_ref in alt_referers:
303
  try:
@@ -305,6 +401,9 @@ def fetch_sv2_stream_direct(sv2_php_url, referer="https://sv2.xemtivitop.com/"):
305
  alt_headers["Referer"] = alt_ref
306
  r3 = requests.get(sv2_php_url, headers=alt_headers, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
307
  if r3.status_code == 200:
 
 
 
308
  m3u8 = extract_m3u8_aggressive(r3.text)
309
  if m3u8:
310
  return m3u8, f"alt_referer_{alt_ref}"
@@ -352,6 +451,10 @@ def fetch_url_with_follow(php_url, referer, timeout=NORMAL_TIMEOUT):
352
  from urllib.parse import urljoin
353
  loc = urljoin(php_url, loc)
354
  return loc, "redirect"
 
 
 
 
355
  m3u8 = extract_m3u8_aggressive(r.text)
356
  if m3u8:
357
  return m3u8, "extracted"
@@ -372,6 +475,9 @@ def fetch_url_with_follow(php_url, referer, timeout=NORMAL_TIMEOUT):
372
  headers2 = {**UA, "Referer": referer}
373
  r2 = requests.get(src, headers=headers2, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
374
  if r2.status_code == 200:
 
 
 
375
  m3u8 = extract_m3u8_aggressive(r2.text)
376
  if m3u8:
377
  return m3u8, "iframe_follow"
@@ -479,11 +585,15 @@ def fetch_vtv_stream(channel_id):
479
  headers = {**UA, "Referer": "https://www.xemtivitop.com/"}
480
  r = requests.get(page_url, headers=headers, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
481
  if r.status_code == 200:
482
- url = extract_m3u8_aggressive(r.text)
483
- if url:
484
- result = verify_hls_url(url, "https://www.xemtivitop.com/", timeout=NORMAL_TIMEOUT) if '.m3u8' in url else url
485
- if not result and url:
486
- result = url
 
 
 
 
487
  except:
488
  pass
489
 
 
5
  FIX 2026-07-06: Force sv2.xemtivitop.com as PRIMARY source for VTV2, VTV3, VTV6, VTV9.
6
  VTV6 MUST use ONLY sv2.xemtivitop.com/live/hot/vtv6.php - no other sources.
7
  Added dedicated extraction for sv2 PHP endpoints with JS parse, redirect chain capture.
8
+ FIX v12: Added extract_base64_m3u8(), POST fallback, JS unescape patterns, query param probing, LONG_TIMEOUT=25
9
  """
10
+ import re, time, threading, json, base64
11
  import requests
12
  from fastapi import APIRouter, Query
13
  from fastapi.responses import JSONResponse, Response
 
155
 
156
  FAST_TIMEOUT = 5
157
  NORMAL_TIMEOUT = 12
158
+ LONG_TIMEOUT = 25
159
 
160
  def _cached(key):
161
  with _vtv_lock:
 
167
  with _vtv_lock:
168
  _vtv_cache[key] = {'t': time.time(), 'd': data}
169
 
170
+ def extract_base64_m3u8(html):
171
+ """Extract m3u8 URL from base64-encoded strings in JS like atob('...') or btoa('...')"""
172
+ if not html:
173
+ return None
174
+ # Pattern: atob("base64string") - decode it
175
+ for m in re.finditer(r'(?:atob|btoa)\s*\(\s*["\']([A-Za-z0-9+/=]+)["\']\s*\)', html):
176
+ try:
177
+ decoded = base64.b64decode(m.group(1)).decode('utf-8', errors='ignore')
178
+ if '.m3u8' in decoded and len(decoded) > 20:
179
+ # Extract URL from decoded
180
+ url_m = re.search(r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)', decoded)
181
+ if url_m:
182
+ return url_m.group(1)
183
+ except:
184
+ pass
185
+ # Pattern: decodeURIComponent / unescape
186
+ for m in re.finditer(r'(?:decodeURIComponent|unescape)\s*\(\s*["\']([^"\']+)["\']\s*\)', html):
187
+ try:
188
+ from urllib.parse import unquote
189
+ decoded = unquote(m.group(1))
190
+ if '.m3u8' in decoded:
191
+ url_m = re.search(r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)', decoded)
192
+ if url_m:
193
+ return url_m.group(1)
194
+ except:
195
+ pass
196
+ return None
197
+
198
  def extract_m3u8_aggressive(html):
199
  """Aggressively extract m3u8 URL from HTML using ALL possible patterns."""
200
  if not html:
 
240
  r"data-[a-z]+\s*=\s*['\"](https?://[^'\"]+)['\"]",
241
  # Pattern 18: direct m3u8 URL with CDN domain
242
  r"(https?://[a-zA-Z0-9.-]+\.(?:cdn|cloudfront|akamaized|vtvgo|fptplay|mediacdn|ssaimh)[^\"'<>\\s]*(?:\.m3u8)[^\"'<>\\s]*)",
243
+ # Pattern 19: JS obfuscated var (_0x...) concatenation - try catching hex vars
244
+ r"""['"]source['"]\s*[+:=]+\s*['"]([^'"]*)['"]""",
245
+ # Pattern 20: document.write with m3u8
246
+ r"""document\.write\s*\(\s*['"]([^'"]*\.m3u8[^'"]*)['"]""",
247
+ # Pattern 21: JSON.parse with m3u8
248
+ r"""JSON\.parse\s*\(\s*['"]([^'"]+)['"]\s*\)""",
249
+ # Pattern 22: eval with m3u8
250
+ r"""eval\s*\(\s*['"]([^'"]*\.m3u8[^'"]*)['"]""",
251
+ # Pattern 23: PHP echo/print with m3u8
252
+ r"""(?:echo|print|printf)\s*['"]([^'"]*\.m3u8[^'"]*)['"]""",
253
+ # Pattern 24: $_GET / $_POST / $_REQUEST with stream param
254
+ r"""['"]([^'"]*stream[^'"]*m3u8[^'"]*)['"]""",
255
+ # Pattern 25: location.href / self.location with m3u8
256
+ r"""(?:self\.)?location(?:\.href)?\s*=\s*['"]([^'"]*\.m3u8[^'"]*)['"]""",
257
+ # Pattern 26: embed src / object data with m3u8
258
+ r"""<(?:embed|object)\s+[^>]*?(?:src|data)\s*=\s*['"]([^'"]*\.m3u8[^'"]*)['"]""",
259
  ]
260
 
261
  for pat in patterns:
 
274
  """
275
  Dedicated fetcher for sv2.xemtivitop.com PHP endpoints.
276
  Uses browser-like session with cookies, follows all redirects aggressively.
277
+ Tries POST fallback, base64 decode, query param probing.
278
  """
279
  session = requests.Session()
280
  session.headers.update({
 
313
  if not html or len(html) < 50:
314
  return None, "empty_response"
315
 
316
+ # Try base64 extraction FIRST (sv2 PHP often uses base64 obfuscation)
317
+ b64_url = extract_base64_m3u8(html)
318
+ if b64_url:
319
+ return b64_url, "base64_decoded"
320
+
321
  # Try aggressive extraction on PHP response
322
  m3u8 = extract_m3u8_aggressive(html)
323
  if m3u8:
 
338
  try:
339
  r2 = session.get(src, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
340
  if r2.status_code == 200:
341
+ # Try base64 on iframe content too
342
+ b64_url2 = extract_base64_m3u8(r2.text)
343
+ if b64_url2:
344
+ return b64_url2, "iframe_base64"
345
  m3u8 = extract_m3u8_aggressive(r2.text)
346
  if m3u8:
347
  return m3u8, "iframe_extracted"
348
  except:
349
  continue
350
 
351
+ # Step 3: Try POST fallback with various payloads
352
+ post_payloads = [
353
+ {"play": "1", "stream": "1"},
354
+ {"play": "1"},
355
+ {"stream": "1"},
356
+ {"link": "1"},
357
+ {"go": "1"},
358
+ {"token": "1"},
359
+ {"live": "1"},
360
+ {"auth": "1"},
361
+ ]
362
+ for payload in post_payloads:
363
+ try:
364
+ r_post = session.post(sv2_php_url, data=payload, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
365
+ if r_post.status_code == 200 and len(r_post.text) > 50:
366
+ html_post = r_post.text
367
+ b64_url2 = extract_base64_m3u8(html_post)
368
+ if b64_url2:
369
+ return b64_url2, f"post_{list(payload.keys())[0]}_base64"
370
+ m3u8 = extract_m3u8_aggressive(html_post)
371
+ if m3u8:
372
+ return m3u8, f"post_{list(payload.keys())[0]}"
373
+ except:
374
+ continue
375
+
376
+ # Step 4: Try query param probing
377
+ query_params = ["?stream=1", "?play=1", "?link=1", "?id=1", "?channel=vtv6", "?go=1", "?live=1"]
378
+ for qp in query_params:
379
+ try:
380
+ r_q = session.get(sv2_php_url + qp, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
381
+ if r_q.status_code == 200 and len(r_q.text) > 50:
382
+ b64_url2 = extract_base64_m3u8(r_q.text)
383
+ if b64_url2:
384
+ return b64_url2, f"query_{qp[1:]}_base64"
385
+ m3u8 = extract_m3u8_aggressive(r_q.text)
386
+ if m3u8:
387
+ return m3u8, f"query_{qp[1:]}"
388
+ except:
389
+ continue
390
+
391
+ # Step 5: Try fetch with different referer (some PHP pages check for specific referer)
392
  alt_referers = [
393
  "https://sv2.xemtivitop.com/",
394
  "https://www.xemtivitop.com/",
395
  "https://xemtivitop.com/",
396
+ "https://xemtv.us/",
397
  ]
398
  for alt_ref in alt_referers:
399
  try:
 
401
  alt_headers["Referer"] = alt_ref
402
  r3 = requests.get(sv2_php_url, headers=alt_headers, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
403
  if r3.status_code == 200:
404
+ b64_url2 = extract_base64_m3u8(r3.text)
405
+ if b64_url2:
406
+ return b64_url2, f"alt_ref_{alt_ref.split('/')[2]}_base64"
407
  m3u8 = extract_m3u8_aggressive(r3.text)
408
  if m3u8:
409
  return m3u8, f"alt_referer_{alt_ref}"
 
451
  from urllib.parse import urljoin
452
  loc = urljoin(php_url, loc)
453
  return loc, "redirect"
454
+ # Try base64 extraction too
455
+ b64_url = extract_base64_m3u8(r.text)
456
+ if b64_url:
457
+ return b64_url, "base64_extracted"
458
  m3u8 = extract_m3u8_aggressive(r.text)
459
  if m3u8:
460
  return m3u8, "extracted"
 
475
  headers2 = {**UA, "Referer": referer}
476
  r2 = requests.get(src, headers=headers2, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
477
  if r2.status_code == 200:
478
+ b64_url2 = extract_base64_m3u8(r2.text)
479
+ if b64_url2:
480
+ return b64_url2, "iframe_base64"
481
  m3u8 = extract_m3u8_aggressive(r2.text)
482
  if m3u8:
483
  return m3u8, "iframe_follow"
 
585
  headers = {**UA, "Referer": "https://www.xemtivitop.com/"}
586
  r = requests.get(page_url, headers=headers, timeout=NORMAL_TIMEOUT, allow_redirects=True, verify=False)
587
  if r.status_code == 200:
588
+ b64_url = extract_base64_m3u8(r.text)
589
+ if b64_url:
590
+ result = b64_url
591
+ else:
592
+ url = extract_m3u8_aggressive(r.text)
593
+ if url:
594
+ result = verify_hls_url(url, "https://www.xemtivitop.com/", timeout=NORMAL_TIMEOUT) if '.m3u8' in url else url
595
+ if not result and url:
596
+ result = url
597
  except:
598
  pass
599