@@ -1648,17 +1757,29 @@ function playVideo(id){
const resume=(v.resume_pos&&v.resume_pos>5)?v.resume_pos:0;
player={isTranscode:isT,base:v.video_url,seekBase:isT?resume:0,duration:v.duration_secs||0};
const initSrc=isT?transcodeUrlAt(player.seekBase):esc(safeUrl(v.video_url));
- const dmax=Math.max(1,Math.floor(player.duration||0));
- const bar=isT?`
-
-
-
- 0:00 / ${fmtDur(player.duration)}
-
-
-
-
-
`:'';
+ const pip=!!document.pictureInPictureEnabled;
+ const controls=`
+
⏵
+
+
+
+
+
+
+
+
0:00 / ${fmtDur(player.duration)}
+
+ ${hasSubs?`
`:''}
+
+
+
+ ${pip?`
`:''}
+
+
+
`;
bg.innerHTML=`
${esc(v.title)}${metaLine}
@@ -1666,21 +1787,32 @@ function playVideo(id){
-
${bar}
${chapPane}${transcriptPane}
+
${controls}
${chapPane}${transcriptPane}
`;
document.body.appendChild(bg);
const vid=bg.querySelector('#player-video');
+ // Restore the viewer's last volume + speed.
+ vid.volume=(plPrefs.volume!=null)?plPrefs.volume:1;
+ vid.playbackRate=plPrefs.speed||1;
+ const volEl=bg.querySelector('#v-vol'); if(volEl)volEl.value=vid.volume;
+ const spEl=bg.querySelector('#v-speed'); if(spEl)spEl.textContent=(plPrefs.speed||1)+'×';
// Direct (seekable) videos resume via currentTime; a transcode stream was
- // already started at the resume offset above.
- if(!isT&&resume)vid.addEventListener('loadedmetadata',()=>{vid.currentTime=resume},{once:true});
- if(isT){
- const sk=bg.querySelector('#v-seek');
- sk.addEventListener('input',()=>{sk._drag=true;const tl=document.getElementById('v-time');if(tl)tl.textContent=fmtDur(+sk.value)+' / '+fmtDur(player.duration);});
- sk.addEventListener('change',()=>{sk._drag=false;playerSeek(+sk.value);});
- bg.querySelector('#v-vol').addEventListener('input',function(){vid.volume=+this.value;vid.muted=(+this.value===0);updateVctrl();});
- ['timeupdate','play','pause','volumechange'].forEach(ev=>vid.addEventListener(ev,updateVctrl));
+ // already started at the resume offset above. Pick up the real duration for
+ // direct streams, and honour a saved captions-on preference.
+ vid.addEventListener('loadedmetadata',()=>{
+ if(!isT&&resume)vid.currentTime=resume;
+ if(!player.duration&&isFinite(vid.duration))player.duration=vid.duration;
+ if(hasSubs&&plPrefs.cc&&vid.textTracks&&vid.textTracks[0]){vid.textTracks[0].mode='showing';const b=document.getElementById('v-cc');if(b)b.classList.add('on');}
updateVctrl();
- }
+ },{once:true});
+ plBindScrubber(bg.querySelector('#pl-track'));
+ if(volEl)volEl.addEventListener('input',function(){vid.volume=+this.value;vid.muted=(+this.value===0);plPrefs.volume=vid.volume;savePlPrefs();updateVctrl();});
+ ['timeupdate','play','pause','volumechange','progress','ratechange','seeked','loadeddata'].forEach(ev=>vid.addEventListener(ev,updateVctrl));
+ const wrap=bg.querySelector('#pl-wrap');
+ wrap.addEventListener('pointermove',plActivity);
+ vid.addEventListener('play',plActivity); vid.addEventListener('pause',plActivity);
+ vid.addEventListener('click',()=>vTogglePlay());
+ updateVctrl(); plActivity();
bg.tabIndex=-1;bg.addEventListener('keydown',playerKey);bg.focus();
if(saveTimer)clearInterval(saveTimer);
saveTimer=setInterval(()=>savePosition(vid),5000);
@@ -1702,6 +1834,8 @@ async function savePosition(vid){
function closeModal(el){
if(saveTimer){clearInterval(saveTimer);saveTimer=null}
+ if(plIdleTimer){clearTimeout(plIdleTimer);plIdleTimer=null}
+ plDragging=false;
const vid=el.querySelector('video');
if(vid){savePosition(vid);vid.pause();vid.src=''}
currentPlayingId=null;
@@ -1715,6 +1849,9 @@ async function loadChapters(id){
const list=document.getElementById('chapters-list');if(!list)return;
if(!chapters.length){list.innerHTML='
No chapters';return}
list.innerHTML=chapters.map(c=>`
${fmtDur(c.start)}
${esc(c.title)}
`).join('');
+ // Chapter markers on the scrubber.
+ const ticks=document.getElementById('pl-ticks');
+ if(ticks&&player.duration)ticks.innerHTML=chapters.map(c=>`
`).join('');
const vid=document.getElementById('player-video');
if(vid)vid.addEventListener('timeupdate',()=>{
const t=effTime();let ai=-1;