use %, extract out to function

This commit is contained in:
Samuel Newman
2026-03-06 14:20:25 +02:00
parent d8d293cbe6
commit afb570d99e
2 changed files with 53 additions and 31 deletions
@@ -1,4 +1,4 @@
import {useEffect, useId, useRef, useState} from 'react' import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api' import {type AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -37,7 +37,7 @@ export function VideoEmbedInnerWeb({
throw error throw error
} }
const {hlsRef, loop, cueLineRef} = useHLS({ const {hlsRef, loop, updateCuePositions} = useHLS({
playlist: embed.playlist, playlist: embed.playlist,
setHasSubtitleTrack, setHasSubtitleTrack,
setError, setError,
@@ -90,7 +90,7 @@ export function VideoEmbedInnerWeb({
hasSubtitleTrack={hasSubtitleTrack} hasSubtitleTrack={hasSubtitleTrack}
isGif={embed.presentation === 'gif'} isGif={embed.presentation === 'gif'}
altText={embed.alt} altText={embed.alt}
cueLineRef={cueLineRef} updateCuePositions={updateCuePositions}
/> />
</div> </div>
</View> </View>
@@ -146,7 +146,47 @@ function useHLS({
}, [Hls, setHlsLoading]) }, [Hls, setHlsLoading])
const hlsRef = useRef<HlsTypes.default | undefined>(undefined) const hlsRef = useRef<HlsTypes.default | undefined>(undefined)
const cueLineRef = useRef<number>(-2) const controlsVisibleRef = useRef(false)
/**
* Repositions VTT subtitle cues using percentage-based line values
* (snapToLines=false) so that multi-line/wrapped cues grow upward
* instead of extending offscreen. Moves cues higher when controls
* are visible to avoid occlusion by the scrub bar.
*
* Called from two sites:
* - SUBTITLE_FRAG_PROCESSED: applies positioning to newly loaded cues
* - VideoControls effect: updates positioning when controls show/hide
*/
const updateCuePositions = useCallback(
(controlsVisible?: boolean) => {
if (controlsVisible != null) {
// save controlsVisible state so that when it's called from SUBTITLE_FRAG_PROCESSED,
// the most recent value is used (as we won't know the control state there)
controlsVisibleRef.current = controlsVisible
}
// magic numbers: cue position, % from top of video
const line = controlsVisibleRef.current ? 70 : 85
const video = videoRef.current
if (!video) return
for (let i = 0; i < video.textTracks.length; i++) {
const track = video.textTracks[i]
if (track.cues) {
for (let j = 0; j < track.cues.length; j++) {
const cue = track.cues[j] as VTTCue
cue.snapToLines = false
cue.line = line
}
}
// toggle track mode to force the browser to re-render active cues
if (track.mode === 'showing') {
track.mode = 'hidden'
track.mode = 'showing'
}
}
},
[videoRef],
)
const [lowQualityFragments, setLowQualityFragments] = useState< const [lowQualityFragments, setLowQualityFragments] = useState<
HlsTypes.Fragment[] HlsTypes.Fragment[]
>([]) >([])
@@ -222,12 +262,8 @@ function useHLS({
} }
}) })
hls.on(Hls.Events.CUES_PARSED, (_event, data) => { hls.on(Hls.Events.SUBTITLE_FRAG_PROCESSED, () => {
if (Array.isArray(data.cues)) { updateCuePositions()
for (const cue of data.cues) {
cue.line = cueLineRef.current
}
}
}) })
hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => { hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
@@ -317,6 +353,6 @@ function useHLS({
return { return {
hlsRef, hlsRef,
loop: !hasLowQualityFragmentAtStart, loop: !hasLowQualityFragmentAtStart,
cueLineRef, updateCuePositions,
} }
} }
@@ -48,7 +48,7 @@ export function Controls({
hasSubtitleTrack, hasSubtitleTrack,
isGif, isGif,
altText, altText,
cueLineRef, updateCuePositions,
}: { }: {
videoRef: React.RefObject<HTMLVideoElement | null> videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null> hlsRef: React.RefObject<Hls | undefined | null>
@@ -62,7 +62,7 @@ export function Controls({
hasSubtitleTrack: boolean hasSubtitleTrack: boolean
isGif: boolean isGif: boolean
altText?: string altText?: string
cueLineRef: React.RefObject<number> updateCuePositions: (controlsVisible?: boolean) => void
}) { }) {
const { const {
play, play,
@@ -297,25 +297,11 @@ export function Controls({
(interactingViaKeypress ? hasFocus : hovered) (interactingViaKeypress ? hasFocus : hovered)
// adjust subtitle cue positioning to avoid occlusion by controls // adjust subtitle cue positioning to avoid occlusion by controls
const cueLine = showControls ? -4 : -2 // uses percentage-based positioning (snapToLines=false) so wrapped
// multi-line cues grow upward instead of extending offscreen
useEffect(() => { useEffect(() => {
cueLineRef.current = cueLine updateCuePositions(showControls)
const video = videoRef.current }, [showControls, updateCuePositions])
if (!video) return
for (let i = 0; i < video.textTracks.length; i++) {
const track = video.textTracks[i]
if (track.cues) {
for (let j = 0; j < track.cues.length; j++) {
;(track.cues[j] as VTTCue).line = cueLine
}
}
// toggle track mode to force the browser to re-render active cues
if (track.mode === 'showing') {
track.mode = 'hidden'
track.mode = 'showing'
}
}
}, [cueLine, videoRef, cueLineRef])
if (isGif) { if (isGif) {
return ( return (