start working on controls
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z" fill="#000"/></svg>
|
||||||
|
After Width: | Height: | Size: 392 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z" fill="#000"/></svg>
|
||||||
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z" fill="#000"/></svg>
|
||||||
|
After Width: | Height: | Size: 183 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z" fill="#000"/></svg>
|
||||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1,9 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const ArrowsDiagonalOut_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M13 4a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0V6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L17.586 5H14a1 1 0 0 1-1-1Zm-9 9a1 1 0 0 1 1 1v3.586l4.293-4.293a1 1 0 0 1 1.414 1.414L6.414 19H10a1 1 0 1 1 0 2H5a2 2 0 0 1-2-2v-5a1 1 0 0 1 1-1Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ArrowsDiagonalIn_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M20.957 3.043a1 1 0 0 1 0 1.414L16.414 9H20a1 1 0 1 1 0 2h-5a2 2 0 0 1-2-2V4a1 1 0 1 1 2 0v3.586l4.543-4.543a1 1 0 0 1 1.414 0ZM3 14a1 1 0 0 1 1-1h5a2 2 0 0 1 2 2v5a1 1 0 1 1-2 0v-3.586l-4.543 4.543a1 1 0 0 1-1.414-1.414L7.586 15H4a1 1 0 0 1-1-1Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Pause_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Zm7 1a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Zm3-1a1 1 0 0 0-1 1v12a1 1 0 1 0 2 0V6a1 1 0 0 0-1-1Z',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Pause_Filled_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 6a3 3 0 0 1 6 0v12a3 3 0 1 1-6 0V6ZM14 6a3 3 0 1 1 6 0v12a3 3 0 1 1-6 0V6Z',
|
||||||
|
})
|
||||||
@@ -17,6 +17,7 @@ export function VideoEmbed({source}: {source: string}) {
|
|||||||
source,
|
source,
|
||||||
})
|
})
|
||||||
const [onScreen, setOnScreen] = useState(false)
|
const [onScreen, setOnScreen] = useState(false)
|
||||||
|
const [hasBeenOnScreen, setHasBeenOnScreen] = useState(false)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const onPress = useCallback(() => setActive(), [setActive])
|
const onPress = useCallback(() => setActive(), [setActive])
|
||||||
@@ -28,6 +29,9 @@ export function VideoEmbed({source}: {source: string}) {
|
|||||||
const entry = entries[0]
|
const entry = entries[0]
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
setOnScreen(entry.isIntersecting)
|
setOnScreen(entry.isIntersecting)
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenOnScreen(true)
|
||||||
|
}
|
||||||
sendPosition(
|
sendPosition(
|
||||||
entry.boundingClientRect.y + entry.boundingClientRect.height / 2,
|
entry.boundingClientRect.y + entry.boundingClientRect.height / 2,
|
||||||
)
|
)
|
||||||
@@ -48,7 +52,7 @@ export function VideoEmbed({source}: {source: string}) {
|
|||||||
a.my_xs,
|
a.my_xs,
|
||||||
]}>
|
]}>
|
||||||
<div ref={ref} style={{display: 'flex', flex: 1}}>
|
<div ref={ref} style={{display: 'flex', flex: 1}}>
|
||||||
{onScreen || active ? (
|
{hasBeenOnScreen || active ? (
|
||||||
<VideoEmbedInner
|
<VideoEmbedInner
|
||||||
source={source}
|
source={source}
|
||||||
active={active}
|
active={active}
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react'
|
import React, {useEffect, useRef, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import Hls from 'hls.js'
|
import Hls from 'hls.js'
|
||||||
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
|
import {ArrowsDiagonalOut_Stroke2_Corner2_Rounded as FullscreenIcon} from '#/components/icons/ArrowsDiagonal'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
|
import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
|
||||||
|
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
|
|
||||||
export function VideoEmbedInner({
|
export function VideoEmbedInner({
|
||||||
active,
|
active,
|
||||||
@@ -79,8 +89,6 @@ export function VideoPlayer({
|
|||||||
const ref = useRef<HTMLVideoElement>(null)
|
const ref = useRef<HTMLVideoElement>(null)
|
||||||
const [focused, setFocused] = useState(false)
|
const [focused, setFocused] = useState(false)
|
||||||
|
|
||||||
const {play, pause, togglePlayPause} = useVideoUtils(ref)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
|
if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
@@ -107,22 +115,6 @@ export function VideoPlayer({
|
|||||||
}
|
}
|
||||||
}, [source, hls])
|
}, [source, hls])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (active) {
|
|
||||||
play()
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
pause()
|
|
||||||
setFocused(false)
|
|
||||||
}
|
|
||||||
}, [active, play, pause])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!onScreen) {
|
|
||||||
pause()
|
|
||||||
}
|
|
||||||
}, [onScreen, pause])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -140,10 +132,18 @@ export function VideoPlayer({
|
|||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
background: active ? 'red' : 'blue',
|
background: active ? 'red' : 'blue',
|
||||||
opacity: 0.5,
|
opacity: 0.8,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Controls
|
||||||
|
videoRef={ref}
|
||||||
|
active={active}
|
||||||
|
setActive={setActive}
|
||||||
|
focused={focused}
|
||||||
|
setFocused={setFocused}
|
||||||
|
onScreen={onScreen}
|
||||||
|
/>
|
||||||
<video
|
<video
|
||||||
src={source}
|
src={source}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -152,51 +152,244 @@ export function VideoPlayer({
|
|||||||
preload="none"
|
preload="none"
|
||||||
loop
|
loop
|
||||||
muted={!focused}
|
muted={!focused}
|
||||||
autoPlay={active}
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Controls({
|
||||||
|
videoRef,
|
||||||
|
active,
|
||||||
|
setActive,
|
||||||
|
focused,
|
||||||
|
setFocused,
|
||||||
|
onScreen,
|
||||||
|
}: {
|
||||||
|
videoRef: React.RefObject<HTMLVideoElement>
|
||||||
|
active: boolean
|
||||||
|
setActive: () => void
|
||||||
|
focused: boolean
|
||||||
|
setFocused: (focused: boolean) => void
|
||||||
|
onScreen: boolean
|
||||||
|
}) {
|
||||||
|
const {play, pause, playing, muted, togglePlayPause, currentTime, duration} =
|
||||||
|
useVideoUtils(videoRef)
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {
|
||||||
|
state: hovered,
|
||||||
|
onIn: onMouseEnter,
|
||||||
|
onOut: onMouseLeave,
|
||||||
|
} = useInteractionState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active) {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
pause()
|
||||||
|
setFocused(false)
|
||||||
|
}
|
||||||
|
}, [active, play, pause, setFocused])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onScreen) {
|
||||||
|
pause()
|
||||||
|
}
|
||||||
|
}, [onScreen, pause])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
onClick={evt => {
|
onClick={evt => {
|
||||||
evt.stopPropagation()
|
evt.stopPropagation()
|
||||||
if (focused) {
|
if (!focused) {
|
||||||
togglePlayPause()
|
|
||||||
} else {
|
|
||||||
if (!active) {
|
if (!active) {
|
||||||
setActive()
|
setActive()
|
||||||
}
|
}
|
||||||
setFocused(true)
|
setFocused(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
onMouseEnter={onMouseEnter}
|
||||||
|
onMouseLeave={onMouseLeave}>
|
||||||
|
{hovered && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.px_xs,
|
||||||
|
a.pt_md,
|
||||||
|
a.pb_lg,
|
||||||
|
a.absolute,
|
||||||
|
a.gap_xs,
|
||||||
|
a.flex_row,
|
||||||
|
{bottom: 0},
|
||||||
|
{backgroundColor: 'rgba(0, 0, 0, 0.5)'},
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={_(playing ? msg`Pause` : msg`Play`)}
|
||||||
|
onPress={() => togglePlayPause()}
|
||||||
|
variant="ghost"
|
||||||
|
shape="round"
|
||||||
|
size="medium">
|
||||||
|
<ButtonIcon icon={playing ? PauseIcon : PlayIcon} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
label={_(muted ? msg`Unmute` : msg`Mute`)}
|
||||||
|
onPress={() => {
|
||||||
|
console.log('clicked mute btn')
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
shape="round"
|
||||||
|
size="medium">
|
||||||
|
<ButtonIcon icon={muted ? MuteIcon : UnmuteIcon} />
|
||||||
|
</Button>
|
||||||
|
<View style={a.flex_1} />
|
||||||
|
{/* TODO: find workaround for iOS Safari */}
|
||||||
|
<Button
|
||||||
|
label={_(muted ? msg`Unmute` : msg`Mute`)}
|
||||||
|
onPress={() => {
|
||||||
|
console.log('clicked fullscreen btn')
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
shape="round"
|
||||||
|
size="medium">
|
||||||
|
<ButtonIcon icon={FullscreenIcon} />
|
||||||
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
{(hovered || !focused) && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(200)}
|
||||||
|
exiting={FadeOut.duration(200)}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
height: 5,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.4)',
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
{duration > 0 && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.h_full,
|
||||||
|
a.mr_auto,
|
||||||
|
{
|
||||||
|
backgroundColor: t.palette.white,
|
||||||
|
width: `${(currentTime / duration) * 100}%`,
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
|
function useVideoUtils(ref: React.RefObject<HTMLVideoElement>) {
|
||||||
const play = () => {
|
const [playing, setPlaying] = useState(false)
|
||||||
|
const [muted, setMuted] = useState(true)
|
||||||
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
|
const [duration, setDuration] = useState(0)
|
||||||
|
const [canPlay, setCanPlay] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return
|
||||||
|
let current = ref.current
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
if (!current) return
|
||||||
|
setCurrentTime(current.currentTime || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDurationChange = () => {
|
||||||
|
if (!current) return
|
||||||
|
setDuration(current.duration || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
setPlaying(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
setPlaying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMute = () => {
|
||||||
|
setMuted(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnmute = () => {
|
||||||
|
setMuted(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCanPlay = () => {
|
||||||
|
setCanPlay(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.current.addEventListener('timeupdate', handleTimeUpdate)
|
||||||
|
ref.current.addEventListener('durationchange', handleDurationChange)
|
||||||
|
ref.current.addEventListener('play', handlePlay)
|
||||||
|
ref.current.addEventListener('pause', handlePause)
|
||||||
|
ref.current.addEventListener('mute', handleMute)
|
||||||
|
ref.current.addEventListener('unmute', handleUnmute)
|
||||||
|
ref.current.addEventListener('ended', handlePause)
|
||||||
|
ref.current.addEventListener('error', handlePause)
|
||||||
|
ref.current.addEventListener('canplay', handleCanPlay)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
current.removeEventListener('timeupdate', handleTimeUpdate)
|
||||||
|
current.removeEventListener('durationchange', handleDurationChange)
|
||||||
|
current.removeEventListener('play', handlePlay)
|
||||||
|
current.removeEventListener('pause', handlePause)
|
||||||
|
current.removeEventListener('mute', handleMute)
|
||||||
|
current.removeEventListener('unmute', handleUnmute)
|
||||||
|
current.removeEventListener('ended', handlePause)
|
||||||
|
current.removeEventListener('error', handlePause)
|
||||||
|
current.removeEventListener('canplay', handleCanPlay)
|
||||||
|
}
|
||||||
|
}, [ref, playing])
|
||||||
|
|
||||||
|
// to explain why I'm doing this in an effect - if you call play()
|
||||||
|
// before it's loaded, at least on firefox, it'll throw when you then
|
||||||
|
// later go to pause/unload the video. Therefore we should keep
|
||||||
|
// play state outside of the video, and then only call it when
|
||||||
|
// playing === true and canPlay === true
|
||||||
|
useEffect(() => {
|
||||||
if (!ref.current) return
|
if (!ref.current) return
|
||||||
|
|
||||||
// this is how you check if the video is ready to play
|
if (playing) {
|
||||||
if (ref.current.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
|
if (canPlay) {
|
||||||
ref.current.play()
|
ref.current.play()
|
||||||
} else {
|
|
||||||
// TODO: find different solution. can sometimes cause double-playing
|
|
||||||
ref.current.addEventListener('canplay', () => {
|
|
||||||
ref.current?.play()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ref.current.pause()
|
||||||
|
}
|
||||||
|
}, [playing, ref, canPlay])
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
setPlaying(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
if (!ref.current) return
|
setPlaying(false)
|
||||||
ref.current.pause()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePlayPause = () => {
|
const togglePlayPause = () => {
|
||||||
if (!ref.current) return
|
if (playing) {
|
||||||
if (ref.current.paused) {
|
|
||||||
play()
|
play()
|
||||||
} else {
|
} else {
|
||||||
pause()
|
pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {play, pause, togglePlayPause}
|
return {play, pause, togglePlayPause, duration, currentTime, playing, muted}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user