···11-import React from 'react'
22-import {useVideoPlayer, VideoPlayer} from 'expo-video'
33-44-import {isAndroid, isNative} from '#/platform/detection'
55-66-const Context = React.createContext<{
77- activeSource: string
88- activeViewId: string | undefined
99- setActiveSource: (src: string | null, viewId: string | null) => void
1010- player: VideoPlayer
1111-} | null>(null)
1212-1313-export function Provider({children}: {children: React.ReactNode}) {
1414- if (!isNative) {
1515- throw new Error('ActiveVideoProvider may only be used on native.')
1616- }
1717-1818- const [activeSource, setActiveSource] = React.useState('')
1919- const [activeViewId, setActiveViewId] = React.useState<string>()
2020-2121- const player = useVideoPlayer(activeSource, p => {
2222- p.muted = true
2323- p.loop = true
2424- // We want to immediately call `play` so we get the loading state
2525- p.play()
2626- })
2727-2828- const setActiveSourceOuter = (src: string | null, viewId: string | null) => {
2929- // HACK
3030- // expo-video doesn't like it when you try and move a `player` to another `VideoView`. Instead, we need to actually
3131- // unregister that player to let the new screen register it. This is only a problem on Android, so we only need to
3232- // apply it there.
3333- if (src === activeSource && isAndroid) {
3434- setActiveSource('')
3535- setTimeout(() => {
3636- setActiveSource(src ? src : '')
3737- }, 100)
3838- } else {
3939- setActiveSource(src ? src : '')
4040- }
4141- setActiveViewId(viewId ? viewId : '')
4242- }
4343-4444- return (
4545- <Context.Provider
4646- value={{
4747- activeSource,
4848- setActiveSource: setActiveSourceOuter,
4949- activeViewId,
5050- player,
5151- }}>
5252- {children}
5353- </Context.Provider>
5454- )
5555-}
5656-5757-export function useActiveVideoNative() {
5858- const context = React.useContext(Context)
5959- if (!context) {
6060- throw new Error(
6161- 'useActiveVideoNative must be used within a ActiveVideoNativeProvider',
6262- )
6363- }
6464- return context
6565-}
+31-111
src/view/com/util/post-embeds/VideoEmbed.tsx
···11-import React, {useCallback, useEffect, useId, useState} from 'react'
11+import React, {useCallback, useState} from 'react'
22import {View} from 'react-native'
33import {ImageBackground} from 'expo-image'
44-import {PlayerError, VideoPlayerStatus} from 'expo-video'
54import {AppBskyEmbedVideo} from '@atproto/api'
65import {msg, Trans} from '@lingui/macro'
76import {useLingui} from '@lingui/react'
8798import {clamp} from '#/lib/numbers'
1010-import {useAutoplayDisabled} from 'state/preferences'
119import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
1210import {atoms as a} from '#/alf'
1311import {Button} from '#/components/Button'
1414-import {useIsWithinMessage} from '#/components/dms/MessageContext'
1212+import {useThrottledValue} from '#/components/hooks/useThrottledValue'
1513import {Loader} from '#/components/Loader'
1614import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
1717-import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
1815import {ErrorBoundary} from '../ErrorBoundary'
1919-import {useActiveVideoNative} from './ActiveVideoNativeContext'
2016import * as VideoFallback from './VideoEmbedInner/VideoFallback'
21172218interface Props {
···59556056function InnerWrapper({embed}: Props) {
6157 const {_} = useLingui()
6262- const {activeSource, activeViewId, setActiveSource, player} =
6363- useActiveVideoNative()
6464- const viewId = useId()
6565-6666- const [playerStatus, setPlayerStatus] = useState<
6767- VideoPlayerStatus | 'paused'
6868- >('paused')
6969- const [isMuted, setIsMuted] = useState(player.muted)
7070- const [isFullscreen, setIsFullscreen] = React.useState(false)
7171- const [timeRemaining, setTimeRemaining] = React.useState(0)
7272- const isWithinMessage = useIsWithinMessage()
7373- const disableAutoplay = useAutoplayDisabled() || isWithinMessage
7474- const isActive = embed.playlist === activeSource && activeViewId === viewId
7575- // There are some different loading states that we should pay attention to and show a spinner for
7676- const isLoading =
7777- isActive &&
7878- (playerStatus === 'waitingToPlayAtSpecifiedRate' ||
7979- playerStatus === 'loading')
8080- // This happens whenever the visibility view decides that another video should start playing
8181- const showOverlay = !isActive || isLoading || playerStatus === 'paused'
5858+ const ref = React.useRef<{togglePlayback: () => void}>(null)
82598383- // send error up to error boundary
8484- const [error, setError] = useState<Error | PlayerError | null>(null)
8585- if (error) {
8686- throw error
8787- }
8888-8989- useEffect(() => {
9090- if (isActive) {
9191- // eslint-disable-next-line @typescript-eslint/no-shadow
9292- const volumeSub = player.addListener('volumeChange', ({isMuted}) => {
9393- setIsMuted(isMuted)
9494- })
9595- const timeSub = player.addListener(
9696- 'timeRemainingChange',
9797- secondsRemaining => {
9898- setTimeRemaining(secondsRemaining)
9999- },
100100- )
101101- const statusSub = player.addListener(
102102- 'statusChange',
103103- (status, oldStatus, playerError) => {
104104- setPlayerStatus(status)
105105- if (status === 'error') {
106106- setError(playerError ?? new Error('Unknown player error'))
107107- }
108108- if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') {
109109- player.play()
110110- }
111111- },
112112- )
113113- return () => {
114114- volumeSub.remove()
115115- timeSub.remove()
116116- statusSub.remove()
117117- }
118118- }
119119- }, [player, isActive, disableAutoplay])
120120-121121- // The source might already be active (for example, if you are scrolling a list of quotes and its all the same
122122- // video). In those cases, just start playing. Otherwise, setting the active source will result in the video
123123- // start playback immediately
124124- const startPlaying = (ignoreAutoplayPreference: boolean) => {
125125- if (disableAutoplay && !ignoreAutoplayPreference) {
126126- return
127127- }
6060+ const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
6161+ 'pending',
6262+ )
6363+ const [isLoading, setIsLoading] = React.useState(false)
6464+ const [isActive, setIsActive] = React.useState(false)
6565+ const showSpinner = useThrottledValue(isActive && isLoading, 100)
12866129129- if (isActive) {
130130- player.play()
131131- } else {
132132- setActiveSource(embed.playlist, viewId)
133133- }
134134- }
6767+ const showOverlay =
6868+ !isActive ||
6969+ isLoading ||
7070+ (status === 'paused' && !isActive) ||
7171+ status === 'pending'
13572136136- const onVisibilityStatusChange = (isVisible: boolean) => {
137137- // When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change
138138- // events
139139- if (isFullscreen) {
140140- return
7373+ React.useEffect(() => {
7474+ if (!isActive && status !== 'pending') {
7575+ setStatus('pending')
14176 }
142142- if (isVisible) {
143143- startPlaying(false)
144144- } else {
145145- // Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted
146146- // until it gets replaced by another video
147147- if (disableAutoplay) {
148148- setActiveSource(null, null)
149149- } else {
150150- player.muted = true
151151- if (player.playing) {
152152- player.pause()
153153- }
154154- }
155155- }
156156- }
7777+ }, [isActive, status])
1577815879 return (
159159- <VisibilityView enabled={true} onChangeStatus={onVisibilityStatusChange}>
160160- {isActive ? (
161161- <VideoEmbedInnerNative
162162- embed={embed}
163163- timeRemaining={timeRemaining}
164164- isMuted={isMuted}
165165- isFullscreen={isFullscreen}
166166- setIsFullscreen={setIsFullscreen}
167167- />
168168- ) : null}
8080+ <>
8181+ <VideoEmbedInnerNative
8282+ embed={embed}
8383+ setStatus={setStatus}
8484+ setIsLoading={setIsLoading}
8585+ setIsActive={setIsActive}
8686+ ref={ref}
8787+ />
16988 <ImageBackground
17089 source={{uri: embed.thumbnail}}
17190 accessibilityIgnoresInvertColors
···185104 >
186105 <Button
187106 style={[a.flex_1, a.align_center, a.justify_center]}
188188- onPress={() => startPlaying(true)}
107107+ onPress={() => {
108108+ ref.current?.togglePlayback()
109109+ }}
189110 label={_(msg`Play video`)}
190111 color="secondary">
191191- {isLoading ? (
112112+ {showSpinner ? (
192113 <View
193114 style={[
194115 a.rounded_full,
195116 a.p_xs,
196117 a.align_center,
197118 a.justify_center,
198198- {backgroundColor: 'rgba(0,0,0,0.5)'},
199119 ]}>
200120 <Loader size="2xl" style={{color: 'white'}} />
201121 </View>
···204124 )}
205125 </Button>
206126 </ImageBackground>
207207- </VisibilityView>
127127+ </>
208128 )
209129}
210130