Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[Video] Refactor HLS logic (#5468)

* Extract HLS interop into useHLS

* Rename variable

* Move flushing outside an effect

* use continue instead of return

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

authored by

dan
Samuel Newman
and committed by
GitHub
ddaf2c62 4f021740

+121 -104
+121 -104
src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx
··· 3 3 import {AppBskyEmbedVideo} from '@atproto/api' 4 4 import Hls, {Events, FragChangedData, Fragment} from 'hls.js' 5 5 6 + import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 6 7 import {atoms as a} from '#/alf' 7 8 import {MediaInsetBorder} from '#/components/MediaInsetBorder' 8 9 import {Controls} from './web-controls/VideoControls' ··· 30 31 throw error 31 32 } 32 33 34 + const hlsRef = useHLS({ 35 + focused, 36 + playlist: embed.playlist, 37 + setHasSubtitleTrack, 38 + setError, 39 + videoRef, 40 + }) 41 + 42 + return ( 43 + <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> 44 + <div ref={containerRef} style={{height: '100%', width: '100%'}}> 45 + <figure style={{margin: 0, position: 'absolute', inset: 0}}> 46 + <video 47 + ref={videoRef} 48 + poster={embed.thumbnail} 49 + style={{width: '100%', height: '100%', objectFit: 'contain'}} 50 + playsInline 51 + preload="none" 52 + muted={!focused} 53 + aria-labelledby={embed.alt ? figId : undefined} 54 + /> 55 + {embed.alt && ( 56 + <figcaption 57 + id={figId} 58 + style={{ 59 + position: 'absolute', 60 + width: 1, 61 + height: 1, 62 + padding: 0, 63 + margin: -1, 64 + overflow: 'hidden', 65 + clip: 'rect(0, 0, 0, 0)', 66 + whiteSpace: 'nowrap', 67 + borderWidth: 0, 68 + }}> 69 + {embed.alt} 70 + </figcaption> 71 + )} 72 + </figure> 73 + <Controls 74 + videoRef={videoRef} 75 + hlsRef={hlsRef} 76 + active={active} 77 + setActive={setActive} 78 + focused={focused} 79 + setFocused={setFocused} 80 + onScreen={onScreen} 81 + fullscreenRef={containerRef} 82 + hasSubtitleTrack={hasSubtitleTrack} 83 + /> 84 + <MediaInsetBorder /> 85 + </div> 86 + </View> 87 + ) 88 + } 89 + 90 + export class HLSUnsupportedError extends Error { 91 + constructor() { 92 + super('HLS is not supported') 93 + } 94 + } 95 + 96 + export class VideoNotFoundError extends Error { 97 + constructor() { 98 + super('Video not found') 99 + } 100 + } 101 + 102 + function useHLS({ 103 + focused, 104 + playlist, 105 + setHasSubtitleTrack, 106 + setError, 107 + videoRef, 108 + }: { 109 + focused: boolean 110 + playlist: string 111 + setHasSubtitleTrack: (v: boolean) => void 112 + setError: (v: Error | null) => void 113 + videoRef: React.RefObject<HTMLVideoElement> 114 + }) { 33 115 const hlsRef = useRef<Hls | undefined>(undefined) 34 116 const [lowQualityFragments, setLowQualityFragments] = useState<Fragment[]>([]) 35 117 118 + // purge low quality segments from buffer on next frag change 119 + const handleFragChange = useNonReactiveCallback( 120 + (_event: Events.FRAG_CHANGED, {frag}: FragChangedData) => { 121 + if (!hlsRef.current) return 122 + const hls = hlsRef.current 123 + 124 + if (focused && hls.nextAutoLevel > 0) { 125 + // if the current quality level goes above 0, flush the low quality segments 126 + const flushed: Fragment[] = [] 127 + 128 + for (const lowQualFrag of lowQualityFragments) { 129 + // avoid if close to the current fragment 130 + if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { 131 + continue 132 + } 133 + 134 + hls.trigger(Hls.Events.BUFFER_FLUSHING, { 135 + startOffset: lowQualFrag.start, 136 + endOffset: lowQualFrag.end, 137 + type: 'video', 138 + }) 139 + 140 + flushed.push(lowQualFrag) 141 + } 142 + 143 + setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) 144 + } 145 + }, 146 + ) 147 + 36 148 useEffect(() => { 37 149 if (!videoRef.current) return 38 150 if (!Hls.isSupported()) throw new HLSUnsupportedError() ··· 46 158 hlsRef.current = hls 47 159 48 160 hls.attachMedia(videoRef.current) 49 - hls.loadSource(embed.playlist) 161 + hls.loadSource(playlist) 50 162 51 163 // initial value, later on it's managed by Controls 52 164 hls.autoLevelCapping = 0 ··· 54 166 // manually loop, so if we've flushed the first buffer it doesn't get confused 55 167 const abortController = new AbortController() 56 168 const {signal} = abortController 57 - videoRef.current.addEventListener( 169 + const videoNode = videoRef.current 170 + videoNode.addEventListener( 58 171 'ended', 59 172 function () { 60 - this.currentTime = 0 61 - this.play() 173 + videoNode.currentTime = 0 174 + videoNode.play() 62 175 }, 63 176 {signal}, 64 177 ) ··· 90 203 } 91 204 }) 92 205 206 + hls.on(Hls.Events.FRAG_CHANGED, handleFragChange) 207 + 93 208 return () => { 94 209 hlsRef.current = undefined 95 210 hls.detachMedia() 96 211 hls.destroy() 97 212 abortController.abort() 98 213 } 99 - }, [embed.playlist]) 100 - 101 - // purge low quality segments from buffer on next frag change 102 - useEffect(() => { 103 - if (!hlsRef.current) return 104 - 105 - const current = hlsRef.current 106 - 107 - if (focused) { 108 - function fragChanged( 109 - _event: Events.FRAG_CHANGED, 110 - {frag}: FragChangedData, 111 - ) { 112 - // if the current quality level goes above 0, flush the low quality segments 113 - if (current.nextAutoLevel > 0) { 114 - const flushed: Fragment[] = [] 115 - 116 - for (const lowQualFrag of lowQualityFragments) { 117 - // avoid if close to the current fragment 118 - if (Math.abs(frag.start - lowQualFrag.start) < 0.1) { 119 - return 120 - } 121 - 122 - current.trigger(Hls.Events.BUFFER_FLUSHING, { 123 - startOffset: lowQualFrag.start, 124 - endOffset: lowQualFrag.end, 125 - type: 'video', 126 - }) 127 - 128 - flushed.push(lowQualFrag) 129 - } 130 - 131 - setLowQualityFragments(prev => prev.filter(f => !flushed.includes(f))) 132 - } 133 - } 134 - current.on(Hls.Events.FRAG_CHANGED, fragChanged) 135 - 136 - return () => { 137 - current.off(Hls.Events.FRAG_CHANGED, fragChanged) 138 - } 139 - } 140 - }, [focused, lowQualityFragments]) 141 - 142 - return ( 143 - <View style={[a.flex_1, a.rounded_md, a.overflow_hidden]}> 144 - <div ref={containerRef} style={{height: '100%', width: '100%'}}> 145 - <figure style={{margin: 0, position: 'absolute', inset: 0}}> 146 - <video 147 - ref={videoRef} 148 - poster={embed.thumbnail} 149 - style={{width: '100%', height: '100%', objectFit: 'contain'}} 150 - playsInline 151 - preload="none" 152 - muted={!focused} 153 - aria-labelledby={embed.alt ? figId : undefined} 154 - /> 155 - {embed.alt && ( 156 - <figcaption 157 - id={figId} 158 - style={{ 159 - position: 'absolute', 160 - width: 1, 161 - height: 1, 162 - padding: 0, 163 - margin: -1, 164 - overflow: 'hidden', 165 - clip: 'rect(0, 0, 0, 0)', 166 - whiteSpace: 'nowrap', 167 - borderWidth: 0, 168 - }}> 169 - {embed.alt} 170 - </figcaption> 171 - )} 172 - </figure> 173 - <Controls 174 - videoRef={videoRef} 175 - hlsRef={hlsRef} 176 - active={active} 177 - setActive={setActive} 178 - focused={focused} 179 - setFocused={setFocused} 180 - onScreen={onScreen} 181 - fullscreenRef={containerRef} 182 - hasSubtitleTrack={hasSubtitleTrack} 183 - /> 184 - <MediaInsetBorder /> 185 - </div> 186 - </View> 187 - ) 188 - } 189 - 190 - export class HLSUnsupportedError extends Error { 191 - constructor() { 192 - super('HLS is not supported') 193 - } 194 - } 214 + }, [playlist, setError, setHasSubtitleTrack, videoRef, handleFragChange]) 195 215 196 - export class VideoNotFoundError extends Error { 197 - constructor() { 198 - super('Video not found') 199 - } 216 + return hlsRef 200 217 }