[Video] Add disable autoplay for native, more tweaking (#5178)
parent
bdff8752fb
commit
60182cd874
|
@ -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],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -67,9 +67,6 @@ export function VideoEmbedInnerNative({
|
|||
PlatformInfo.setAudioActive(false)
|
||||
player.muted = true
|
||||
player.playbackRate = 1
|
||||
if (!player.playing) {
|
||||
player.play()
|
||||
}
|
||||
setIsFullscreen(false)
|
||||
}}
|
||||
accessibilityLabel={
|
||||
|
|
Loading…
Reference in New Issue