[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'
export const useDedupe = () => {
export const useDedupe = (timeout = 250) => {
const canDo = React.useRef(true)
return React.useCallback((cb: () => unknown) => {
if (canDo.current) {
canDo.current = false
setTimeout(() => {
canDo.current = true
}, 250)
cb()
return true
}
return false
}, [])
return React.useCallback(
(cb: () => unknown) => {
if (canDo.current) {
canDo.current = false
setTimeout(() => {
canDo.current = true
}, timeout)
cb()
return true
}
return false
},
[timeout],
)
}

View File

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

View File

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

View File

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

View File

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