[Video] Add disable autoplay for native, more tweaking (#5178)

zio/stable
Hailey 2024-09-06 09:31:01 -07:00 committed by GitHub
parent bdff8752fb
commit 60182cd874
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 84 additions and 63 deletions

View File

@ -1,17 +1,20 @@
import React from 'react' import React from 'react'
export const useDedupe = () => { export const useDedupe = (timeout = 250) => {
const canDo = React.useRef(true) const canDo = React.useRef(true)
return React.useCallback((cb: () => unknown) => { return React.useCallback(
(cb: () => unknown) => {
if (canDo.current) { if (canDo.current) {
canDo.current = false canDo.current = false
setTimeout(() => { setTimeout(() => {
canDo.current = true canDo.current = true
}, 250) }, timeout)
cb() cb()
return true return true
} }
return false return false
}, []) },
[timeout],
)
} }

View File

@ -7,6 +7,7 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useScrollHandlers} from '#/lib/ScrollContext' import {useScrollHandlers} from '#/lib/ScrollContext'
import {useDedupe} from 'lib/hooks/useDedupe' import {useDedupe} from 'lib/hooks/useDedupe'
import {addStyle} from 'lib/styles' import {addStyle} from 'lib/styles'
import {isIOS} from 'platform/detection'
import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView' import {updateActiveViewAsync} from '../../../../modules/expo-bluesky-swiss-army/src/VisibilityView'
import {FlatList_INTERNAL} from './Views' import {FlatList_INTERNAL} from './Views'
@ -49,7 +50,7 @@ function ListImpl<ItemT>(
) { ) {
const isScrolledDown = useSharedValue(false) const isScrolledDown = useSharedValue(false)
const pal = usePalette('default') const pal = usePalette('default')
const dedupe = useDedupe() const dedupe = useDedupe(400)
function handleScrolledDownChange(didScrollDown: boolean) { function handleScrolledDownChange(didScrollDown: boolean) {
onScrolledDownChange?.(didScrollDown) onScrolledDownChange?.(didScrollDown)
@ -68,6 +69,7 @@ function ListImpl<ItemT>(
onBeginDragFromContext?.(e, ctx) onBeginDragFromContext?.(e, ctx)
}, },
onEndDrag(e, ctx) { onEndDrag(e, ctx) {
runOnJS(updateActiveViewAsync)()
onEndDragFromContext?.(e, ctx) onEndDragFromContext?.(e, ctx)
}, },
onScroll(e, ctx) { onScroll(e, ctx) {
@ -81,11 +83,14 @@ function ListImpl<ItemT>(
} }
} }
if (isIOS) {
runOnJS(dedupe)(updateActiveViewAsync) runOnJS(dedupe)(updateActiveViewAsync)
}
}, },
// Note: adding onMomentumBegin here makes simulator scroll // Note: adding onMomentumBegin here makes simulator scroll
// lag on Android. So either don't add it, or figure out why. // lag on Android. So either don't add it, or figure out why.
onMomentumEnd(e, ctx) { onMomentumEnd(e, ctx) {
runOnJS(updateActiveViewAsync)()
onMomentumEndFromContext?.(e, ctx) onMomentumEndFromContext?.(e, ctx)
}, },
}) })

View File

