+
(null)
+let collageHeaderVisible = true
+let collageHeaderObserver: IntersectionObserver | null = null
+const staggerTimers = new Set>()
+let staggerToken = 0
+
+const { stopped: previewPlaybackStopped } = useIdlePreviewPlayback({
+ onStop: stopPreviews,
+ onRestart: () => startStaggeredPlayback(),
+})
+
+function previewsSuppressed(): boolean {
+ return previewPlaybackStopped.value || !collageHeaderVisible || document.hidden
+}
+
let disposeOutOfBoundsHandler: (() => void) | null = null
let lastReleaseShortcutRow: number | null = null
@@ -362,15 +378,35 @@ function isVideoReady(index: number): boolean {
return videoStates.value[index] === "ready"
}
+function cancelStaggeredPlayback() {
+ staggerToken += 1
+ for (const timer of staggerTimers) {
+ clearTimeout(timer)
+ }
+ staggerTimers.clear()
+}
+
+function scheduleStaggeredStart(token: number, start: () => void, delayMs: number) {
+ const timer = setTimeout(() => {
+ staggerTimers.delete(timer)
+ if (token !== staggerToken || previewsSuppressed()) return
+ start()
+ }, delayMs)
+ staggerTimers.add(timer)
+}
+
// Start staggered video playback
function startStaggeredPlayback() {
+ cancelStaggeredPlayback()
+ const token = staggerToken
const videos = videoRefs.value.filter((v) => v !== null) as HTMLVideoElement[]
- if (videos.length === 0) return
+ if (videos.length === 0 || previewsSuppressed()) return
if (safariAutoplay) {
videos.forEach((video, index) => {
const offset = COLLAGE_START_OFFSETS_SECONDS[index] ?? 0
const startVideo = () => {
+ if (token !== staggerToken || previewsSuppressed()) return
video.currentTime = offset
video.play().catch(() => {})
}
@@ -390,11 +426,29 @@ function startStaggeredPlayback() {
// Set up staggered start for remaining videos
for (let i = 1; i < videos.length; i++) {
- setTimeout(() => {
- const video = videos[i]
- if (!video) return
- video.play().catch(() => {})
- }, i * 2000)
+ scheduleStaggeredStart(
+ token,
+ () => {
+ const video = videos[i]
+ if (!video) return
+ video.play().catch(() => {})
+ },
+ i * 2000,
+ )
+ }
+}
+
+// Stop all collage previews and cancel any pending staggered starts
+function stopPreviews() {
+ cancelStaggeredPlayback()
+ clearHoverAudioIdleTimer()
+ hoveredVideoIndex = null
+ for (const interval of volumeFadeIntervals.values()) {
+ clearInterval(interval)
+ }
+ volumeFadeIntervals.clear()
+ for (const video of videoRefs.value) {
+ video?.pause()
}
}
@@ -497,6 +551,19 @@ onMounted(() => {
startStaggeredPlayback()
}, 100)
+ // Pause the collage videos while the header is scrolled out of view
+ if (collageHeaderRef.value) {
+ collageHeaderObserver = new IntersectionObserver((entries) => {
+ collageHeaderVisible = entries[0]?.isIntersecting ?? true
+ if (collageHeaderVisible) {
+ startStaggeredPlayback()
+ } else {
+ stopPreviews()
+ }
+ })
+ collageHeaderObserver.observe(collageHeaderRef.value)
+ }
+
registerMovieOutOfBoundsShortcut()
})
@@ -990,6 +1057,9 @@ onUnmounted(() => {
window.removeEventListener("mousemove", handleHoverAudioMouseMove)
window.removeEventListener("mediahive:gamepad-action", handleGamepadAction as EventListener)
clearHoverAudioIdleTimer()
+ cancelStaggeredPlayback()
+ collageHeaderObserver?.disconnect()
+ collageHeaderObserver = null
disposeOutOfBoundsHandler?.()
disposeOutOfBoundsHandler = null
lastReleaseShortcutRow = null
diff --git a/frontend/src/components/SeriesFullView.vue b/frontend/src/components/SeriesFullView.vue
index ceded68..1e7f718 100644
--- a/frontend/src/components/SeriesFullView.vue
+++ b/frontend/src/components/SeriesFullView.vue
@@ -62,6 +62,8 @@
:src="getSeasonPoster(season)"
class="season-poster-img"
:alt="season.name || `Season ${season.season_number}`"
+ loading="lazy"
+ decoding="async"
/>
{{ season.season_number }}
@@ -99,7 +101,7 @@
v-if="getEpisodeVideoSources(episode).length > 0"
:ref="(el) => setVideoRef(el as HTMLVideoElement, `${sIndex}-${eIndex}`)"
:autoplay="false"
- preload="auto"
+ :preload="sIndex === activeSeasonIndex ? 'auto' : 'none'"
loop
muted
playsinline
@@ -153,6 +155,8 @@
:src="getCoverUrl(movie.cover_path, movie.root_id)"
class="linked-movie-poster"
:alt="movie.title || 'Movie'"
+ loading="lazy"
+ decoding="async"
/>