diff options
Diffstat (limited to 'src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx')
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx | 225 |
1 files changed, 121 insertions, 104 deletions
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 82b2503eb..b49c49e4a 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {AppBskyEmbedVideo} from '@atproto/api' import Hls, {Events, FragChangedData, Fragment} from 'hls.js' +import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {atoms as a} from '#/alf' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Controls} from './web-controls/VideoControls' @@ -30,9 +31,120 @@ export function VideoEmbedInnerWeb({ throw error } + const hlsRef = useHLS({ + focused, + playlist: embed.playlist, + setHasSubtitleTrack, + setError, + videoRef, + }) + + return ( + <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> + <div ref={containerRef} style={{height: '100%', width: '100%'}}> + <figure style={{margin: 0, position: 'absolute', inset: 0}}> + <video + ref={videoRef} + poster={embed.thumbnail} + style={{width: '100%', height: '100%', objectFit: 'contain'}} + playsInline + preload="none" + muted={!focused} + aria-labelledby={embed.alt ? figId : undefined} + /> + {embed.alt && ( + <figcaption + id={figId} + style={{ + position: 'absolute', + width: 1, + height: 1, + padding: 0, + margin: -1, + overflow: 'hidden', + clip: 'rect(0, 0, 0, 0)', + whiteSpace: 'nowrap', + borderWidth: 0, + }}> + {embed.alt} + </figcaption> + )} + </figure> + <Controls + videoRef={videoRef} + hlsRef={hlsRef} + active={active} + setActive={setActive} + focused={focused} + setFocused={setFocused} + onScreen={onScreen} + fullscreenRef={containerRef} + hasSubtitleTrack={hasSubtitleTrack} + /> + <MediaInsetBorder /> + </div> + </View> + ) +} + +export class HLSUnsupportedError extends Error { + constructor() { + super('HLS is not supported') + } +} + +export class VideoNotFoundError extends Error { + constructor() { + super('Video not found') + } +} + +function useHLS({ + focused, + playlist, + setHasSubtitleTrack, + setError, + videoRef, +}: { + focused: boolean + playlist: string + setHasSubtitleTrack: (v: boolean) => void + setError: (v: Error | null) => void + videoRef: React.RefObject<HTMLVideoElement> +}) { const hlsRef = useRef<Hls | undefined>(undefined) const [lowQualityFragments, setLowQualityFragments] = useState<Fragment[]>([]) + // purge low quality segments from buffer on next frag change + const handleFragChange = useNonReactiveCallback( + (_event: Events.FRAG_CHANGED, {frag}: FragChangedData) => { + if (!hlsRef.current) return + const hls = hlsRef.current + + if (focused && hls.nextAutoLevel > 0) { + // if the current quality level goes above 0, flush the low quality segments + const flushed: Fragment[] = [] + + for (const lowQualFrag of lowQualityFragments) { + // avoid if close to the current fragment + if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { + continue + } + + hls.trigger(Hls.Events.BUFFER_FLUSHING, { + startOffset: lowQualFrag.start, + endOffset: lowQualFrag.end, + type: 'video', + }) + + flushed.push(lowQualFrag) + } + + setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) + } + }, + ) + useEffect(() => { if (!videoRef.current) return if (!Hls.isSupported()) throw new HLSUnsupportedError() @@ -46,7 +158,7 @@ export function VideoEmbedInnerWeb({ hlsRef.current = hls hls.attachMedia(videoRef.current) - hls.loadSource(embed.playlist) + hls.loadSource(playlist) // initial value, later on it's managed by Controls hls.autoLevelCapping = 0 @@ -54,11 +166,12 @@ export function VideoEmbedInnerWeb({ // manually loop, so if we've flushed the first buffer it doesn't get confused const abortController = new AbortController() const {signal} = abortController - videoRef.current.addEventListener( + const videoNode = videoRef.current + videoNode.addEventListener( 'ended', function () { - this.currentTime = 0 - this.play() + videoNode.currentTime = 0 + videoNode.play() }, {signal}, ) @@ -90,111 +203,15 @@ export function VideoEmbedInnerWeb({ } }) + hls.on(Hls.Events.FRAG_CHANGED, handleFragChange) + return () => { hlsRef.current = undefined hls.detachMedia() hls.destroy() abortController.abort() } - }, [embed.playlist]) - - // purge low quality segments from buffer on next frag change - useEffect(() => { - if (!hlsRef.current) return + }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange]) - const current = hlsRef.current - - if (focused) { - function fragChanged( - _event: Events.FRAG_CHANGED, - {frag}: FragChangedData, - ) { - // if the current quality level goes above 0, flush the low quality segments - if (current.nextAutoLevel > 0) { - const flushed: Fragment[] = [] - - for (const lowQualFrag of lowQualityFragments) { - // avoid if close to the current fragment - if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { - return - } - - current.trigger(Hls.Events.BUFFER_FLUSHING, { - startOffset: lowQualFrag.start, - endOffset: lowQualFrag.end, - type: 'video', - }) - - flushed.push(lowQualFrag) - } - - setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) - } - } - current.on(Hls.Events.FRAG_CHANGED, fragChanged) - - return () => { - current.off(Hls.Events.FRAG_CHANGED, fragChanged) - } - } - }, [focused, lowQualityFragments]) - - return ( - <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> - <div ref={containerRef} style={{height: '100%', width: '100%'}}> - <figure style={{margin: 0, position: 'absolute', inset: 0}}> - <video - ref={videoRef} - poster={embed.thumbnail} - style={{width: '100%', height: '100%', objectFit: 'contain'}} - playsInline - preload="none" - muted={!focused} - aria-labelledby={embed.alt ? figId : undefined} - /> - {embed.alt && ( - <figcaption - id={figId} - style={{ - position: 'absolute', - width: 1, - height: 1, - padding: 0, - margin: -1, - overflow: 'hidden', - clip: 'rect(0, 0, 0, 0)', - whiteSpace: 'nowrap', - borderWidth: 0, - }}> - {embed.alt} - </figcaption> - )} - </figure> - <Controls - videoRef={videoRef} - hlsRef={hlsRef} - active={active} - setActive={setActive} - focused={focused} - setFocused={setFocused} - onScreen={onScreen} - fullscreenRef={containerRef} - hasSubtitleTrack={hasSubtitleTrack} - /> - <MediaInsetBorder /> - </div> - </View> - ) -} - -export class HLSUnsupportedError extends Error { - constructor() { - super('HLS is not supported') - } -} - -export class VideoNotFoundError extends Error { - constructor() { - super('Video not found') - } + return hlsRef } |