Add native Picture-in-Picture support and debug ended event

Picture-in-Picture (PiP) improvements:
- Add native browser PiP button for videos (P key or 📺 button)
- PiP has true OS-level always-on-top when you right-click and select "Keep on top"
- PiP works on mobile and desktop with browser's native implementation
- Button shows on mobile (PiP is useful on mobile devices)
- Active state toggles button text between "📺 PiP" and "📺 Exit PiP"

Debug 'ended' event not firing:
- Add explicit ended event listener with console logging
- Log when media starts playing
- Log playback progress every 5 seconds
- This will help identify why auto-advance isn't working

The native PiP API provides better always-on-top than our window.focus()
approach because it's implemented at the OS level by the browser.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
russell@unturf.com 2026-01-21 12:49:44 -05:00
parent 741004b2d2
commit d244617554
2 changed files with 89 additions and 13 deletions

View file

@ -381,6 +381,13 @@
goToProductPage();
}
break;
case 'p':
case 'P':
e.preventDefault();
if (typeof togglePip === 'function') {
togglePip();
}
break;
}
}

View file

@ -72,10 +72,8 @@
}
/* Hide desktop-only buttons on mobile */
#pipBtn,
#autoResizeBtn,
#alwaysOnTopBtn,
#fullscreenBtn {
#alwaysOnTopBtn {
display: none;
}
@ -231,8 +229,10 @@
<!-- Row 2: Features -->
<button id="alwaysOnTopBtn" onclick="toggleAlwaysOnTop()" title="Always on Top (T)">📌 On Top</button>
{% if media_type == 'video' %}
<button id="pipBtn" onclick="togglePip()" title="Picture-in-Picture (P) - Right-click PiP for true always-on-top">📺 PiP</button>
{% endif %}
<button onclick="goToProductPage()" title="Go to Product Page (G)">🔗 Page</button>
<span></span><!-- Empty cell for grid alignment -->
<!-- Row 3: More options -->
<button id="autoResizeBtn" onclick="toggleAutoResize()" title="Auto-Resize Window (R)">↔ Resize</button>
@ -260,6 +260,41 @@
window.open(window.playerData.productUrl, '_blank');
}
// Toggle native Picture-in-Picture (true always-on-top via browser)
function togglePip() {
const media = document.getElementById('media');
if (!media || media.tagName !== 'VIDEO') {
return;
}
if (document.pictureInPictureElement) {
document.exitPictureInPicture().catch(err => {
console.error('Failed to exit PiP:', err);
});
} else {
media.requestPictureInPicture().catch(err => {
console.error('Failed to enter PiP:', err);
});
}
}
// Update PiP button state
document.addEventListener('enterpictureinpicture', () => {
const pipBtn = document.getElementById('pipBtn');
if (pipBtn) {
pipBtn.classList.add('active');
pipBtn.textContent = '📺 Exit PiP';
}
});
document.addEventListener('leavepictureinpicture', () => {
const pipBtn = document.getElementById('pipBtn');
if (pipBtn) {
pipBtn.classList.remove('active');
pipBtn.textContent = '📺 PiP';
}
});
// Mobile: tap to show/hide controls
let hideTimeout;
const isMobile = window.innerWidth <= 600;
@ -314,17 +349,51 @@
if (media && (media.tagName === 'VIDEO' || media.tagName === 'AUDIO')) {
// Try to play muted first (allowed by most browsers)
media.muted = true;
media.play().then(() => {
// Once playing, unmute after a short delay
setTimeout(() => {
media.muted = false;
}, 100);
}).catch(err => {
console.log('Autoplay failed:', err);
// User will need to tap play button
});
const playPromise = media.play();
if (playPromise !== undefined) {
playPromise.then(() => {
// Once playing, unmute after a short delay
setTimeout(() => {
media.muted = false;
}, 100);
}).catch(err => {
console.log('Autoplay failed:', err);
// User will need to tap play button
});
}
}
}
// Debug: Add ended event listener to verify it works
(function() {
const media = document.getElementById('media');
if (media && (media.tagName === 'VIDEO' || media.tagName === 'AUDIO')) {
media.addEventListener('ended', function() {
console.log('Media ended event fired!');
if (window.playerData && window.playerData.nextProductId) {
console.log('Navigating to next:', window.playerData.nextProductId);
window.location.href = '/player/' + window.playerData.nextProductId;
} else {
console.log('No next product available');
}
});
// Debug: Log when media is playing
media.addEventListener('play', function() {
console.log('Media started playing');
});
// Debug: Log time updates to verify playback
let lastLogTime = 0;
media.addEventListener('timeupdate', function() {
const currentTime = Math.floor(media.currentTime);
if (currentTime > lastLogTime && currentTime % 5 === 0) {
console.log('Playback at:', currentTime + 's / ' + Math.floor(media.duration) + 's');
lastLogTime = currentTime;
}
});
}
})();
</script>
</body>
</html>