diff options
Diffstat (limited to 'src/view/com/util/post-embeds/VideoEmbed.tsx')
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbed.tsx | 196 |
1 files changed, 140 insertions, 56 deletions
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index a5bc97f85..d10a6fe69 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,6 +1,7 @@ -import React, {useCallback, useId, useState} from 'react' +import React, {useCallback, useEffect, useId, useState} from 'react' import {View} from 'react-native' import {Image} from 'expo-image' +import {VideoPlayerStatus} from 'expo-video' import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -10,56 +11,40 @@ import {useGate} from '#/lib/statsig/statsig' import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a} from '#/alf' import {Button} from '#/components/Button' +import {Loader} from '#/components/Loader' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' import {ErrorBoundary} from '../ErrorBoundary' import {useActiveVideoNative} from './ActiveVideoNativeContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' -export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { - const {_} = useLingui() - const {activeSource, activeViewId, setActiveSource, player} = - useActiveVideoNative() - const viewId = useId() +interface Props { + embed: AppBskyEmbedVideo.View +} - const [isFullscreen, setIsFullscreen] = React.useState(false) - const isActive = embed.playlist === activeSource && activeViewId === viewId +export function VideoEmbed({embed}: Props) { + const gate = useGate() const [key, setKey] = useState(0) + const renderError = useCallback( (error: unknown) => ( <VideoError error={error} retry={() => setKey(key + 1)} /> ), [key], ) - const gate = useGate() - - const onChangeStatus = (isVisible: boolean) => { - if (isVisible) { - setActiveSource(embed.playlist, viewId) - if (!player.playing) { - player.play() - } - } else if (!isFullscreen) { - player.muted = true - if (player.playing) { - player.pause() - } - } - } - - if (!gate('video_view_on_posts')) { - return null - } let aspectRatio = 16 / 9 - if (embed.aspectRatio) { const {width, height} = embed.aspectRatio aspectRatio = width / height aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) } + if (!gate('video_view_on_posts')) { + return null + } + return ( <View style={[ @@ -71,39 +56,138 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { a.my_xs, ]}> <ErrorBoundary renderError={renderError} key={key}> - <VisibilityView enabled={true} onChangeStatus={onChangeStatus}> - {isActive ? ( - <VideoEmbedInnerNative - embed={embed} - isFullscreen={isFullscreen} - setIsFullscreen={setIsFullscreen} - /> - ) : ( - <> - <Image - source={{uri: embed.thumbnail}} - alt={embed.alt} - style={a.flex_1} - contentFit="cover" - accessibilityIgnoresInvertColors - /> - <Button - style={[a.absolute, a.inset_0]} - onPress={() => { - setActiveSource(embed.playlist, viewId) - }} - label={_(msg`Play video`)} - color="secondary"> - <PlayButtonIcon /> - </Button> - </> - )} - </VisibilityView> + <InnerWrapper embed={embed} /> </ErrorBoundary> </View> ) } +function InnerWrapper({embed}: Props) { + const {_} = useLingui() + const {activeSource, activeViewId, setActiveSource, player} = + useActiveVideoNative() + const viewId = useId() + + const [playerStatus, setPlayerStatus] = useState<VideoPlayerStatus>('loading') + const [isMuted, setIsMuted] = useState(player.muted) + const [isFullscreen, setIsFullscreen] = React.useState(false) + const [timeRemaining, setTimeRemaining] = React.useState(0) + const isActive = embed.playlist === activeSource && activeViewId === viewId + const isLoading = + isActive && + (playerStatus === 'waitingToPlayAtSpecifiedRate' || + playerStatus === 'loading') + + useEffect(() => { + if (isActive) { + // eslint-disable-next-line @typescript-eslint/no-shadow + const volumeSub = player.addListener('volumeChange', ({isMuted}) => { + setIsMuted(isMuted) + }) + const timeSub = player.addListener( + 'timeRemainingChange', + secondsRemaining => { + setTimeRemaining(secondsRemaining) + }, + ) + const statusSub = player.addListener( + 'statusChange', + (status, _oldStatus, error) => { + setPlayerStatus(status) + if (status === 'error') { + throw error + } + }, + ) + return () => { + volumeSub.remove() + timeSub.remove() + statusSub.remove() + } + } + }, [player, isActive]) + + useEffect(() => { + if (!isActive && playerStatus !== 'loading') { + setPlayerStatus('loading') + } + }, [isActive, playerStatus]) + + const onChangeStatus = (isVisible: boolean) => { + if (isFullscreen) { + return + } + + if (isVisible) { + setActiveSource(embed.playlist, viewId) + if (!player.playing) { + player.play() + } + } else { + player.muted = true + if (player.playing) { + player.pause() + } + } + } + + return ( + <VisibilityView enabled={true} onChangeStatus={onChangeStatus}> + {isActive ? ( + <VideoEmbedInnerNative + embed={embed} + timeRemaining={timeRemaining} + isMuted={isMuted} + isFullscreen={isFullscreen} + setIsFullscreen={setIsFullscreen} + /> + ) : null} + {!isActive || isLoading ? ( + <View + style={[ + { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + ]}> + <Image + source={{uri: embed.thumbnail}} + alt={embed.alt} + style={a.flex_1} + contentFit="cover" + accessibilityIgnoresInvertColors + /> + <Button + style={[a.absolute, a.inset_0]} + onPress={() => { + setActiveSource(embed.playlist, viewId) + }} + label={_(msg`Play video`)} + color="secondary"> + {isLoading ? ( + <View + style={[ + a.rounded_full, + a.p_xs, + a.absolute, + {top: 'auto', left: 'auto'}, + {backgroundColor: 'rgba(0,0,0,0.5)'}, + ]}> + <Loader size="2xl" style={{color: 'white'}} /> + </View> + ) : ( + <PlayButtonIcon /> + )} + </Button> + </View> + ) : null} + </VisibilityView> + ) +} + function VideoError({retry}: {error: unknown; retry: () => void}) { return ( <VideoFallback.Container> |