Spaces:
Running
Running
Restore static/yt_live.js to 0cc342c
Browse files- static/yt_live.js +35 -92
static/yt_live.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
// === VNEWS — VTV LIVE + Inline Recorder
|
| 2 |
// Features: PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
-
// FIX
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
@@ -171,7 +171,7 @@
|
|
| 171 |
if (el) el.style.display = 'none';
|
| 172 |
}
|
| 173 |
|
| 174 |
-
// ===== UPLOAD TO WALL (Short AI)
|
| 175 |
function uploadToWall(blob, title, source) {
|
| 176 |
const formData = new FormData();
|
| 177 |
formData.append('video', blob, `vtv-${_currentCh}-${Date.now()}.webm`);
|
|
@@ -179,56 +179,18 @@
|
|
| 179 |
formData.append('source', source || 'vtv_inline_recorder');
|
| 180 |
showProc('⏳ Đang upload lên Short AI...');
|
| 181 |
return fetch('/api/wall', { method: 'POST', body: formData })
|
| 182 |
-
.then(r =>
|
| 183 |
-
console.log('[VTV Upload] HTTP status:', r.status, r.statusText);
|
| 184 |
-
// Read as text first, then parse JSON manually for robustness
|
| 185 |
-
return r.text().then(text => {
|
| 186 |
-
console.log('[VTV Upload] Response text:', text.substring(0, 200));
|
| 187 |
-
let data;
|
| 188 |
-
try { data = JSON.parse(text); } catch(e) {
|
| 189 |
-
console.error('[VTV Upload] JSON parse failed:', e, 'Raw:', text.substring(0, 500));
|
| 190 |
-
throw new Error('Server trả về lỗi (không phải JSON). Status: ' + r.status);
|
| 191 |
-
}
|
| 192 |
-
return data;
|
| 193 |
-
});
|
| 194 |
-
})
|
| 195 |
.then(data => {
|
| 196 |
hideProc();
|
| 197 |
-
|
| 198 |
-
if (data && data.ok === true) {
|
| 199 |
recStatus('✅ Đã tạo Short AI! Xem trong mục Tường AI.', 'ok');
|
| 200 |
-
// CRITICAL FIX: Update _wallPosts and refresh slider immediately
|
| 201 |
-
if (data.post) {
|
| 202 |
-
if (typeof _wallPosts !== 'undefined') {
|
| 203 |
-
_wallPosts.unshift(data.post);
|
| 204 |
-
console.log('[VTV Upload] Added post to _wallPosts, total:', _wallPosts.length);
|
| 205 |
-
}
|
| 206 |
-
// Refresh Short AI slider on home view
|
| 207 |
-
if (typeof refreshShortAISlider === 'function') {
|
| 208 |
-
setTimeout(() => {
|
| 209 |
-
refreshShortAISlider();
|
| 210 |
-
console.log('[VTV Upload] refreshShortAISlider called');
|
| 211 |
-
}, 300);
|
| 212 |
-
}
|
| 213 |
-
// Switch to home so user can see the new slide
|
| 214 |
-
if (typeof switchCat === 'function') {
|
| 215 |
-
setTimeout(() => {
|
| 216 |
-
switchCat('home');
|
| 217 |
-
setTimeout(() => {
|
| 218 |
-
const s = document.getElementById('short-ai-section');
|
| 219 |
-
if (s) s.scrollIntoView({behavior:'smooth', block:'start'});
|
| 220 |
-
}, 500);
|
| 221 |
-
}, 800);
|
| 222 |
-
}
|
| 223 |
-
}
|
| 224 |
return data;
|
| 225 |
} else {
|
| 226 |
-
throw new Error(data
|
| 227 |
}
|
| 228 |
})
|
| 229 |
.catch(e => {
|
| 230 |
hideProc();
|
| 231 |
-
console.error('[VTV Upload] Error:', e);
|
| 232 |
recStatus('⚠️ ' + e.message + '. Đang tải về...', 'err');
|
| 233 |
setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
|
| 234 |
throw e;
|
|
@@ -237,6 +199,7 @@
|
|
| 237 |
|
| 238 |
// ===== AI TITLE GENERATION =====
|
| 239 |
async function generateAITitle(blob, channelName) {
|
|
|
|
| 240 |
return new Promise((resolve) => {
|
| 241 |
const video = document.createElement('video');
|
| 242 |
video.src = URL.createObjectURL(blob);
|
|
@@ -244,6 +207,8 @@
|
|
| 244 |
video.onloadedmetadata = function() {
|
| 245 |
URL.revokeObjectURL(video.src);
|
| 246 |
const dur = Math.round(video.duration || 0);
|
|
|
|
|
|
|
| 247 |
const now = new Date();
|
| 248 |
const timeStr = now.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
|
| 249 |
const dateStr = now.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit'});
|
|
@@ -254,6 +219,7 @@
|
|
| 254 |
`⚡ ${channelName} Highlights - ${dateStr}`,
|
| 255 |
`📱 ${channelName} Short - ${timeStr} ${dateStr}`,
|
| 256 |
];
|
|
|
|
| 257 |
const idx = dur < 15 ? 4 : dur < 30 ? 1 : dur < 45 ? 2 : 0;
|
| 258 |
resolve(titles[idx % titles.length]);
|
| 259 |
};
|
|
@@ -481,6 +447,7 @@
|
|
| 481 |
// CRITICAL: Unmute video so captureStream includes audio tracks
|
| 482 |
const wasMuted = video.muted;
|
| 483 |
video.muted = false;
|
|
|
|
| 484 |
const wasVolume = video.volume;
|
| 485 |
video.volume = 0.3;
|
| 486 |
|
|
@@ -523,6 +490,7 @@
|
|
| 523 |
pv.src = URL.createObjectURL(_rec.blob);
|
| 524 |
document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
|
| 525 |
const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
|
|
|
|
| 526 |
recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
|
| 527 |
};
|
| 528 |
_rec.recorder.onerror = e => {
|
|
@@ -531,7 +499,7 @@
|
|
| 531 |
recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
|
| 532 |
};
|
| 533 |
|
| 534 |
-
_rec.recorder.start(500);
|
| 535 |
video.currentTime = _rec.startTime;
|
| 536 |
|
| 537 |
const btn = document.getElementById('vtv-rec-btn');
|
|
@@ -590,22 +558,22 @@
|
|
| 590 |
}
|
| 591 |
};
|
| 592 |
|
| 593 |
-
// ===== RATIO CROP —
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
function processRatio(blob, ratio, action, title) {
|
| 595 |
showProc(`🔄 Đang xử lý ${ratio}...`);
|
| 596 |
|
| 597 |
-
const blobUrl = URL.createObjectURL(blob);
|
| 598 |
const video = document.createElement('video');
|
| 599 |
-
video.src =
|
| 600 |
video.volume = 0;
|
| 601 |
video.preload = 'auto';
|
| 602 |
video.playsInline = true;
|
| 603 |
|
| 604 |
-
function cleanup() {
|
| 605 |
-
// Don't revoke blobUrl here — audioVideo may still need it
|
| 606 |
-
// It will be revoked after recording stops
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
function onReady() {
|
| 610 |
video.removeEventListener('loadedmetadata', onReady);
|
| 611 |
video.removeEventListener('canplay', onReady);
|
|
@@ -613,7 +581,7 @@
|
|
| 613 |
const vw = video.videoWidth, vh = video.videoHeight;
|
| 614 |
if (!vw || !vh) {
|
| 615 |
hideProc(); recStatus('❌ Video không có kích thước hợp lệ.', 'err');
|
| 616 |
-
URL.revokeObjectURL(
|
| 617 |
}
|
| 618 |
|
| 619 |
let cw, ch, sx, sy;
|
|
@@ -635,7 +603,7 @@
|
|
| 635 |
// Parallel video to capture audio from blob
|
| 636 |
let recordStream = canvasStream;
|
| 637 |
const audioVideo = document.createElement('video');
|
| 638 |
-
audioVideo.src =
|
| 639 |
audioVideo.muted = true;
|
| 640 |
audioVideo.volume = 0;
|
| 641 |
audioVideo.preload = 'auto';
|
|
@@ -661,15 +629,14 @@
|
|
| 661 |
|
| 662 |
let rec;
|
| 663 |
try { rec = new MediaRecorder(recordStream, {mimeType: mime}); }
|
| 664 |
-
catch(e) { hideProc(); recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err'); URL.revokeObjectURL(
|
| 665 |
|
| 666 |
const chunks = [];
|
| 667 |
rec.ondataavailable = e => { if(e.data && e.data.size > 0) chunks.push(e.data); };
|
| 668 |
rec.onstop = () => {
|
| 669 |
-
hideProc();
|
| 670 |
-
URL.revokeObjectURL(blobUrl);
|
| 671 |
if (chunks.length === 0) {
|
| 672 |
-
recStatus('❌ Render thất bại
|
| 673 |
return;
|
| 674 |
}
|
| 675 |
const out = new Blob(chunks, {type: 'video/webm'});
|
|
@@ -679,7 +646,7 @@
|
|
| 679 |
uploadToWall(out, title);
|
| 680 |
}
|
| 681 |
};
|
| 682 |
-
rec.onerror = e => { hideProc(); recStatus('❌ Lỗi render: ' + (e.error?.message || '?'), 'err'); URL.revokeObjectURL(
|
| 683 |
|
| 684 |
rec.start(200);
|
| 685 |
|
|
@@ -690,7 +657,6 @@
|
|
| 690 |
function finish() {
|
| 691 |
if (finished) return;
|
| 692 |
finished = true;
|
| 693 |
-
console.log('[VTV Crop] finish() called');
|
| 694 |
try { rec.requestData(); } catch(e) {}
|
| 695 |
setTimeout(() => {
|
| 696 |
if (rec.state !== 'inactive') rec.stop();
|
|
@@ -700,26 +666,6 @@
|
|
| 700 |
video.onended = finish;
|
| 701 |
audioVideo.onended = finish;
|
| 702 |
|
| 703 |
-
// TIMEOUT FALLBACK: if video.ended doesn't fire within expected duration + 3s
|
| 704 |
-
const expectedDur = (video.duration || 0) * 1000;
|
| 705 |
-
if (expectedDur > 0 && isFinite(expectedDur)) {
|
| 706 |
-
setTimeout(() => {
|
| 707 |
-
if (!finished) {
|
| 708 |
-
console.warn('[VTV Crop] Timeout fallback after', expectedDur + 3000, 'ms');
|
| 709 |
-
finish();
|
| 710 |
-
}
|
| 711 |
-
}, expectedDur + 3000);
|
| 712 |
-
} else {
|
| 713 |
-
// If duration is unknown (e.g. blob), use a generous timeout
|
| 714 |
-
console.warn('[VTV Crop] Duration unknown, using 60s timeout');
|
| 715 |
-
setTimeout(() => {
|
| 716 |
-
if (!finished) {
|
| 717 |
-
console.warn('[VTV Crop] 60s timeout fallback');
|
| 718 |
-
finish();
|
| 719 |
-
}
|
| 720 |
-
}, 60000);
|
| 721 |
-
}
|
| 722 |
-
|
| 723 |
function draw() {
|
| 724 |
if (video.paused || video.ended) return;
|
| 725 |
ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch);
|
|
@@ -728,13 +674,13 @@
|
|
| 728 |
draw();
|
| 729 |
}).catch(e => {
|
| 730 |
hideProc(); recStatus('❌ Lỗi phát video: ' + e.message, 'err');
|
| 731 |
-
URL.revokeObjectURL(
|
| 732 |
});
|
| 733 |
}
|
| 734 |
|
| 735 |
video.onerror = function() {
|
| 736 |
hideProc(); recStatus('❌ Không đọc được video.', 'err');
|
| 737 |
-
URL.revokeObjectURL(
|
| 738 |
};
|
| 739 |
|
| 740 |
video.addEventListener('loadedmetadata', onReady);
|
|
@@ -802,7 +748,7 @@
|
|
| 802 |
function activateMiniPlayer(){
|
| 803 |
if(!_currentCh) return; createMiniPlayer(); _miniActive=true;
|
| 804 |
const mini=document.getElementById('vtv-mini'); const mv=document.getElementById('vtv-mini-vid'); const v=document.getElementById('vtv-player');
|
| 805 |
-
if(_hls){const h=new Hls({enableWorker:true,lowLatencyMode:
|
| 806 |
else if(v&&v.src){mv.src=v.src; mv.play().catch(()=>{});}
|
| 807 |
const ch=CHANNELS.find(c=>c.id===_currentCh); if(ch) document.getElementById('vtv-mini-ch').textContent=ch.name;
|
| 808 |
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(()=>{});
|
|
@@ -813,7 +759,7 @@
|
|
| 813 |
async function loadAllStreams(){
|
| 814 |
if(_loading) return; _loading=true;
|
| 815 |
const el=document.getElementById('vtv-load'); if(el) el.innerHTML='<div class="vtv-spinner"></div>Đang tải kênh...';
|
| 816 |
-
try{const r=await fetch('/api/vtv/streams'); if(r.ok){const d=await r.json(); CHANNELS.forEach(ch=>{const i=d[ch.id];
|
| 817 |
CHANNELS.forEach(ch=>{const t=document.getElementById('vtvt-'+ch.id); if(t){if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){t.classList.remove('off'); t.textContent=ch.name;}else{t.style.opacity='0.35'; t.textContent=ch.name+' ✕';}}});
|
| 818 |
_loading=false; _streamsLoaded=true;
|
| 819 |
}
|
|
@@ -832,7 +778,7 @@
|
|
| 832 |
async function loadEpg(chId){
|
| 833 |
const el=document.getElementById('vtv-epg-list'); if(!el) return;
|
| 834 |
el.innerHTML='<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
|
| 835 |
-
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>';}
|
| 836 |
}
|
| 837 |
|
| 838 |
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';}};
|
|
@@ -855,12 +801,9 @@
|
|
| 855 |
video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
|
| 856 |
loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
|
| 857 |
if(_hls){_hls.destroy();_hls=null;}
|
| 858 |
-
// Force video element reset to prevent cached content from showing
|
| 859 |
-
video.removeAttribute('src'); video.pause(); video.load();
|
| 860 |
const urls=STREAMS[chId]||[];
|
| 861 |
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;}
|
| 862 |
-
|
| 863 |
-
setTimeout(()=>{_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);},50);
|
| 864 |
loadEpg(chId);
|
| 865 |
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(()=>{});}
|
| 866 |
};
|
|
@@ -869,7 +812,7 @@
|
|
| 869 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 870 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 871 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 872 |
-
const hls=new Hls({enableWorker:true,lowLatencyMode:
|
| 873 |
hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
|
| 874 |
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);}}});
|
| 875 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
|
@@ -890,4 +833,4 @@
|
|
| 890 |
window.loadHome=async function(){const old=document.getElementById('vtv-block');if(old)old.remove();_blockInserted=false;const r=await _origLoadHome.apply(this,arguments);try{pinBlock();}catch(e){}return r;};
|
| 891 |
window.loadHome.__vtvWrapped=true;
|
| 892 |
}
|
| 893 |
-
})();
|
|
|
|
| 1 |
+
// === VNEWS — VTV LIVE + Inline Recorder v6 ===
|
| 2 |
// Features: PiP, mini-player, INLINE RECORDER with ratio crop + AI title + Short AI upload
|
| 3 |
+
// FIX v6: audio from captureStream (unmute during record), video.ended for crop, AI title generation
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
|
|
| 171 |
if (el) el.style.display = 'none';
|
| 172 |
}
|
| 173 |
|
| 174 |
+
// ===== UPLOAD TO WALL (Short AI) =====
|
| 175 |
function uploadToWall(blob, title, source) {
|
| 176 |
const formData = new FormData();
|
| 177 |
formData.append('video', blob, `vtv-${_currentCh}-${Date.now()}.webm`);
|
|
|
|
| 179 |
formData.append('source', source || 'vtv_inline_recorder');
|
| 180 |
showProc('⏳ Đang upload lên Short AI...');
|
| 181 |
return fetch('/api/wall', { method: 'POST', body: formData })
|
| 182 |
+
.then(r => r.json())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
.then(data => {
|
| 184 |
hideProc();
|
| 185 |
+
if (data.ok) {
|
|
|
|
| 186 |
recStatus('✅ Đã tạo Short AI! Xem trong mục Tường AI.', 'ok');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
return data;
|
| 188 |
} else {
|
| 189 |
+
throw new Error(data.error || 'Upload lỗi');
|
| 190 |
}
|
| 191 |
})
|
| 192 |
.catch(e => {
|
| 193 |
hideProc();
|
|
|
|
| 194 |
recStatus('⚠️ ' + e.message + '. Đang tải về...', 'err');
|
| 195 |
setTimeout(() => downloadBlob(blob, `vtv-${_currentCh}-${Date.now()}.webm`), 800);
|
| 196 |
throw e;
|
|
|
|
| 199 |
|
| 200 |
// ===== AI TITLE GENERATION =====
|
| 201 |
async function generateAITitle(blob, channelName) {
|
| 202 |
+
// Get video info for AI context
|
| 203 |
return new Promise((resolve) => {
|
| 204 |
const video = document.createElement('video');
|
| 205 |
video.src = URL.createObjectURL(blob);
|
|
|
|
| 207 |
video.onloadedmetadata = function() {
|
| 208 |
URL.revokeObjectURL(video.src);
|
| 209 |
const dur = Math.round(video.duration || 0);
|
| 210 |
+
const w = video.videoWidth, h = video.videoHeight;
|
| 211 |
+
// Generate title based on channel + duration + timestamp
|
| 212 |
const now = new Date();
|
| 213 |
const timeStr = now.toLocaleTimeString('vi-VN',{hour:'2-digit',minute:'2-digit'});
|
| 214 |
const dateStr = now.toLocaleDateString('vi-VN',{day:'2-digit',month:'2-digit'});
|
|
|
|
| 219 |
`⚡ ${channelName} Highlights - ${dateStr}`,
|
| 220 |
`📱 ${channelName} Short - ${timeStr} ${dateStr}`,
|
| 221 |
];
|
| 222 |
+
// Pick one based on duration
|
| 223 |
const idx = dur < 15 ? 4 : dur < 30 ? 1 : dur < 45 ? 2 : 0;
|
| 224 |
resolve(titles[idx % titles.length]);
|
| 225 |
};
|
|
|
|
| 447 |
// CRITICAL: Unmute video so captureStream includes audio tracks
|
| 448 |
const wasMuted = video.muted;
|
| 449 |
video.muted = false;
|
| 450 |
+
// Also set volume low so user doesn't hear double audio
|
| 451 |
const wasVolume = video.volume;
|
| 452 |
video.volume = 0.3;
|
| 453 |
|
|
|
|
| 490 |
pv.src = URL.createObjectURL(_rec.blob);
|
| 491 |
document.getElementById('vtv-rec-preview-wrap').style.display = 'block';
|
| 492 |
const sizeMB = (_rec.blob.size/1024/1024).toFixed(1);
|
| 493 |
+
const hasAudio = _rec.blob.size > 50000; // rough heuristic
|
| 494 |
recStatus('✅ Record xong! ' + sizeMB + 'MB' + (audioTracks.length > 0 ? ' (có audio)' : ' (video only)'), 'ok');
|
| 495 |
};
|
| 496 |
_rec.recorder.onerror = e => {
|
|
|
|
| 499 |
recStatus('❌ Lỗi: ' + (e.error?.message||'?'), 'err');
|
| 500 |
};
|
| 501 |
|
| 502 |
+
_rec.recorder.start(500); // 500ms timeslice
|
| 503 |
video.currentTime = _rec.startTime;
|
| 504 |
|
| 505 |
const btn = document.getElementById('vtv-rec-btn');
|
|
|
|
| 558 |
}
|
| 559 |
};
|
| 560 |
|
| 561 |
+
// ===== RATIO CROP — v6: reliable render =====
|
| 562 |
+
// Key fixes:
|
| 563 |
+
// 1. video.volume=0 (not muted) to avoid autoplay block
|
| 564 |
+
// 2. video.ended event to stop (NOT setTimeout with duration)
|
| 565 |
+
// 3. 200ms timeslice for frequent data collection
|
| 566 |
+
// 4. rec.requestData() flush before stop
|
| 567 |
+
// 5. Parallel audioVideo element to capture audio from blob
|
| 568 |
function processRatio(blob, ratio, action, title) {
|
| 569 |
showProc(`🔄 Đang xử lý ${ratio}...`);
|
| 570 |
|
|
|
|
| 571 |
const video = document.createElement('video');
|
| 572 |
+
video.src = URL.createObjectURL(blob);
|
| 573 |
video.volume = 0;
|
| 574 |
video.preload = 'auto';
|
| 575 |
video.playsInline = true;
|
| 576 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
function onReady() {
|
| 578 |
video.removeEventListener('loadedmetadata', onReady);
|
| 579 |
video.removeEventListener('canplay', onReady);
|
|
|
|
| 581 |
const vw = video.videoWidth, vh = video.videoHeight;
|
| 582 |
if (!vw || !vh) {
|
| 583 |
hideProc(); recStatus('❌ Video không có kích thước hợp lệ.', 'err');
|
| 584 |
+
URL.revokeObjectURL(video.src); return;
|
| 585 |
}
|
| 586 |
|
| 587 |
let cw, ch, sx, sy;
|
|
|
|
| 603 |
// Parallel video to capture audio from blob
|
| 604 |
let recordStream = canvasStream;
|
| 605 |
const audioVideo = document.createElement('video');
|
| 606 |
+
audioVideo.src = video.src;
|
| 607 |
audioVideo.muted = true;
|
| 608 |
audioVideo.volume = 0;
|
| 609 |
audioVideo.preload = 'auto';
|
|
|
|
| 629 |
|
| 630 |
let rec;
|
| 631 |
try { rec = new MediaRecorder(recordStream, {mimeType: mime}); }
|
| 632 |
+
catch(e) { hideProc(); recStatus('❌ Lỗi MediaRecorder: ' + e.message, 'err'); URL.revokeObjectURL(video.src); return; }
|
| 633 |
|
| 634 |
const chunks = [];
|
| 635 |
rec.ondataavailable = e => { if(e.data && e.data.size > 0) chunks.push(e.data); };
|
| 636 |
rec.onstop = () => {
|
| 637 |
+
hideProc(); URL.revokeObjectURL(video.src);
|
|
|
|
| 638 |
if (chunks.length === 0) {
|
| 639 |
+
recStatus('❌ Render thất bại. Thử lại hoặc chọn "Gốc".', 'err');
|
| 640 |
return;
|
| 641 |
}
|
| 642 |
const out = new Blob(chunks, {type: 'video/webm'});
|
|
|
|
| 646 |
uploadToWall(out, title);
|
| 647 |
}
|
| 648 |
};
|
| 649 |
+
rec.onerror = e => { hideProc(); recStatus('❌ Lỗi render: ' + (e.error?.message || '?'), 'err'); URL.revokeObjectURL(video.src); };
|
| 650 |
|
| 651 |
rec.start(200);
|
| 652 |
|
|
|
|
| 657 |
function finish() {
|
| 658 |
if (finished) return;
|
| 659 |
finished = true;
|
|
|
|
| 660 |
try { rec.requestData(); } catch(e) {}
|
| 661 |
setTimeout(() => {
|
| 662 |
if (rec.state !== 'inactive') rec.stop();
|
|
|
|
| 666 |
video.onended = finish;
|
| 667 |
audioVideo.onended = finish;
|
| 668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
function draw() {
|
| 670 |
if (video.paused || video.ended) return;
|
| 671 |
ctx.drawImage(video, sx, sy, cw, ch, 0, 0, cw, ch);
|
|
|
|
| 674 |
draw();
|
| 675 |
}).catch(e => {
|
| 676 |
hideProc(); recStatus('❌ Lỗi phát video: ' + e.message, 'err');
|
| 677 |
+
URL.revokeObjectURL(video.src);
|
| 678 |
});
|
| 679 |
}
|
| 680 |
|
| 681 |
video.onerror = function() {
|
| 682 |
hideProc(); recStatus('❌ Không đọc được video.', 'err');
|
| 683 |
+
URL.revokeObjectURL(video.src);
|
| 684 |
};
|
| 685 |
|
| 686 |
video.addEventListener('loadedmetadata', onReady);
|
|
|
|
| 748 |
function activateMiniPlayer(){
|
| 749 |
if(!_currentCh) return; createMiniPlayer(); _miniActive=true;
|
| 750 |
const mini=document.getElementById('vtv-mini'); const mv=document.getElementById('vtv-mini-vid'); const v=document.getElementById('vtv-player');
|
| 751 |
+
if(_hls){const h=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:10}); h.loadSource(STREAMS[_currentCh][0]); h.attachMedia(mv); mv.play().catch(()=>{});}
|
| 752 |
else if(v&&v.src){mv.src=v.src; mv.play().catch(()=>{});}
|
| 753 |
const ch=CHANNELS.find(c=>c.id===_currentCh); if(ch) document.getElementById('vtv-mini-ch').textContent=ch.name;
|
| 754 |
fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[]; const n=p.find(x=>x.now); document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});
|
|
|
|
| 759 |
async function loadAllStreams(){
|
| 760 |
if(_loading) return; _loading=true;
|
| 761 |
const el=document.getElementById('vtv-load'); if(el) el.innerHTML='<div class="vtv-spinner"></div>Đang tải kênh...';
|
| 762 |
+
try{const r=await fetch('/api/vtv/streams',{signal:AbortSignal.timeout(10000)}); if(r.ok){const d=await r.json(); CHANNELS.forEach(ch=>{const i=d[ch.id]; if(i&&i.stream_url){let u=i.stream_url; if(NEEDS_PROXY.test(u)) u='/api/proxy/m3u8/vtv?url='+encodeURIComponent(u); STREAMS[ch.id]=[u];} else STREAMS[ch.id]=[];});}}catch(e){}
|
| 763 |
CHANNELS.forEach(ch=>{const t=document.getElementById('vtvt-'+ch.id); if(t){if(STREAMS[ch.id]&&STREAMS[ch.id].length>0){t.classList.remove('off'); t.textContent=ch.name;}else{t.style.opacity='0.35'; t.textContent=ch.name+' ✕';}}});
|
| 764 |
_loading=false; _streamsLoaded=true;
|
| 765 |
}
|
|
|
|
| 778 |
async function loadEpg(chId){
|
| 779 |
const el=document.getElementById('vtv-epg-list'); if(!el) return;
|
| 780 |
el.innerHTML='<div class="vtv-epg-loading"><div class="vtv-epg-sp"></div>Đang tải lịch...</div>';
|
| 781 |
+
try{const r=await fetch('/api/vtv/epg/'+chId,{signal:AbortSignal.timeout(10000)}); if(!r.ok) throw new Error(); const d=await r.json(); const p=d.programs||[]; if(!p.length){el.innerHTML='<div class="vtv-epg-empty">Chưa có lịch</div>';return;} let h=''; p.forEach(x=>{const n=x.now?' now':''; h+='<div class="vtv-epg-item'+n+'"><span class="t">'+(x.time||'')+(x.end_time?'-'+x.end_time:'')+'</span> <span class="n">'+(x.title||'')+'</span></div>';}); el.innerHTML=h; const ni=el.querySelector('.vtv-epg-item.now'); if(ni) ni.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});}catch(e){el.innerHTML='<div class="vtv-epg-empty">Không tải được lịch</div>';}
|
| 782 |
}
|
| 783 |
|
| 784 |
window._vtvToggleEpg=function(){const l=document.getElementById('vtv-epg-list'),b=document.getElementById('vtv-epg-toggle'); if(!l||!b)return; if(l.style.display==='none'){l.style.display='flex';b.textContent='Ẩn';}else{l.style.display='none';b.textContent='Hiện';}};
|
|
|
|
| 801 |
video.style.display='none'; errEl.style.display='none'; loadEl.style.display='flex';
|
| 802 |
loadEl.innerHTML='<div class="vtv-spinner"></div>Đang kết nối '+ch.name+'...';
|
| 803 |
if(_hls){_hls.destroy();_hls=null;}
|
|
|
|
|
|
|
| 804 |
const urls=STREAMS[chId]||[];
|
| 805 |
if(!urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=chId==='vtvprime'?'VTVPrime: Kênh trả phí.':ch.name+': Không có luồng.';return;}
|
| 806 |
+
_tryPlay(video,urls,0,ch.name,loadEl,errEl,errMsg);
|
|
|
|
| 807 |
loadEpg(chId);
|
| 808 |
if(_miniActive){document.getElementById('vtv-mini-ch').textContent=ch.name;fetch('/api/vtv/epg/'+_currentCh).then(r=>r.json()).then(d=>{const p=d.programs||[];const n=p.find(x=>x.now);document.getElementById('vtv-mini-epg').textContent=n?n.time+' '+n.title:(p[0]?p[0].time+' '+p[0].title:'');}).catch(()=>{});}
|
| 809 |
};
|
|
|
|
| 812 |
if(idx>=urls.length){loadEl.style.display='none';errEl.style.display='flex';errMsg.textContent=name+': Tất cả nguồn lỗi.';return;}
|
| 813 |
const src=urls[idx]; loadEl.innerHTML='<div class="vtv-spinner"></div>Kết nối '+name+' ('+(idx+1)+'/'+urls.length+')...';
|
| 814 |
if(typeof Hls!=='undefined'&&Hls.isSupported()){
|
| 815 |
+
const hls=new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:20}); _hls=hls; hls.loadSource(src); hls.attachMedia(video);
|
| 816 |
hls.on(Hls.Events.MANIFEST_PARSED,()=>{video.play().catch(()=>{});loadEl.style.display='none';video.style.display='block';});
|
| 817 |
let rec=0; hls.on(Hls.Events.ERROR,(ev,data)=>{if(data.fatal){if(data.type===Hls.ErrorTypes.NETWORK_ERROR){rec++;if(rec<=3)setTimeout(()=>hls.startLoad(),2000);else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}else if(data.type===Hls.ErrorTypes.MEDIA_ERROR){try{hls.recoverMediaError();}catch(e){}}else{hls.destroy();_hls=null;_tryPlay(video,urls,idx+1,name,loadEl,errEl,errMsg);}}});
|
| 818 |
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
|
|
|
| 833 |
window.loadHome=async function(){const old=document.getElementById('vtv-block');if(old)old.remove();_blockInserted=false;const r=await _origLoadHome.apply(this,arguments);try{pinBlock();}catch(e){}return r;};
|
| 834 |
window.loadHome.__vtvWrapped=true;
|
| 835 |
}
|
| 836 |
+
})();
|