diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-08-07 18:47:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-07 18:47:51 +0100 |
commit | fff2c079c2554861764974aaeeb56f79a25ba82a (patch) | |
tree | 5c5771bcac37f5ae076e56cab78903d18b108366 /src/view/com/util/post-embeds/VideoEmbed.web.tsx | |
parent | b701e8c68c1122bf138575804af41260ec1c436d (diff) | |
download | voidsky-fff2c079c2554861764974aaeeb56f79a25ba82a.tar.zst |
* attempt some sort of "usurping" system * polling-based active video approach * split into inner component again * click to steal active video * disable findAndActivateVideo on native * new intersectionobserver approach - wip * fix types * disable perf optimisation to allow overflow * make active player indicator subtler, clean up video utils * partially fix double-playing * start working on controls * fullscreen API * get buttons working somewhat * rm source from where it shouldn't be * use video elem as source of truth * fix keyboard nav + mute state * new icons, add fullscreen + time + fix play * unmount when far offscreen + round 2dp * listen globally to clicks rather than blur event * move controls to new file * reduce quality when not active * add hover state to buttons * stop propagation of videoplayer click * move around autoplay effects * increase background contrast * add subtitles button * add stopPropagation to root of video player * clean up VideoWebControls * fix chrome * change quality based on focused state * use autoLevelCapping instead of nextLevel * get subtitle track from stream * always use hlsjs * rework hls into a ref * render player earlier, allowing preload * add error boundary * clean up component structure and organisation * rework fullscreen API * disable fullscreen on iPhone * don't play when ready on pause * debounce buffering * simplify giant list of event listeners * update pref * reduce prop drilling * minimise rerenders in `ActiveViewContext` * restore prop drilling --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com> Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view/com/util/post-embeds/VideoEmbed.web.tsx')
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbed.web.tsx | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx new file mode 100644 index 000000000..08932f91f --- /dev/null +++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx @@ -0,0 +1,190 @@ +import React, {useCallback, useEffect, useRef, useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {ErrorBoundary} from '../ErrorBoundary' +import {useActiveVideoView} from './ActiveVideoContext' +import {VideoEmbedInner} from './VideoEmbedInner' +import {HLSUnsupportedError} from './VideoEmbedInner.web' + +export function VideoEmbed({source}: {source: string}) { + const t = useTheme() + const ref = useRef<HTMLDivElement>(null) + const {active, setActive, sendPosition, currentActiveView} = + useActiveVideoView({source}) + const [onScreen, setOnScreen] = useState(false) + + useEffect(() => { + if (!ref.current) return + const observer = new IntersectionObserver( + entries => { + const entry = entries[0] + if (!entry) return + setOnScreen(entry.isIntersecting) + sendPosition( + entry.boundingClientRect.y + entry.boundingClientRect.height / 2, + ) + }, + {threshold: 0.5}, + ) + observer.observe(ref.current) + return () => observer.disconnect() + }, [sendPosition]) + + const [key, setKey] = useState(0) + const renderError = useCallback( + (error: unknown) => ( + <VideoError error={error} retry={() => setKey(key + 1)} /> + ), + [key], + ) + + return ( + <View + style={[ + a.w_full, + {aspectRatio: 16 / 9}, + t.atoms.bg_contrast_25, + a.rounded_sm, + a.my_xs, + ]}> + <div + ref={ref} + style={{display: 'flex', flex: 1, cursor: 'default'}} + onClick={evt => evt.stopPropagation()}> + <ErrorBoundary renderError={renderError} key={key}> + <ViewportObserver + sendPosition={sendPosition} + isAnyViewActive={currentActiveView !== null}> + <VideoEmbedInner + source={source} + active={active} + setActive={setActive} + onScreen={onScreen} + /> + </ViewportObserver> + </ErrorBoundary> + </div> + </View> + ) +} + +/** + * Renders a 100vh tall div and watches it with an IntersectionObserver to + * send the position of the div when it's near the screen. + */ +function ViewportObserver({ + children, + sendPosition, + isAnyViewActive, +}: { + children: React.ReactNode + sendPosition: (position: number) => void + isAnyViewActive?: boolean +}) { + const ref = useRef<HTMLDivElement>(null) + const [nearScreen, setNearScreen] = useState(false) + + // Send position when scrolling. This is done with an IntersectionObserver + // observing a div of 100vh height + useEffect(() => { + if (!ref.current) return + const observer = new IntersectionObserver( + entries => { + const entry = entries[0] + if (!entry) return + const position = + entry.boundingClientRect.y + entry.boundingClientRect.height / 2 + sendPosition(position) + setNearScreen(entry.isIntersecting) + }, + {threshold: Array.from({length: 101}, (_, i) => i / 100)}, + ) + observer.observe(ref.current) + return () => observer.disconnect() + }, [sendPosition]) + + // In case scrolling hasn't started yet, send up the position + useEffect(() => { + if (ref.current && !isAnyViewActive) { + const rect = ref.current.getBoundingClientRect() + const position = rect.y + rect.height / 2 + sendPosition(position) + } + }, [isAnyViewActive, sendPosition]) + + return ( + <View style={[a.flex_1, a.flex_row]}> + {nearScreen && children} + <div + ref={ref} + style={{ + position: 'absolute', + top: 'calc(50% - 50vh)', + left: '50%', + height: '100vh', + width: 1, + pointerEvents: 'none', + }} + /> + </View> + ) +} + +function VideoError({error, retry}: {error: unknown; retry: () => void}) { + const t = useTheme() + const {_} = useLingui() + + const isHLS = error instanceof HLSUnsupportedError + + return ( + <View + style={[ + a.flex_1, + t.atoms.bg_contrast_25, + a.justify_center, + a.align_center, + a.px_lg, + a.border, + t.atoms.border_contrast_low, + a.rounded_sm, + a.gap_lg, + ]}> + <Text + style={[ + a.text_center, + t.atoms.text_contrast_high, + a.text_md, + a.leading_snug, + {maxWidth: 300}, + ]}> + {isHLS ? ( + <Trans> + Your browser does not support the video format. Please try a + different browser. + </Trans> + ) : ( + <Trans> + An error occurred while loading the video. Please try again later. + </Trans> + )} + </Text> + {!isHLS && ( + <Button + onPress={retry} + size="small" + color="secondary_inverted" + variant="solid" + label={_(msg`Retry`)}> + <ButtonText> + <Trans>Retry</Trans> + </ButtonText> + </Button> + )} + </View> + ) +} |