Replace PiP mode with draggable pop-out window
The native browser Picture-in-Picture mode had limitations: - Couldn't stay on top of all windows (terminals went over it) - No user control over drag/resize - Browser-controlled positioning Changes: - Simplified openMediaPlayer() to always use pop-out window - Pop-out window is draggable and resizable by user - Always-on-top toggle uses window.focus() for better control - Removed PiP button and togglePip() function - Removed keyboard shortcut for PiP (P key) - Cleaned up unused helper functions (preloadNextMedia, loadMedia) - Reorganized player controls (3x3 grid) The pop-out window gives users full control over positioning and sizing.
This commit is contained in:
parent
9f895f17e0
commit
af8da71c9a
4 changed files with 10 additions and 385 deletions
|
|
@ -45,26 +45,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle Picture-in-Picture mode
|
||||
*/
|
||||
window.togglePip = async function() {
|
||||
if (!document.pictureInPictureEnabled || !media || media.tagName !== 'VIDEO') {
|
||||
alert('Picture-in-Picture is not supported for this media');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (document.pictureInPictureElement) {
|
||||
await document.exitPictureInPicture();
|
||||
} else {
|
||||
await media.requestPictureInPicture();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error toggling PiP:', err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle fullscreen mode
|
||||
*/
|
||||
|
|
@ -407,13 +387,6 @@
|
|||
goToProductPage();
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
e.preventDefault();
|
||||
if (typeof togglePip === 'function') {
|
||||
togglePip();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,184 +96,10 @@
|
|||
</section>
|
||||
|
||||
<script>
|
||||
// Global player data for navigation
|
||||
let currentPlayerData = null;
|
||||
let preloadedNextMedia = null;
|
||||
|
||||
// Preload next media for instant playback
|
||||
async function preloadNextMedia() {
|
||||
if (!currentPlayerData || !currentPlayerData.nextProductId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/player/${currentPlayerData.nextProductId}`);
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const nextMediaEl = doc.querySelector('video, audio');
|
||||
|
||||
if (nextMediaEl && nextMediaEl.src) {
|
||||
// Create hidden preload element
|
||||
if (preloadedNextMedia) {
|
||||
preloadedNextMedia.remove();
|
||||
}
|
||||
|
||||
preloadedNextMedia = document.createElement(nextMediaEl.tagName.toLowerCase());
|
||||
preloadedNextMedia.src = nextMediaEl.src;
|
||||
preloadedNextMedia.preload = 'auto';
|
||||
preloadedNextMedia.style.display = 'none';
|
||||
document.body.appendChild(preloadedNextMedia);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Could not preload next media:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load new media into existing element
|
||||
async function loadMedia(mediaElement, productId) {
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newVideoEl = doc.querySelector('video, audio');
|
||||
const scriptEl = doc.querySelector('script:last-of-type');
|
||||
|
||||
if (newVideoEl && newVideoEl.src) {
|
||||
mediaElement.src = newVideoEl.src;
|
||||
await mediaElement.play();
|
||||
|
||||
// Update playerData for continued navigation
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
currentPlayerData = eval('(' + match[1] + ')');
|
||||
|
||||
// Navigate parent page to show the new product
|
||||
if (currentPlayerData.productUrl) {
|
||||
window.history.pushState({}, '', currentPlayerData.productUrl);
|
||||
}
|
||||
|
||||
// Preload next video after switching
|
||||
preloadNextMedia();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading media:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function openMediaPlayer(productId) {
|
||||
// Mobile detection
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// On mobile, open in new tab (works better than PiP)
|
||||
if (isMobile) {
|
||||
window.open(`/player/${productId}`, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
// Desktop: Quick check if this is video/audio or other media
|
||||
// For images/PDFs, just open window directly to avoid Firefox blocking
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
|
||||
// Parse the response to extract media URL and nav data
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const videoEl = doc.querySelector('video, audio');
|
||||
const imageEl = doc.querySelector('img#media');
|
||||
const iframeEl = doc.querySelector('iframe#media');
|
||||
|
||||
// For images and PDFs, just open pop-out window immediately
|
||||
if (imageEl || iframeEl) {
|
||||
window.open(`/player/${productId}`, 'mediaPlayer', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoEl) {
|
||||
console.error('No media element found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create hidden video/audio element on this page
|
||||
const media = document.createElement(videoEl.tagName.toLowerCase());
|
||||
media.src = videoEl.src;
|
||||
media.style.position = 'fixed';
|
||||
media.style.bottom = '20px';
|
||||
media.style.right = '20px';
|
||||
media.style.width = '320px';
|
||||
media.style.zIndex = '9999';
|
||||
media.controls = true;
|
||||
media.autoplay = true;
|
||||
media.playsInline = true; // Important for iOS
|
||||
document.body.appendChild(media);
|
||||
|
||||
// Extract playerData from script
|
||||
const scriptEl = doc.querySelector('script:last-of-type');
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
currentPlayerData = eval('(' + match[1] + ')');
|
||||
}
|
||||
}
|
||||
|
||||
// Enter Picture-in-Picture mode (only for video)
|
||||
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
|
||||
await media.play();
|
||||
await media.requestPictureInPicture();
|
||||
|
||||
// Preload next video immediately for instant playback
|
||||
preloadNextMedia();
|
||||
|
||||
// Auto-advance when video ends
|
||||
media.addEventListener('ended', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up Media Session API for prev/next controls
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', async () => {
|
||||
if (currentPlayerData && currentPlayerData.prevProductId) {
|
||||
await loadMedia(media, currentPlayerData.prevProductId);
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up when PiP closes
|
||||
media.addEventListener('leavepictureinpicture', () => {
|
||||
document.body.removeChild(media);
|
||||
});
|
||||
} else if (media.tagName === 'AUDIO') {
|
||||
// Audio: keep small floating player on page
|
||||
media.play();
|
||||
|
||||
// Preload next audio
|
||||
preloadNextMedia();
|
||||
|
||||
// Auto-advance when audio ends
|
||||
media.addEventListener('ended', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening media player:', err);
|
||||
}
|
||||
function openMediaPlayer(productId) {
|
||||
// Open pop-out player window (draggable and resizable)
|
||||
const features = 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no';
|
||||
window.open(`/player/${productId}`, 'mediaPlayer', features);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -219,12 +219,12 @@
|
|||
<!-- Row 2: Features -->
|
||||
<button id="alwaysOnTopBtn" onclick="toggleAlwaysOnTop()" title="Always on Top (T)">📌 On Top</button>
|
||||
<button id="fullscreenBtn" onclick="toggleFullscreen()" title="Fullscreen (F)">⛶ Full</button>
|
||||
<button id="pipBtn" onclick="togglePip()" title="Picture-in-Picture (P)">📺 PiP</button>
|
||||
<button onclick="goToProductPage()" title="Go to Product Page (G)">🔗 Page</button>
|
||||
|
||||
<!-- Row 3: More options -->
|
||||
<button id="autoResizeBtn" onclick="toggleAutoResize()" title="Auto-Resize Window (R)">↔ Resize</button>
|
||||
<button onclick="goToProductPage()" title="Go to Product Page (G)">🔗 Page</button>
|
||||
<button onclick="window.close()" title="Close Window (ESC)">✕ Close</button>
|
||||
<span></span><!-- Empty cell for grid alignment -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -174,184 +174,10 @@
|
|||
</section>
|
||||
|
||||
<script>
|
||||
// Global player data for navigation
|
||||
let currentPlayerData = null;
|
||||
let preloadedNextMedia = null;
|
||||
|
||||
// Preload next media for instant playback
|
||||
async function preloadNextMedia() {
|
||||
if (!currentPlayerData || !currentPlayerData.nextProductId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/player/${currentPlayerData.nextProductId}`);
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const nextMediaEl = doc.querySelector('video, audio');
|
||||
|
||||
if (nextMediaEl && nextMediaEl.src) {
|
||||
// Create hidden preload element
|
||||
if (preloadedNextMedia) {
|
||||
preloadedNextMedia.remove();
|
||||
}
|
||||
|
||||
preloadedNextMedia = document.createElement(nextMediaEl.tagName.toLowerCase());
|
||||
preloadedNextMedia.src = nextMediaEl.src;
|
||||
preloadedNextMedia.preload = 'auto';
|
||||
preloadedNextMedia.style.display = 'none';
|
||||
document.body.appendChild(preloadedNextMedia);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Could not preload next media:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Load new media into existing element
|
||||
async function loadMedia(mediaElement, productId) {
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const newVideoEl = doc.querySelector('video, audio');
|
||||
const scriptEl = doc.querySelector('script:last-of-type');
|
||||
|
||||
if (newVideoEl && newVideoEl.src) {
|
||||
mediaElement.src = newVideoEl.src;
|
||||
await mediaElement.play();
|
||||
|
||||
// Update playerData for continued navigation
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
currentPlayerData = eval('(' + match[1] + ')');
|
||||
|
||||
// Navigate parent page to show the new product
|
||||
if (currentPlayerData.productUrl) {
|
||||
window.history.pushState({}, '', currentPlayerData.productUrl);
|
||||
}
|
||||
|
||||
// Preload next video after switching
|
||||
preloadNextMedia();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading media:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function openMediaPlayer(productId) {
|
||||
// Mobile detection
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// On mobile, open in new tab (works better than PiP)
|
||||
if (isMobile) {
|
||||
window.open(`/player/${productId}`, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
// Desktop: Quick check if this is video/audio or other media
|
||||
// For images/PDFs, just open window directly to avoid Firefox blocking
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
|
||||
// Parse the response to extract media URL and nav data
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const videoEl = doc.querySelector('video, audio');
|
||||
const imageEl = doc.querySelector('img#media');
|
||||
const iframeEl = doc.querySelector('iframe#media');
|
||||
|
||||
// For images and PDFs, just open pop-out window immediately
|
||||
if (imageEl || iframeEl) {
|
||||
window.open(`/player/${productId}`, 'mediaPlayer', 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoEl) {
|
||||
console.error('No media element found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create hidden video/audio element on this page
|
||||
const media = document.createElement(videoEl.tagName.toLowerCase());
|
||||
media.src = videoEl.src;
|
||||
media.style.position = 'fixed';
|
||||
media.style.bottom = '20px';
|
||||
media.style.right = '20px';
|
||||
media.style.width = '320px';
|
||||
media.style.zIndex = '9999';
|
||||
media.controls = true;
|
||||
media.autoplay = true;
|
||||
media.playsInline = true; // Important for iOS
|
||||
document.body.appendChild(media);
|
||||
|
||||
// Extract playerData from script
|
||||
const scriptEl = doc.querySelector('script:last-of-type');
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
currentPlayerData = eval('(' + match[1] + ')');
|
||||
}
|
||||
}
|
||||
|
||||
// Enter Picture-in-Picture mode (only for video)
|
||||
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
|
||||
await media.play();
|
||||
await media.requestPictureInPicture();
|
||||
|
||||
// Preload next video immediately for instant playback
|
||||
preloadNextMedia();
|
||||
|
||||
// Auto-advance when video ends
|
||||
media.addEventListener('ended', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up Media Session API for prev/next controls
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', async () => {
|
||||
if (currentPlayerData && currentPlayerData.prevProductId) {
|
||||
await loadMedia(media, currentPlayerData.prevProductId);
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up when PiP closes
|
||||
media.addEventListener('leavepictureinpicture', () => {
|
||||
document.body.removeChild(media);
|
||||
});
|
||||
} else if (media.tagName === 'AUDIO') {
|
||||
// Audio: keep small floating player on page
|
||||
media.play();
|
||||
|
||||
// Preload next audio
|
||||
preloadNextMedia();
|
||||
|
||||
// Auto-advance when audio ends
|
||||
media.addEventListener('ended', async () => {
|
||||
if (currentPlayerData && currentPlayerData.nextProductId) {
|
||||
await loadMedia(media, currentPlayerData.nextProductId);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening media player:', err);
|
||||
}
|
||||
function openMediaPlayer(productId) {
|
||||
// Open pop-out player window (draggable and resizable)
|
||||
const features = 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no';
|
||||
window.open(`/player/${productId}`, 'mediaPlayer', features);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue