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:
parent
9fc9c71410
commit
54deee7fe8
1 changed files with 193 additions and 5 deletions
|
|
@ -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 ▶ 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 ▶ 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 -%}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue