Spaces:
Running
Running
Fix VTV6 stream + fix click tabs vtv2/vtv3/vtv9
#34
by bep40 - opened
- static/yt_live.js +3 -843
- 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
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
| 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 =
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 483 |
-
if
|
| 484 |
-
result =
|
| 485 |
-
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|