feat: enable picture-in-picture when video is scrolled out of view (#258)

Co-authored-by: Wutche <wuthchechikaome75@gmail.com>
This commit is contained in:
Alchemist
2025-04-12 19:24:39 -07:00
committed by GitHub
parent 4a143d1814
commit c95444748a
2 changed files with 97 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
import { cn } from '@/lib/utils'
import NsfwOverlay from '../NsfwOverlay'
import { useEffect, useRef } from 'react'
import VideoManager from '@/services/videomanager'
export default function VideoPlayer({
src,
@@ -12,15 +14,63 @@ export default function VideoPlayer({
isNsfw?: boolean
size?: 'normal' | 'small'
}) {
const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const video = videoRef.current
const container = containerRef.current
if (!video || !container) return
const observer = new IntersectionObserver(
async ([entry]) => {
const isVisible = entry.isIntersecting
if (!isVisible && !video.paused) {
await VideoManager.enterPiP(video)
}
if (isVisible) {
if (
document.pictureInPictureElement === video ||
(video as any).webkitPresentationMode === 'picture-in-picture'
) {
await VideoManager.exitPiP(video)
}
}
},
{
threshold: 0.5
}
)
observer.observe(container)
return () => {
observer.unobserve(container)
}
}, [])
const handlePlay = async () => {
const video = videoRef.current
if (!video) return
await VideoManager.playVideo(video)
}
return (
<div className="relative">
<video
controls
className={cn('rounded-lg', size === 'small' ? 'h-[15vh]' : 'h-[30vh]', className)}
src={src}
onClick={(e) => e.stopPropagation()}
/>
{isNsfw && <NsfwOverlay className="rounded-lg" />}
</div>
<>
<div ref={containerRef} className="relative">
<video
ref={videoRef}
controls
className={cn('rounded-lg', size === 'small' ? 'h-[15vh]' : 'h-[30vh]', className)}
src={src}
onClick={(e) => e.stopPropagation()}
onPlay={handlePlay}
/>
{isNsfw && <NsfwOverlay className="rounded-lg" />}
</div>
</>
)
}

View File

@@ -0,0 +1,38 @@
class VideoManager {
private static currentVideo: HTMLVideoElement | null = null
static async enterPiP(video: HTMLVideoElement) {
if (VideoManager.currentVideo && VideoManager.currentVideo !== video) {
await VideoManager.exitPiP(VideoManager.currentVideo)
}
if ('requestPictureInPicture' in video) {
await video.requestPictureInPicture()
} else if ('webkitSetPresentationMode' in video) {
;(video as any).webkitSetPresentationMode('picture-in-picture')
}
}
static async exitPiP(video: HTMLVideoElement) {
video.pause()
if (document.pictureInPictureElement === video) {
await document.exitPictureInPicture()
} else if ('webkitSetPresentationMode' in video) {
;(video as any).webkitSetPresentationMode('inline')
}
if (VideoManager.currentVideo === video) {
VideoManager.currentVideo = null
}
}
static async playVideo(video: HTMLVideoElement) {
if (VideoManager.currentVideo && VideoManager.currentVideo !== video) {
await VideoManager.exitPiP(VideoManager.currentVideo)
}
VideoManager.currentVideo = video
video.play()
}
}
export default VideoManager