Add native Picture-in-Picture mode with Media Session API
- Click video enters native browser PiP mode directly - Uses Media Session API to add prev/next track buttons to PiP - Falls back to small floating player if PiP not supported - Cleaner than pop-out window, truly floats over all apps
This commit is contained in:
parent
a93d5e457f
commit
dcbfd10101
2 changed files with 134 additions and 22 deletions
|
|
@ -96,19 +96,75 @@
|
|||
</section>
|
||||
|
||||
<script>
|
||||
function openMediaPlayer(productId) {
|
||||
const features = 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no';
|
||||
const playerWindow = window.open('/player/' + productId, 'mediaPlayer', features);
|
||||
async function openMediaPlayer(productId) {
|
||||
// Fetch player data to get media URL and navigation info
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
|
||||
// Apply always-on-top if enabled in localStorage
|
||||
if (localStorage.getItem('player_always_on_top') === 'true' && playerWindow) {
|
||||
const focusInterval = setInterval(() => {
|
||||
if (playerWindow.closed) {
|
||||
clearInterval(focusInterval);
|
||||
} else {
|
||||
playerWindow.focus();
|
||||
// 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 scriptEl = doc.querySelector('script:last-of-type');
|
||||
|
||||
if (!videoEl) {
|
||||
console.error('No media element found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create hidden video 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;
|
||||
document.body.appendChild(media);
|
||||
|
||||
// Extract playerData from script
|
||||
let playerData = null;
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
playerData = eval('(' + match[1] + ')');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Enter Picture-in-Picture mode
|
||||
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
|
||||
await media.play();
|
||||
await media.requestPictureInPicture();
|
||||
|
||||
// Set up Media Session API for prev/next controls
|
||||
if ('mediaSession' in navigator && playerData) {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
if (playerData.prevProductId) {
|
||||
window.location.href = `/player/${playerData.prevProductId}`;
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
if (playerData.nextProductId) {
|
||||
window.location.href = `/player/${playerData.nextProductId}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up when PiP closes
|
||||
media.addEventListener('leavepictureinpicture', () => {
|
||||
document.body.removeChild(media);
|
||||
});
|
||||
} else {
|
||||
// Fallback: keep small floating player on page
|
||||
media.play();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening media player:', err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -174,19 +174,75 @@
|
|||
</section>
|
||||
|
||||
<script>
|
||||
function openMediaPlayer(productId) {
|
||||
const features = 'width=800,height=600,menubar=no,toolbar=no,location=no,status=no,scrollbars=no';
|
||||
const playerWindow = window.open('/player/' + productId, 'mediaPlayer', features);
|
||||
async function openMediaPlayer(productId) {
|
||||
// Fetch player data to get media URL and navigation info
|
||||
try {
|
||||
const response = await fetch(`/player/${productId}`);
|
||||
const html = await response.text();
|
||||
|
||||
// Apply always-on-top if enabled in localStorage
|
||||
if (localStorage.getItem('player_always_on_top') === 'true' && playerWindow) {
|
||||
const focusInterval = setInterval(() => {
|
||||
if (playerWindow.closed) {
|
||||
clearInterval(focusInterval);
|
||||
} else {
|
||||
playerWindow.focus();
|
||||
// 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 scriptEl = doc.querySelector('script:last-of-type');
|
||||
|
||||
if (!videoEl) {
|
||||
console.error('No media element found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create hidden video 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;
|
||||
document.body.appendChild(media);
|
||||
|
||||
// Extract playerData from script
|
||||
let playerData = null;
|
||||
if (scriptEl) {
|
||||
const scriptContent = scriptEl.textContent;
|
||||
const match = scriptContent.match(/window\.playerData\s*=\s*(\{[^}]+\})/);
|
||||
if (match) {
|
||||
playerData = eval('(' + match[1] + ')');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Enter Picture-in-Picture mode
|
||||
if (document.pictureInPictureEnabled && media.tagName === 'VIDEO') {
|
||||
await media.play();
|
||||
await media.requestPictureInPicture();
|
||||
|
||||
// Set up Media Session API for prev/next controls
|
||||
if ('mediaSession' in navigator && playerData) {
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
if (playerData.prevProductId) {
|
||||
window.location.href = `/player/${playerData.prevProductId}`;
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
if (playerData.nextProductId) {
|
||||
window.location.href = `/player/${playerData.nextProductId}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up when PiP closes
|
||||
media.addEventListener('leavepictureinpicture', () => {
|
||||
document.body.removeChild(media);
|
||||
});
|
||||
} else {
|
||||
// Fallback: keep small floating player on page
|
||||
media.play();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error opening media player:', err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue