Add direct PiP button on product page bypassing pop-up window

Add 📺 PiP button overlay on video thumbnails that opens Document PiP
directly from the product page without opening pop-up window first.

Features:
- PiP button positioned at top-right of video thumbnail
- Fetches video URL from /player/{product_id} endpoint
- Opens Document PiP window with video + prev/next controls
- Navigates to /product/{id} pages (not /player/{id})
- Auto-advances to next product when video ends
- Falls back to pop-up window if Document PiP unsupported
- Comprehensive console logging for debugging

User flow now:
1. Product page with video thumbnail
2. Click ▶ play overlay -> pop-out window (existing behavior)
3. Click 📺 PiP button -> directly to Picture-in-Picture mode

This eliminates the need to open pop-up then click PiP button.

Requires Chrome 111+ or Edge 111+ for Document PiP support.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
russell@unturf.com 2026-01-22 08:06:19 -05:00
parent 9fc9c71410
commit 54deee7fe8

View file

@ -37,11 +37,14 @@
{% set video_extensions = ["mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v"] %}
{% if signed_get_object_url and product.extensions.get("product") in video_extensions %}
{% if request.popout_player_enabled and product.visibility == 1 and "preview" in product.extensions %}
<a href="javascript:void(0)" onclick="openMediaPlayer('{{ product.id }}')" class="video-thumbnail-container">
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main" />
<div class="video-play-overlay"></div>
</a>
<div class="video-click-to-play">click &#9654; to play in pop-out player</div>
<div style="position: relative; display: inline-block;">
<a href="javascript:void(0)" onclick="openMediaPlayer('{{ product.id }}')" class="video-thumbnail-container">
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main" />
<div class="video-play-overlay"></div>
</a>
<button onclick="openPipPlayer('{{ product.id }}')" style="position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">📺 PiP</button>
</div>
<div class="video-click-to-play">click &#9654; for pop-out or 📺 for picture-in-picture</div>
{% else %}
<a href="{{ signed_get_object_url }}" target="_blank" class="video-thumbnail-container">
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main" />
@ -179,6 +182,191 @@ function openMediaPlayer(productId) {
const features = 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no';
window.open(`/player/${productId}`, 'mediaPlayer', features);
}
// Open directly in Document Picture-in-Picture mode
async function openPipPlayer(productId) {
console.log('Opening PiP for product:', productId);
// Check for Document PiP support
if (!('documentPictureInPicture' in window)) {
alert('Document Picture-in-Picture is not supported in this browser.\n\nPlease use Chrome 111+ or Edge 111+.\n\nFalling back to pop-up window.');
openMediaPlayer(productId);
return;
}
try {
// Fetch the player page to get video URL and metadata
console.log('Fetching player page...');
const response = await fetch(`/player/${productId}`);
const html = await response.text();
// Parse HTML to extract video URL and metadata
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Find video element and get source
const videoEl = doc.querySelector('video');
if (!videoEl || !videoEl.src) {
console.error('No video element or source found');
alert('Could not find video. Opening in pop-up window instead.');
openMediaPlayer(productId);
return;
}
const videoSrc = videoEl.src;
console.log('Video source:', videoSrc);
// Extract player data from script tag
const scriptContent = doc.querySelector('script:not([src])').textContent;
const playerDataMatch = scriptContent.match(/window\.playerData\s*=\s*({[^}]+})/);
let playerData = { prevProductId: null, nextProductId: null };
if (playerDataMatch) {
try {
// Clean up the matched content and parse it
const dataStr = playerDataMatch[1]
.replace(/"/g, '"')
.replace(/'/g, '"')
.replace(/(\w+):/g, '"$1":')
.replace(/null/g, 'null');
playerData = JSON.parse(dataStr);
console.log('Parsed player data:', playerData);
} catch (e) {
console.warn('Could not parse player data:', e);
}
}
// Get product title
const titleEl = doc.querySelector('.product-title');
const productTitle = titleEl ? titleEl.textContent : 'Video';
// Open Document PiP window with custom controls
console.log('Opening Document PiP window...');
const pipWindow = await window.documentPictureInPicture.requestWindow({
width: 640,
height: 420 // 360 for 16:9 video + 60 for controls
});
console.log('Document PiP window opened');
// Add styles to PiP window
const pipStyle = pipWindow.document.createElement('style');
pipStyle.textContent = `
body {
margin: 0;
padding: 0;
background: #000;
overflow: hidden;
font-family: sans-serif;
}
.pip-container {
display: grid;
grid-template-rows: 1fr auto;
width: 100vw;
height: 100vh;
}
.pip-video {
width: 100%;
height: 100%;
object-fit: contain;
}
.pip-controls {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 0.5rem;
padding: 0.5rem;
background: rgba(0,0,0,0.9);
align-items: center;
}
.pip-controls button {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.pip-controls button:hover:not(:disabled) {
background: rgba(255,255,255,0.4);
}
.pip-controls button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.pip-title {
color: white;
font-size: 0.9rem;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
pipWindow.document.head.appendChild(pipStyle);
// Build PiP content
const container = pipWindow.document.createElement('div');
container.className = 'pip-container';
// Create video element
const video = pipWindow.document.createElement('video');
video.className = 'pip-video';
video.src = videoSrc;
video.controls = true;
video.autoplay = true;
// Create controls
const controls = pipWindow.document.createElement('div');
controls.className = 'pip-controls';
// Previous button
const prevBtn = pipWindow.document.createElement('button');
prevBtn.textContent = '◀ Prev';
prevBtn.disabled = !playerData.prevProductId;
if (playerData.prevProductId) {
prevBtn.onclick = () => {
window.location.href = `/product/${playerData.prevProductId}`;
};
}
// Title
const title = pipWindow.document.createElement('div');
title.className = 'pip-title';
title.textContent = productTitle;
// Next button
const nextBtn = pipWindow.document.createElement('button');
nextBtn.textContent = 'Next ▶';
nextBtn.disabled = !playerData.nextProductId;
if (playerData.nextProductId) {
nextBtn.onclick = () => {
window.location.href = `/product/${playerData.nextProductId}`;
};
}
controls.appendChild(prevBtn);
controls.appendChild(title);
controls.appendChild(nextBtn);
container.appendChild(video);
container.appendChild(controls);
pipWindow.document.body.appendChild(container);
// Auto-advance when video ends
video.addEventListener('ended', () => {
if (playerData.nextProductId) {
window.location.href = `/product/${playerData.nextProductId}`;
}
});
console.log('Document PiP setup complete');
} catch (err) {
console.error('Failed to open Document PiP:', err);
alert('Failed to open Picture-in-Picture: ' + err.message + '\n\nOpening in pop-up window instead.');
openMediaPlayer(productId);
}
}
</script>
{%- endblock -%}