bep40 commited on
Commit
5b6000c
·
verified ·
1 Parent(s): 88a69e5

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
Files changed (1) hide show
  1. 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 = '';