<div {{ stimulus_controller('@pwa/picture-in-picture') }}>
<div class="advanced-player" {{ stimulus_target('@pwa/picture-in-picture', 'container') }}>
<video
id="player-video"
{{ stimulus_target('@pwa/picture-in-picture', 'floating') }}
src="/videos/demo.mp4"
></video>
<div class="player-ui">
<div class="progress-container">
<input type="range" id="progress" min="0" max="100" value="0">
<span id="time">0:00 / 0:00</span>
</div>
<div class="controls">
<button id="play-pause">▶️</button>
<button id="mute">🔊</button>
<input type="range" id="volume" min="0" max="100" value="100">
<button id="fullscreen">⛶</button>
<button
{{ stimulus_action('@pwa/picture-in-picture', 'toggle', 'click') }}
id="pip-btn"
>
📺 PiP
</button>
</div>
</div>
</div>
<div class="video-description">
<h2>Video Title</h2>
<p>Video description and details...</p>
</div>
</div>
<script>
const video = document.getElementById('player-video');
const playPauseBtn = document.getElementById('play-pause');
const muteBtn = document.getElementById('mute');
const volumeSlider = document.getElementById('volume');
const progressSlider = document.getElementById('progress');
const timeDisplay = document.getElementById('time');
const pipBtn = document.getElementById('pip-btn');
// Play/Pause
playPauseBtn.addEventListener('click', () => {
if (video.paused) {
video.play();
playPauseBtn.textContent = '⏸️';
} else {
video.pause();
playPauseBtn.textContent = '▶️';
}
});
// Mute/Unmute
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '🔇' : '🔊';
});
// Volume
volumeSlider.addEventListener('input', (e) => {
video.volume = e.target.value / 100;
});
// Progress
video.addEventListener('timeupdate', () => {
const percent = (video.currentTime / video.duration) * 100;
progressSlider.value = percent;
const current = formatTime(video.currentTime);
const total = formatTime(video.duration);
timeDisplay.textContent = `${current} / ${total}`;
});
progressSlider.addEventListener('input', (e) => {
const time = (e.target.value / 100) * video.duration;
video.currentTime = time;
});
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// PiP Events
document.addEventListener('pwa--picture-in-picture:enter', () => {
pipBtn.textContent = '⬛ Exit PiP';
pipBtn.classList.add('active');
// Auto-play when entering PiP
video.play();
console.log('Video is now floating - continue browsing!');
});
document.addEventListener('pwa--picture-in-picture:exit', () => {
pipBtn.textContent = '📺 PiP';
pipBtn.classList.remove('active');
console.log('Video returned to main window');
});
document.addEventListener('pwa--picture-in-picture:unsupported', () => {
pipBtn.style.display = 'none';
console.warn('Picture-in-Picture not supported');
});
document.addEventListener('pwa--picture-in-picture:error', (event) => {
console.error('PiP error:', event.detail);
alert('Failed to activate Picture-in-Picture mode');
});
// Fullscreen
document.getElementById('fullscreen').addEventListener('click', () => {
if (video.requestFullscreen) {
video.requestFullscreen();
}
});
</script>
<style>
.advanced-player {
position: relative;
max-width: 800px;
background: #000;
border-radius: 8px;
overflow: hidden;
}
video {
width: 100%;
display: block;
}
.player-ui {
background: linear-gradient(transparent, rgba(0,0,0,0.8));
padding: 10px;
}
.controls {
display: flex;
gap: 10px;
align-items: center;
}
.controls button {
background: transparent;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
padding: 5px 10px;
}
.controls button.active {
background: rgba(255,255,255,0.2);
border-radius: 4px;
}
.progress-container {
margin-bottom: 10px;
}
#progress {
width: 100%;
}
#time {
color: white;
font-size: 12px;
}
.video-description {
padding: 20px;
}
/* Style for when video is in PiP */
.advanced-player.in-pip {
opacity: 0.5;
}
</style>