[Video] Add disable autoplay for native, more tweaking (#5178)
parent
bdff8752fb
commit
60182cd874
|
@ -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(
|
||||||
if (canDo.current) {
|
(cb: () => unknown) => {
|
||||||
canDo.current = false
|
if (canDo.current) {
|
||||||
setTimeout(() => {
|
canDo.current = false
|
||||||
canDo.current = true
|
setTimeout(() => {
|
||||||
}, 250)
|
canDo.current = true
|
||||||
cb()
|
}, timeout)
|
||||||
return true
|
cb()
|
||||||
}
|
return true
|
||||||
return false
|
}
|
||||||
}, [])
|
return false
|
||||||
|
},
|
||||||
|
[timeout],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runOnJS(dedupe)(updateActiveViewAsync)
|
if (isIOS) {
|
||||||
|
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)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
player.play()
|
||||||
if (!player.playing) {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setPlayerStatus('switching')
|
setActiveSource(embed.playlist, viewId)
|
||||||
player.muted = true
|
}
|
||||||
if (player.playing) {
|
}
|
||||||
player.pause()
|
|
||||||
|
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 (
|
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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={
|
||||||
|
|
Loading…
Reference in New Issue