dynamic subtitle positioning so overlays stay inside the video content area
This commit is contained in:
@@ -74,10 +74,34 @@ function parseVTT(raw: string): Cue[] {
|
|||||||
return cues
|
return cues
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getVideoFitOffsets(video: HTMLVideoElement): { top: number; bottom: number } {
|
||||||
|
const rect = video.getBoundingClientRect()
|
||||||
|
const vW = video.videoWidth || 1920
|
||||||
|
const vH = video.videoHeight || 1080
|
||||||
|
const videoAspect = vW / vH
|
||||||
|
const containerAspect = rect.width / rect.height
|
||||||
|
|
||||||
|
let displayHeight: number
|
||||||
|
if (containerAspect > videoAspect) {
|
||||||
|
displayHeight = rect.width / videoAspect
|
||||||
|
} else {
|
||||||
|
displayHeight = rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayTop = rect.top + (rect.height - displayHeight) / 2
|
||||||
|
const displayBottom = displayTop + displayHeight
|
||||||
|
return {
|
||||||
|
top: displayTop,
|
||||||
|
bottom: window.innerHeight - displayBottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function SubtitleOverlay({ playerRef, subtitleUrl }: Props) {
|
export default function SubtitleOverlay({ playerRef, subtitleUrl }: Props) {
|
||||||
const { className, style } = useSubtitleStyles()
|
const { className, style } = useSubtitleStyles()
|
||||||
|
const subtitlePosition = usePreferencesStore(s => s.subtitlePosition)
|
||||||
const cuesRef = useRef<Cue[]>([])
|
const cuesRef = useRef<Cue[]>([])
|
||||||
const [activeCues, setActiveCues] = useState<string[]>([])
|
const [activeCues, setActiveCues] = useState<string[]>([])
|
||||||
|
const [offset, setOffset] = useState<number | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cuesRef.current = []
|
cuesRef.current = []
|
||||||
@@ -142,13 +166,57 @@ export default function SubtitleOverlay({ playerRef, subtitleUrl }: Props) {
|
|||||||
}
|
}
|
||||||
}, [playerRef, subtitleUrl])
|
}, [playerRef, subtitleUrl])
|
||||||
|
|
||||||
|
// Keep subtitles inside the actual video content area (not the black
|
||||||
|
// letterbox bars). In portrait mode object-fit:contain leaves huge black
|
||||||
|
// bars and our old fixed bottom-32 landed deep inside them where Chrome's
|
||||||
|
// video compositor layer clips short DOM overlays.
|
||||||
|
useEffect(() => {
|
||||||
|
const player = playerRef.current
|
||||||
|
const el = (player as any)?.el as HTMLElement | undefined
|
||||||
|
const video = el?.querySelector('video') as HTMLVideoElement | null
|
||||||
|
if (!video) return
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const { top: fitTop, bottom: fitBottom } = getVideoFitOffsets(video)
|
||||||
|
if (subtitlePosition === 'bottom') {
|
||||||
|
setOffset(Math.max(128, fitBottom + 32))
|
||||||
|
} else {
|
||||||
|
setOffset(Math.max(96, fitTop + 32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(update)
|
||||||
|
ro.observe(video)
|
||||||
|
window.addEventListener('resize', update)
|
||||||
|
|
||||||
|
// Also update once metadata is known (videoWidth/videoHeight may be 0
|
||||||
|
// before loadedmetadata fires).
|
||||||
|
video.addEventListener('loadedmetadata', update)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ro.disconnect()
|
||||||
|
window.removeEventListener('resize', update)
|
||||||
|
video.removeEventListener('loadedmetadata', update)
|
||||||
|
}
|
||||||
|
}, [playerRef, subtitlePosition])
|
||||||
|
|
||||||
if (activeCues.length === 0) return null
|
if (activeCues.length === 0) return null
|
||||||
|
|
||||||
|
const posStyle: React.CSSProperties =
|
||||||
|
offset != null
|
||||||
|
? subtitlePosition === 'bottom'
|
||||||
|
? { bottom: offset }
|
||||||
|
: { top: offset }
|
||||||
|
: {}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${className} z-[100]`}
|
className={`${className} z-[100]`}
|
||||||
style={{
|
style={{
|
||||||
...style,
|
...style,
|
||||||
|
...posStyle,
|
||||||
transform: 'translateZ(0)',
|
transform: 'translateZ(0)',
|
||||||
willChange: 'transform',
|
willChange: 'transform',
|
||||||
backfaceVisibility: 'hidden',
|
backfaceVisibility: 'hidden',
|
||||||
|
|||||||
Reference in New Issue
Block a user