attempt some sort of "usurping" system
This commit is contained in:
@@ -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'
|
||||
|
||||
const ActiveVideoContext = React.createContext<{
|
||||
activeViewId: string | null
|
||||
setActiveView: (viewId: string, src: string) => void
|
||||
requestActive: (viewId: string, src: string, y: number) => void
|
||||
allowUsurp: (viewId: string) => void
|
||||
} | null>(null)
|
||||
|
||||
export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
|
||||
const [activeViewId, setActiveViewId] = 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(
|
||||
() => ({
|
||||
activeViewId,
|
||||
setActiveView: (viewId: string, src: string) => {
|
||||
setActiveViewId(viewId)
|
||||
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 (
|
||||
@@ -31,7 +77,7 @@ export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
|
||||
)
|
||||
}
|
||||
|
||||
export function useActiveVideoView() {
|
||||
export function useActiveVideoView(source: string) {
|
||||
const context = React.useContext(ActiveVideoContext)
|
||||
if (!context) {
|
||||
throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
|
||||
@@ -41,8 +87,13 @@ export function useActiveVideoView() {
|
||||
return {
|
||||
active: context.activeViewId === id,
|
||||
setActive: useCallback(
|
||||
(source: string) => context.setActiveView(id, source),
|
||||
[context, id],
|
||||
() => context.setActiveView(id, source),
|
||||
[context, id, source],
|
||||
),
|
||||
requestActive: useCallback(
|
||||
(y: number) => context.requestActive(id, source, y),
|
||||
[context, id, source],
|
||||
),
|
||||
allowUsurp: useCallback(() => context.allowUsurp(id), [context, id]),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import {VideoEmbedInner} from './VideoEmbedInner'
|
||||
|
||||
export function VideoEmbed({source}: {source: string}) {
|
||||
const t = useTheme()
|
||||
const {active, setActive} = useActiveVideoView()
|
||||
const {active, setActive} = useActiveVideoView(source)
|
||||
const {_} = useLingui()
|
||||
|
||||
const onPress = useCallback(() => setActive(source), [setActive, source])
|
||||
const onPress = useCallback(() => setActive(), [setActive])
|
||||
|
||||
return (
|
||||
<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 />
|
||||
}
|
||||
Reference in New Issue
Block a user