Spaces:
Running
Running
Fix: M3U player video tags now have allowFullscreen, x5-playsinline, webkit-playsinline for mobile fullscreen. Added PiP (Picture-in-Picture) button and togglePiP function. Enhanced toggleFeedFullscreen with screen.orientation.lock landscape on mobile. Fixed HLS live manifest - single segment replaced with 3-segment sliding window for continuous playback.
Browse files- static/app_v2.js +58 -3
static/app_v2.js
CHANGED
|
@@ -1686,7 +1686,7 @@ async function doInteract(videoId,type){try{const r=await fetch('/api/v2/interac
|
|
| 1686 |
async function getInteractions(videoId){try{return await fetch('/api/v2/interactions?id='+encodeURIComponent(videoId)).then(r=>r.json());}catch(e){return{views:0,likes:0,comments:0};}}
|
| 1687 |
async function getComments(videoId){try{const j=await fetch('/api/v2/comments?id='+encodeURIComponent(videoId)).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
|
| 1688 |
async function postComment(videoId,text){try{const j=await fetch('/api/v2/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,text})}).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
|
| 1689 |
-
function buildTikTokSlide(opts){return`<div class="tiktok-slide" data-vid="${esc(opts.videoId)}" data-slide-kind="${esc(opts.kind||'')}"${opts.attrs||''}><div class="slide-gesture" data-gesture="1"></div><div class="slide-status" id="stt-${esc(opts.videoId)}"></div>${opts.vtag}<div class="tiktok-bottom"><span class="badge ${opts.badgeClass||'badge-fpt'}">${opts.badge||''}</span><p class="tiktok-title">${esc(opts.title)}</p>${opts.desc?`<p class="tiktok-desc">${esc(opts.desc)}</p>`:''}</div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation();doView('${esc(opts.videoId)}',this)"><div class="icon">👁</div><div class="count" id="vc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doLike('${esc(opts.videoId)}',this)"><div class="icon">❤️</div><div class="count" id="lc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleComments('${esc(opts.videoId)}',${opts.idx})"><div class="icon">💬</div><div class="count" id="cc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doShare('${esc(opts.title)}','${esc(opts.shareUrl||'')}','${esc(opts.shareImg||'')}','${esc(opts.postId||'')}')"><div class="icon">📤</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggle169View('${esc(opts.videoId)}','169-toggle-${opts.idx}')"><div class="icon" id="169-toggle-${opts.idx}">🖥️</div></button>${opts.extraBtn||''}<button class="tiktok-right-btn" onclick="event.stopPropagation();toggleFeedFullscreen('${esc(opts.videoId)}')"><div class="icon" id="fs-toggle-${opts.idx}">⛶</div></button></div><span class="tiktok-counter">${opts.idx+1}/${opts.total}</span><div class="inline-comments" id="cmt-inline-${opts.idx}" style="display:none"></div></div>`;}
|
| 1690 |
function toggleFeedFullscreen(videoId){
|
| 1691 |
const slide = videoId ? document.querySelector('.tiktok-slide[data-vid="'+videoId+'"]') : null;
|
| 1692 |
const feedEl = slide ? slide.closest('.tiktok-container') : document.querySelector('.tiktok-container');
|
|
@@ -1696,10 +1696,65 @@ function toggleFeedFullscreen(videoId){
|
|
| 1696 |
const icons = feedEl.querySelectorAll('[id^="fs-toggle-"]');
|
| 1697 |
icons.forEach(ic => { ic.textContent = on ? '✕' : '⛶'; });
|
| 1698 |
if(on){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1699 |
// keep the active slide centered & playable in full height
|
| 1700 |
setTimeout(()=>{ slide && slide.scrollIntoView({block:'nearest'}); }, 60);
|
| 1701 |
} else if(slide){
|
| 1702 |
slide.scrollIntoView({block:'nearest'});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1703 |
}
|
| 1704 |
}
|
| 1705 |
async function doView(videoId,btn){const j=await doInteract(videoId,'view');if(j){const c=btn.querySelector('.count');if(c)c.textContent=fmtNum(j.views);}}
|
|
@@ -2839,7 +2894,7 @@ async function openM3UPlayer(chIdx){
|
|
| 2839 |
// mirror fallback for the same channel is done via the status retry.
|
| 2840 |
var purl = _m3uProxyUrl(firstStream);
|
| 2841 |
var vtag = '<div class="yt-thumb-wrap" style="'+(logo?('background-image:url(\''+esc(logo)+'\')'):'')+'">'
|
| 2842 |
-
+ '<video playsinline preload="none" data-hls="' + esc(purl) + '" loop'
|
| 2843 |
+ (logo ? ' poster="' + esc(logo) + '"' : '')
|
| 2844 |
+ ' style="width:100%;height:100%;object-fit:contain;background:#000"></video></div>';
|
| 2845 |
var title = och.name || och.title || 'Kênh';
|
|
@@ -2957,7 +3012,7 @@ function playM3UStream(chIdx, streamIdx){
|
|
| 2957 |
window._m3uOrdered = ordered;
|
| 2958 |
var logo = ch.logo || '';
|
| 2959 |
var vtag = '<div class="yt-thumb-wrap" style="'+(logo?('background-image:url(\''+esc(logo)+'\')'):'')+'">'
|
| 2960 |
-
+ '<video playsinline preload="none" data-hls="' + esc(purl) + '" loop'
|
| 2961 |
+ (logo ? ' poster="' + esc(logo) + '"' : '')
|
| 2962 |
+ ' style="width:100%;height:100%;object-fit:contain;background:#000"></video></div>';
|
| 2963 |
var _chJson = '';
|
|
|
|
| 1686 |
async function getInteractions(videoId){try{return await fetch('/api/v2/interactions?id='+encodeURIComponent(videoId)).then(r=>r.json());}catch(e){return{views:0,likes:0,comments:0};}}
|
| 1687 |
async function getComments(videoId){try{const j=await fetch('/api/v2/comments?id='+encodeURIComponent(videoId)).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
|
| 1688 |
async function postComment(videoId,text){try{const j=await fetch('/api/v2/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,text})}).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
|
| 1689 |
+
function buildTikTokSlide(opts){return`<div class="tiktok-slide" data-vid="${esc(opts.videoId)}" data-slide-kind="${esc(opts.kind||'')}"${opts.attrs||''}><div class="slide-gesture" data-gesture="1"></div><div class="slide-status" id="stt-${esc(opts.videoId)}"></div>${opts.vtag}<div class="tiktok-bottom"><span class="badge ${opts.badgeClass||'badge-fpt'}">${opts.badge||''}</span><p class="tiktok-title">${esc(opts.title)}</p>${opts.desc?`<p class="tiktok-desc">${esc(opts.desc)}</p>`:''}</div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation();doView('${esc(opts.videoId)}',this)"><div class="icon">👁</div><div class="count" id="vc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doLike('${esc(opts.videoId)}',this)"><div class="icon">❤️</div><div class="count" id="lc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleComments('${esc(opts.videoId)}',${opts.idx})"><div class="icon">💬</div><div class="count" id="cc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doShare('${esc(opts.title)}','${esc(opts.shareUrl||'')}','${esc(opts.shareImg||'')}','${esc(opts.postId||'')}')"><div class="icon">📤</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggle169View('${esc(opts.videoId)}','169-toggle-${opts.idx}')"><div class="icon" id="169-toggle-${opts.idx}">🖥️</div></button>${opts.extraBtn||''}<button class="tiktok-right-btn" onclick="event.stopPropagation();togglePiP('${esc(opts.videoId)}')" title="Picture-in-Picture"><div class="icon" id="pip-toggle-${opts.idx}">⧉</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleFeedFullscreen('${esc(opts.videoId)}')" title="Toàn màn hình"><div class="icon" id="fs-toggle-${opts.idx}">⛶</div></button></div><span class="tiktok-counter">${opts.idx+1}/${opts.total}</span><div class="inline-comments" id="cmt-inline-${opts.idx}" style="display:none"></div></div>`;}
|
| 1690 |
function toggleFeedFullscreen(videoId){
|
| 1691 |
const slide = videoId ? document.querySelector('.tiktok-slide[data-vid="'+videoId+'"]') : null;
|
| 1692 |
const feedEl = slide ? slide.closest('.tiktok-container') : document.querySelector('.tiktok-container');
|
|
|
|
| 1696 |
const icons = feedEl.querySelectorAll('[id^="fs-toggle-"]');
|
| 1697 |
icons.forEach(ic => { ic.textContent = on ? '✕' : '⛶'; });
|
| 1698 |
if(on){
|
| 1699 |
+
// On mobile: lock to landscape for a true fullscreen experience
|
| 1700 |
+
if(slide){
|
| 1701 |
+
const vid = slide.querySelector('video');
|
| 1702 |
+
if(vid){
|
| 1703 |
+
// Try the native Fullscreen API first (works on mobile with allowFullscreen attr)
|
| 1704 |
+
if(vid.requestFullscreen){
|
| 1705 |
+
vid.requestFullscreen().catch(()=>{
|
| 1706 |
+
// Fallback: try to enter Picture-in-Picture if fullscreen fails
|
| 1707 |
+
if(vid.disablePictureInPicture === false && vid.requestPictureInPicture){
|
| 1708 |
+
vid.requestPictureInPicture().catch(()=>{});
|
| 1709 |
+
}
|
| 1710 |
+
});
|
| 1711 |
+
}
|
| 1712 |
+
// Lock screen orientation on mobile browsers that support it
|
| 1713 |
+
if(screen.orientation && screen.orientation.lock){
|
| 1714 |
+
screen.orientation.lock('landscape').catch(()=>{});
|
| 1715 |
+
}
|
| 1716 |
+
}
|
| 1717 |
+
}
|
| 1718 |
// keep the active slide centered & playable in full height
|
| 1719 |
setTimeout(()=>{ slide && slide.scrollIntoView({block:'nearest'}); }, 60);
|
| 1720 |
} else if(slide){
|
| 1721 |
slide.scrollIntoView({block:'nearest'});
|
| 1722 |
+
// Restore portrait orientation on mobile
|
| 1723 |
+
if(screen.orientation && screen.orientation.lock){
|
| 1724 |
+
screen.orientation.lock('portrait-primary').catch(()=>{});
|
| 1725 |
+
}
|
| 1726 |
+
// Exit native fullscreen if active
|
| 1727 |
+
if(document.fullscreenElement){
|
| 1728 |
+
document.exitFullscreen().catch(()=>{});
|
| 1729 |
+
}
|
| 1730 |
+
}
|
| 1731 |
+
}
|
| 1732 |
+
|
| 1733 |
+
// Picture-in-Picture toggle for M3U player
|
| 1734 |
+
function togglePiP(videoId){
|
| 1735 |
+
const slide = videoId ? document.querySelector('.tiktok-slide[data-vid="'+videoId+'"]') : null;
|
| 1736 |
+
if(!slide) return;
|
| 1737 |
+
const vid = slide.querySelector('video');
|
| 1738 |
+
if(!vid) return;
|
| 1739 |
+
const icons = document.querySelectorAll('[id^="pip-toggle-"]');
|
| 1740 |
+
const icon = icons[0];
|
| 1741 |
+
if(!icon) return;
|
| 1742 |
+
// Check if PiP is supported
|
| 1743 |
+
if(!vid.disablePictureInPicture && vid.requestPictureInPicture){
|
| 1744 |
+
if(document.pictureInPictureElement){
|
| 1745 |
+
// Currently in PiP - exit
|
| 1746 |
+
document.exitPictureInPicture().catch(()=>{});
|
| 1747 |
+
if(icon) icon.textContent = '⧉';
|
| 1748 |
+
} else {
|
| 1749 |
+
// Enter PiP
|
| 1750 |
+
vid.requestPictureInPicture().then(function(){
|
| 1751 |
+
if(icon) icon.textContent = '❐';
|
| 1752 |
+
}).catch(function(err){
|
| 1753 |
+
toast('PiP không khả dụng: ' + (err.message || err));
|
| 1754 |
+
});
|
| 1755 |
+
}
|
| 1756 |
+
} else {
|
| 1757 |
+
toast('Trình duyệt không hỗ trợ Picture-in-Picture');
|
| 1758 |
}
|
| 1759 |
}
|
| 1760 |
async function doView(videoId,btn){const j=await doInteract(videoId,'view');if(j){const c=btn.querySelector('.count');if(c)c.textContent=fmtNum(j.views);}}
|
|
|
|
| 2894 |
// mirror fallback for the same channel is done via the status retry.
|
| 2895 |
var purl = _m3uProxyUrl(firstStream);
|
| 2896 |
var vtag = '<div class="yt-thumb-wrap" style="'+(logo?('background-image:url(\''+esc(logo)+'\')'):'')+'">'
|
| 2897 |
+
+ '<video playsinline x5-playsinline webkit-playsinline allowFullscreen preload="none" data-hls="' + esc(purl) + '" loop'
|
| 2898 |
+ (logo ? ' poster="' + esc(logo) + '"' : '')
|
| 2899 |
+ ' style="width:100%;height:100%;object-fit:contain;background:#000"></video></div>';
|
| 2900 |
var title = och.name || och.title || 'Kênh';
|
|
|
|
| 3012 |
window._m3uOrdered = ordered;
|
| 3013 |
var logo = ch.logo || '';
|
| 3014 |
var vtag = '<div class="yt-thumb-wrap" style="'+(logo?('background-image:url(\''+esc(logo)+'\')'):'')+'">'
|
| 3015 |
+
+ '<video playsinline x5-playsinline webkit-playsinline allowFullscreen preload="none" data-hls="' + esc(purl) + '" loop'
|
| 3016 |
+ (logo ? ' poster="' + esc(logo) + '"' : '')
|
| 3017 |
+ ' style="width:100%;height:100%;object-fit:contain;background:#000"></video></div>';
|
| 3018 |
var _chJson = '';
|