diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index 82b2503eb1..b49c49e4ab 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 ( + +
+
+
+ + +
+
+ ) +} + +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 +}) { const hlsRef = useRef(undefined) const [lowQualityFragments, setLowQualityFragments] = useState([]) + // 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 ( - -
-
-
- - -
-
- ) -} - -export class HLSUnsupportedError extends Error { - constructor() { - super('HLS is not supported') - } -} - -export class VideoNotFoundError extends Error { - constructor() { - super('Video not found') - } + return hlsRef }