@ -6,7 +6,7 @@ import {isNative} from '#/platform/detection'
const Context = React.createContext<{ const Context = React.createContext<{
activeSource: string activeSource: string
activeViewId: string | undefined activeViewId: string | undefined
setActiveSource: (src: string, viewId: string) => void setActiveSource: (src: string | null, viewId: string | null) => void
player: VideoPlayer player: VideoPlayer
} | null>(null) } | null>(null)
@ -21,12 +21,13 @@ export function Provider({children}: {children: React.ReactNode}) {
const player = useVideoPlayer(activeSource, p => { const player = useVideoPlayer(activeSource, p => {
p.muted = true p.muted = true
p.loop = true p.loop = true
// We want to immediately call `play` so we get the loading state
p.play() p.play()
}) })
const setActiveSourceOuter = (src: string, viewId: string) => { const setActiveSourceOuter = (src: string | null, viewId: string | null) => {
setActiveSource(src) setActiveSource(src ? src : '')
setActiveViewId(viewId) setActiveViewId(viewId ? viewId : '')
} }
return ( return (

View File

@ -1,6 +1,6 @@
import React, {useCallback, useEffect, useId, useState} from 'react' import React, {useCallback, useEffect, useId, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Image} from 'expo-image' import {ImageBackground} from 'expo-image'
import {PlayerError, VideoPlayerStatus} from 'expo-video' import {PlayerError, VideoPlayerStatus} from 'expo-video'
import {AppBskyEmbedVideo} from '@atproto/api' import {AppBskyEmbedVideo} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
import {clamp} from '#/lib/numbers' import {clamp} from '#/lib/numbers'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
import {useAutoplayDisabled} from 'state/preferences'
import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
@ -69,18 +70,20 @@ function InnerWrapper({embed}: Props) {
const viewId = useId() const viewId = useId()
const [playerStatus, setPlayerStatus] = useState< const [playerStatus, setPlayerStatus] = useState<
VideoPlayerStatus | 'switching' VideoPlayerStatus | 'paused'
>('loading') >(player.playing ? 'readyToPlay' : 'paused')
const [isMuted, setIsMuted] = useState(player.muted) const [isMuted, setIsMuted] = useState(player.muted)
const [isFullscreen, setIsFullscreen] = React.useState(false) const [isFullscreen, setIsFullscreen] = React.useState(false)
const [timeRemaining, setTimeRemaining] = React.useState(0) const [timeRemaining, setTimeRemaining] = React.useState(0)
const disableAutoplay = useAutoplayDisabled()
const isActive = embed.playlist === activeSource && activeViewId === viewId const isActive = embed.playlist === activeSource && activeViewId === viewId
// There are some different loading states that we should pay attention to and show a spinner for
const isLoading = const isLoading =
isActive && isActive &&
(playerStatus === 'waitingToPlayAtSpecifiedRate' || (playerStatus === 'waitingToPlayAtSpecifiedRate' ||
playerStatus === 'loading') playerStatus === 'loading')
const isSwitching = playerStatus === 'switching' // This happens whenever the visibility view decides that another video should start playing
const showOverlay = !isActive || isLoading || isSwitching const showOverlay = !isActive || isLoading || playerStatus === 'paused'
// send error up to error boundary // send error up to error boundary
const [error, setError] = useState<Error | PlayerError | null>(null) const [error, setError] = useState<Error | PlayerError | null>(null)
@ -102,11 +105,14 @@ function InnerWrapper({embed}: Props) {
) )
const statusSub = player.addListener( const statusSub = player.addListener(
'statusChange', 'statusChange',
(status, _oldStatus, playerError) => { (status, oldStatus, playerError) => {
setPlayerStatus(status) setPlayerStatus(status)
if (status === 'error') { if (status === 'error') {
setError(playerError ?? new Error('Unknown player error')) setError(playerError ?? new Error('Unknown player error'))
} }
if (status === 'readyToPlay' && oldStatus !== 'readyToPlay') {
player.play()
}
}, },
) )
return () => { return () => {
@ -115,35 +121,47 @@ function InnerWrapper({embed}: Props) {
statusSub.remove() statusSub.remove()
} }
} }
}, [player, isActive]) }, [player, isActive, disableAutoplay])
useEffect(() => { // The source might already be active (for example, if you are scrolling a list of quotes and its all the same
if (!isActive && playerStatus !== 'loading') { // video). In those cases, just start playing. Otherwise, setting the active source will result in the video
setPlayerStatus('loading') // start playback immediately
} const startPlaying = (ignoreAutoplayPreference: boolean) => {
}, [isActive, playerStatus]) if (disableAutoplay && !ignoreAutoplayPreference) {
const onChangeStatus = (isVisible: boolean) => {
if (isFullscreen) {
return return
} }
if (isVisible) { if (isActive) {
setActiveSource(embed.playlist, viewId)
if (!player.playing) {
player.play() player.play()
}
} else { } else {
setPlayerStatus('switching') setActiveSource(embed.playlist, viewId)
}
}
const onVisibilityStatusChange = (isVisible: boolean) => {
// When `isFullscreen` is true, it means we're actually still exiting the fullscreen player. Ignore these change
// events
if (isFullscreen) {
return
}
if (isVisible) {
startPlaying(false)
} else {
// Clear the active source so the video view unmounts when autoplay is disabled. Otherwise, leave it mounted
// until it gets replaced by another video
if (disableAutoplay) {
setActiveSource(null, null)
} else {
player.muted = true player.muted = true
if (player.playing) { if (player.playing) {
player.pause() player.pause()
} }
} }
} }
}
return ( return (
<VisibilityView enabled={true} onChangeStatus={onChangeStatus}> <VisibilityView enabled={true} onChangeStatus={onVisibilityStatusChange}>
{isActive ? ( {isActive ? (
<VideoEmbedInnerNative <VideoEmbedInnerNative
embed={embed} embed={embed}
@ -153,29 +171,26 @@ function InnerWrapper({embed}: Props) {
setIsFullscreen={setIsFullscreen} setIsFullscreen={setIsFullscreen}
/> />
) : null} ) : null}
<View <ImageBackground
source={{uri: embed.thumbnail}}
accessibilityIgnoresInvertColors
style={[ style={[
{ {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0,
backgroundColor: 'transparent', // If you don't add `backgroundColor` to the styles here,
// the play button won't show up on the first render on android 🥴😮‍💨
display: showOverlay ? 'flex' : 'none', display: showOverlay ? 'flex' : 'none',
}, },
]}> ]}
<Image cachePolicy="memory-disk" // Preferring memory cache helps to avoid flicker when re-displaying on android
source={{uri: embed.thumbnail}} >
alt={embed.alt}
style={a.flex_1}
contentFit="cover"
accessibilityIgnoresInvertColors
/>
<Button <Button
style={[a.absolute, a.inset_0]} style={[a.flex_1, a.align_center, a.justify_center]}
onPress={() => { onPress={() => startPlaying(true)}
setActiveSource(embed.playlist, viewId)
}}
label={_(msg`Play video`)} label={_(msg`Play video`)}
color="secondary"> color="secondary">
{isLoading ? ( {isLoading ? (
@ -183,8 +198,8 @@ function InnerWrapper({embed}: Props) {
style={[ style={[
a.rounded_full, a.rounded_full,
a.p_xs, a.p_xs,
a.absolute, a.align_center,
{top: 'auto', left: 'auto'}, a.justify_center,
{backgroundColor: 'rgba(0,0,0,0.5)'}, {backgroundColor: 'rgba(0,0,0,0.5)'},
]}> ]}>
<Loader size="2xl" style={{color: 'white'}} /> <Loader size="2xl" style={{color: 'white'}} />
@ -193,7 +208,7 @@ function InnerWrapper({embed}: Props) {
<PlayButtonIcon /> <PlayButtonIcon />
)} )}
</Button> </Button>
</View> </ImageBackground>
</VisibilityView> </VisibilityView>
) )
} }

View File

@ -67,9 +67,6 @@ export function VideoEmbedInnerNative({
PlatformInfo.setAudioActive(false) PlatformInfo.setAudioActive(false)
player.muted = true player.muted = true
player.playbackRate = 1 player.playbackRate = 1
if (!player.playing) {
player.play()
}
setIsFullscreen(false) setIsFullscreen(false)
}} }}
accessibilityLabel={ accessibilityLabel={