252 lines
5.5 KiB
JavaScript
252 lines
5.5 KiB
JavaScript
import { Portal } from '@gorhom/portal'
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { Pressable, Text, View } from 'react-native'
|
|
|
|
import { resolveMediaUri } from '../utils/resolveMediaUri'
|
|
|
|
const CLOSE_THRESHOLD_SECONDS = 0.35
|
|
const CLOSE_POLL_INTERVAL_MS = 500
|
|
|
|
const overlayStyle = {
|
|
position: 'fixed',
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0,
|
|
backgroundColor: 'black',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
zIndex: 9999,
|
|
}
|
|
|
|
const videoStyle = {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
objectFit: 'cover',
|
|
}
|
|
|
|
const closeButtonStyle = {
|
|
position: 'absolute',
|
|
top: 50,
|
|
right: 20,
|
|
backgroundColor: '#00000080',
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 14,
|
|
borderRadius: 20,
|
|
borderWidth: 1,
|
|
borderColor: '#FFFFFF55',
|
|
cursor: 'pointer',
|
|
}
|
|
|
|
const closeTextStyle = {
|
|
color: '#FFF',
|
|
fontSize: 14,
|
|
}
|
|
|
|
const playButtonStyle = {
|
|
backgroundColor: '#000000CC',
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 20,
|
|
borderRadius: 24,
|
|
borderWidth: 1,
|
|
borderColor: '#FFFFFF88',
|
|
cursor: 'pointer',
|
|
zIndex: 1,
|
|
}
|
|
|
|
const FullscreenIntroVideo = ({
|
|
url,
|
|
visible = true,
|
|
onClose,
|
|
contentFit = 'cover',
|
|
allowSkip = true,
|
|
requireAudio = false,
|
|
offerSoundActivation = false,
|
|
skipLabel = 'Passer la vidéo',
|
|
playLabel = 'Lire la vidéo avec le son',
|
|
}) => {
|
|
const videoRef = useRef(null)
|
|
const [uri, setUri] = useState(null)
|
|
const [muted, setMuted] = useState(false)
|
|
const [awaitingInteraction, setAwaitingInteraction] = useState(false)
|
|
const hasClosedRef = useRef(false)
|
|
|
|
const handleClose = useCallback(() => {
|
|
if (hasClosedRef.current) return
|
|
hasClosedRef.current = true
|
|
onClose?.()
|
|
}, [onClose])
|
|
|
|
const evaluateShouldClose = useCallback(() => {
|
|
const video = videoRef.current
|
|
|
|
if (!video || hasClosedRef.current) {
|
|
return
|
|
}
|
|
|
|
if (video.ended) {
|
|
handleClose()
|
|
return
|
|
}
|
|
|
|
const remaining = video.duration - video.currentTime
|
|
|
|
if (Number.isFinite(remaining) && remaining <= CLOSE_THRESHOLD_SECONDS) {
|
|
handleClose()
|
|
}
|
|
}, [handleClose])
|
|
|
|
useEffect(() => {
|
|
let isMounted = true
|
|
|
|
const assignUri = (nextUri) => {
|
|
if (isMounted) {
|
|
setMuted(false)
|
|
setAwaitingInteraction(false)
|
|
setUri(nextUri)
|
|
}
|
|
}
|
|
|
|
const load = async () => {
|
|
try {
|
|
const nextUri = await resolveMediaUri(url)
|
|
if (!nextUri) {
|
|
if (isMounted) {
|
|
setUri(null)
|
|
handleClose()
|
|
}
|
|
return
|
|
}
|
|
assignUri(nextUri)
|
|
} catch {
|
|
if (isMounted) {
|
|
setUri(null)
|
|
handleClose()
|
|
}
|
|
}
|
|
}
|
|
|
|
load()
|
|
|
|
return () => {
|
|
isMounted = false
|
|
}
|
|
}, [handleClose, url])
|
|
|
|
useEffect(() => {
|
|
if (!visible || !uri) {
|
|
return undefined
|
|
}
|
|
|
|
hasClosedRef.current = false
|
|
let rafId
|
|
|
|
const attemptPlay = () => {
|
|
const video = videoRef.current
|
|
|
|
if (!video) {
|
|
rafId = requestAnimationFrame(attemptPlay)
|
|
return
|
|
}
|
|
|
|
video.currentTime = 0
|
|
const result = video.play()
|
|
|
|
if (result?.catch) {
|
|
result.catch((error) => {
|
|
if (error?.name === 'NotAllowedError' && requireAudio) {
|
|
setAwaitingInteraction(true)
|
|
} else if (error?.name === 'NotAllowedError' && !muted) {
|
|
video.muted = true
|
|
setMuted(true)
|
|
setAwaitingInteraction(offerSoundActivation)
|
|
const mutedResult = video.play()
|
|
mutedResult?.catch?.(() => {})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
attemptPlay()
|
|
const pollId = setInterval(evaluateShouldClose, CLOSE_POLL_INTERVAL_MS)
|
|
|
|
return () => {
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId)
|
|
}
|
|
clearInterval(pollId)
|
|
const video = videoRef.current
|
|
video?.pause()
|
|
}
|
|
}, [evaluateShouldClose, muted, offerSoundActivation, requireAudio, uri, visible])
|
|
|
|
const handleManualPlay = useCallback(() => {
|
|
const video = videoRef.current
|
|
if (!video) return
|
|
|
|
setMuted(false)
|
|
video.muted = false
|
|
video.currentTime = 0
|
|
const result = video.play()
|
|
if (result?.then) {
|
|
result.then(() => setAwaitingInteraction(false)).catch(() => {})
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const video = videoRef.current
|
|
|
|
if (!video || !muted) {
|
|
return
|
|
}
|
|
|
|
const result = video.play()
|
|
|
|
if (result?.catch) {
|
|
result.catch(() => {})
|
|
}
|
|
}, [muted])
|
|
|
|
if (!visible) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Portal>
|
|
<View pointerEvents="box-none" style={overlayStyle}>
|
|
{uri ? (
|
|
<video
|
|
ref={videoRef}
|
|
src={uri}
|
|
style={{ ...videoStyle, objectFit: contentFit }}
|
|
playsInline
|
|
autoPlay
|
|
loop={false}
|
|
muted={muted}
|
|
controls={false}
|
|
onEnded={handleClose}
|
|
onPause={evaluateShouldClose}
|
|
onTimeUpdate={evaluateShouldClose}
|
|
onError={handleClose}
|
|
/>
|
|
) : null}
|
|
{awaitingInteraction ? (
|
|
<Pressable onPress={handleManualPlay} style={playButtonStyle}>
|
|
<Text style={closeTextStyle}>{playLabel}</Text>
|
|
</Pressable>
|
|
) : null}
|
|
{allowSkip ? (
|
|
<Pressable onPress={handleClose} style={closeButtonStyle}>
|
|
<Text style={closeTextStyle}>{skipLabel}</Text>
|
|
</Pressable>
|
|
) : null}
|
|
</View>
|
|
</Portal>
|
|
)
|
|
}
|
|
|
|
export default FullscreenIntroVideo
|