attempt some sort of "usurping" system

This commit is contained in:
Samuel Newman
2024-07-03 17:25:11 +01:00
parent 8ddb28d3c5
commit 8282c434e0
4 changed files with 158 additions and 59 deletions
@@ -1,25 +1,71 @@
import React, {useCallback, useId, useMemo, useState} from 'react' import React, {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react'
import {VideoPlayerProvider} from './VideoPlayerContext' import {VideoPlayerProvider} from './VideoPlayerContext'
const ActiveVideoContext = React.createContext<{ const ActiveVideoContext = React.createContext<{
activeViewId: string | null activeViewId: string | null
setActiveView: (viewId: string, src: string) => void setActiveView: (viewId: string, src: string) => void
requestActive: (viewId: string, src: string, y: number) => void
allowUsurp: (viewId: string) => void
} | null>(null) } | null>(null)
export function ActiveVideoProvider({children}: {children: React.ReactNode}) { export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
const [activeViewId, setActiveViewId] = useState<string | null>(null) const [activeViewId, setActiveViewId] = useState<string | null>(null)
const [source, setSource] = useState<string | null>(null) const [source, setSource] = useState<string | null>(null)
const [canBeUsurped, setCanBeUsurped] = useState(false)
const requestedViews = useRef<{[key: string]: {src: string; y: number}}>({})
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
useEffect(() => {
return () => {
clearTimeout(timeoutRef.current)
}
}, [])
const value = useMemo( const value = useMemo(
() => ({ () => ({
activeViewId, activeViewId,
setActiveView: (viewId: string, src: string) => { setActiveView: (viewId: string, src: string) => {
setActiveViewId(viewId) setActiveViewId(viewId)
setSource(src) setSource(src)
setCanBeUsurped(false)
},
requestActive: (viewId: string, src: string, y: number) => {
if (activeViewId && !canBeUsurped) {
return
}
if (requestedViews.current[viewId]) {
requestedViews.current[viewId].y = y
return
}
clearTimeout(timeoutRef.current)
requestedViews.current[viewId] = {src, y}
timeoutRef.current = setTimeout(() => {
const sortedViews = Object.entries(requestedViews.current).sort(
([, {y: y1}], [, {y: y2}]) => y1 - y2,
)
const [topViewId, {src: topSrc}] = sortedViews[0]
setActiveViewId(topViewId)
setSource(topSrc)
setCanBeUsurped(false)
requestedViews.current = {}
}, 100)
},
allowUsurp: (viewId: string) => {
if (activeViewId === viewId) {
setCanBeUsurped(true)
}
}, },
}), }),
[activeViewId], [activeViewId, canBeUsurped],
) )
return ( return (
@@ -31,7 +77,7 @@ export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
) )
} }
export function useActiveVideoView() { export function useActiveVideoView(source: string) {
const context = React.useContext(ActiveVideoContext) const context = React.useContext(ActiveVideoContext)
if (!context) { if (!context) {
throw new Error('useActiveVideo must be used within a ActiveVideoProvider') throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
@@ -41,8 +87,13 @@ export function useActiveVideoView() {
return { return {
active: context.activeViewId === id, active: context.activeViewId === id,
setActive: useCallback( setActive: useCallback(
(source: string) => context.setActiveView(id, source), () => context.setActiveView(id, source),
[context, id], [context, id, source],
), ),
requestActive: useCallback(
(y: number) => context.requestActive(id, source, y),
[context, id, source],
),
allowUsurp: useCallback(() => context.allowUsurp(id), [context, id]),
} }
} }
+2 -2
View File
@@ -11,10 +11,10 @@ import {VideoEmbedInner} from './VideoEmbedInner'
export function VideoEmbed({source}: {source: string}) { export function VideoEmbed({source}: {source: string}) {
const t = useTheme() const t = useTheme()
const {active, setActive} = useActiveVideoView() const {active, setActive} = useActiveVideoView(source)
const {_} = useLingui() const {_} = useLingui()
const onPress = useCallback(() => setActive(source), [setActive, source]) const onPress = useCallback(() => setActive(), [setActive])
return ( return (
<View <View
@@ -0,0 +1,100 @@
import React, {useEffect, useRef, useState} from 'react'
import {View} from 'react-native'
import Hls from 'hls.js'
import {atoms as a, useTheme} from '#/alf'
import {useActiveVideoView} from './ActiveVideoContext'
export function VideoEmbed({source}: {source: string}) {
const [hls] = useState(() => new Hls())
const hasLoaded = useRef(false)
const {active, setActive, requestActive, allowUsurp} =
useActiveVideoView(source)
const ref = useRef<HTMLVideoElement>(null)
const t = useTheme()
// Use HLS.js to play HLS video
useEffect(() => {
if (ref.current && active && !hasLoaded.current) {
hasLoaded.current = true
if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
ref.current.src = source
} else if (Hls.isSupported()) {
hls.loadSource(source)
hls.attachMedia(ref.current)
} else {
// TODO: fallback
}
}
}, [source, active, hls])
useEffect(() => {
return () => {
hls.destroy()
}
}, [hls])
useEffect(() => {
if (ref.current) {
const observer = new IntersectionObserver(
([entry]) => {
if (ref.current) {
if (entry.intersectionRatio > 0.25) {
if (active) {
if (ref.current.paused) {
ref.current.play()
}
} else {
requestActive(entry.boundingClientRect.y)
}
} else {
if (active) {
if (!ref.current.paused) {
ref.current.pause()
}
}
}
if (entry.intersectionRatio < 0.75) {
allowUsurp()
}
}
},
{threshold: [0, 0.25, 1]},
)
observer.observe(ref.current)
return () => {
observer.disconnect()
}
}
}, [active, requestActive, allowUsurp])
return (
<View
style={[
a.w_full,
a.rounded_sm,
{aspectRatio: 16 / 9},
a.overflow_hidden,
active ? t.atoms.bg_contrast_25 : t.atoms.bg_contrast_200,
a.my_xs,
]}>
<video
ref={ref}
style={a.flex_1}
playsInline
preload="none"
loop
muted
autoPlay={active}
onClick={() => {
if (!active) {
setActive()
}
}}
/>
</View>
)
}
@@ -1,52 +0,0 @@
import React, {useEffect, useRef} from 'react'
import Hls from 'hls.js'
import {atoms as a} from '#/alf'
export const VideoEmbedInner = ({source}: {source: string}) => {
const ref = useRef<HTMLVideoElement>(null)
// Use HLS.js to play HLS video
useEffect(() => {
if (ref.current) {
if (ref.current.canPlayType('application/vnd.apple.mpegurl')) {
ref.current.src = source
} else if (Hls.isSupported()) {
var hls = new Hls()
hls.loadSource(source)
hls.attachMedia(ref.current)
} else {
// TODO: fallback
}
}
}, [source])
useEffect(() => {
if (ref.current) {
const observer = new IntersectionObserver(
([entry]) => {
if (ref.current) {
if (entry.isIntersecting) {
if (ref.current.paused) {
ref.current.play()
}
} else {
if (!ref.current.paused) {
ref.current.pause()
}
}
}
},
{threshold: 0},
)
observer.observe(ref.current)
return () => {
observer.disconnect()
}
}
}, [])
return <video ref={ref} style={a.flex_1} controls playsInline autoPlay loop />
}