diff options
author | Hailey <me@haileyok.com> | 2024-09-04 16:46:01 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-04 16:46:01 -0700 |
commit | 25566984278d84c28933a4ae2685388734829e01 (patch) | |
tree | 0ad0d99272ff73dbe8333b8559ad452cd16598ef /src | |
parent | 76f493c27958d5e1008a3a6aa0ca7f959cbe330d (diff) | |
download | voidsky-25566984278d84c28933a4ae2685388734829e01.tar.zst |
[Video] Add loading state to player (#5149)
Diffstat (limited to 'src')
-rw-r--r-- | src/components/icons/common.tsx | 1 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbed.tsx | 196 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx | 45 |
3 files changed, 156 insertions, 86 deletions
diff --git a/src/components/icons/common.tsx b/src/components/icons/common.tsx index 387115d3a..e83f96f0b 100644 --- a/src/components/icons/common.tsx +++ b/src/components/icons/common.tsx @@ -19,6 +19,7 @@ export const sizes = { md: 20, lg: 24, xl: 28, + '2xl': 32, } export function useCommonSVGProps(props: Props) { 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> diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx index 4fafce1de..3fa159267 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react' +import React, {useCallback, useRef} from 'react' import {Pressable, View} from 'react-native' import Animated, {FadeInDown} from 'react-native-reanimated' import {VideoPlayer, VideoView} from 'expo-video' @@ -22,10 +22,14 @@ export function VideoEmbedInnerNative({ embed, isFullscreen, setIsFullscreen, + isMuted, + timeRemaining, }: { embed: AppBskyEmbedVideo.View isFullscreen: boolean setIsFullscreen: (isFullscreen: boolean) => void + timeRemaining: number + isMuted: boolean }) { const {_} = useLingui() const {player} = useActiveVideoNative() @@ -73,7 +77,12 @@ export function VideoEmbedInnerNative({ } accessibilityHint="" /> - <VideoControls player={player} enterFullscreen={enterFullscreen} /> + <VideoControls + player={player} + enterFullscreen={enterFullscreen} + isMuted={isMuted} + timeRemaining={timeRemaining} + /> </View> ) } @@ -81,40 +90,16 @@ export function VideoEmbedInnerNative({ function VideoControls({ player, enterFullscreen, + timeRemaining, + isMuted, }: { player: VideoPlayer enterFullscreen: () => void + timeRemaining: number + isMuted: boolean }) { const {_} = useLingui() const t = useTheme() - const [isMuted, setIsMuted] = useState(player.muted) - const [timeRemaining, setTimeRemaining] = React.useState(0) - - useEffect(() => { - // 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) => { - if (status === 'error') { - throw error - } - }, - ) - return () => { - volumeSub.remove() - timeSub.remove() - statusSub.remove() - } - }, [player]) const onPressFullscreen = useCallback(() => { switch (player.status) { |