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'
|
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]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user