about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-09-06 09:31:01 -0700
committerGitHub <noreply@github.com>2024-09-06 09:31:01 -0700
commit60182cd874654ce925f2bfe48955d6b7499577ed (patch)
tree0812697ecbcf1e98272b63d4343f8d2de5c6bfd5 /src
parentbdff8752fbae6f3c5e485e39178793c1e14a3982 (diff)
downloadvoidsky-60182cd874654ce925f2bfe48955d6b7499577ed.tar.zst
[Video] Add disable autoplay for native, more tweaking (#5178)
Diffstat (limited to 'src')
-rw-r--r--src/lib/hooks/useDedupe.ts27
-rw-r--r--src/view/com/util/List.tsx9
-rw-r--r--src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx9
-rw-r--r--src/view/com/util/post-embeds/VideoEmbed.tsx93
-rw-r--r--src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx3
5 files changed, 81 insertions, 60 deletions
diff --git a/src/lib/hooks/useDedupe.ts b/src/lib/hooks/useDedupe.ts
index 13b5b83f5..b6ca5abbf 100644
--- a/src/lib/hooks/useDedupe.ts
+++ b/src/lib/hooks/useDedupe.ts
@@ -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],
+  )
 }
diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx
index c62ac5ed1..79dd2f491 100644
--- a/src/view/com/util/List.tsx
+++ b/src/view/com/util/List.tsx
@@ -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)
     },
   })
diff --git a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx
index bdc7967cb..da8c7a98c 100644
--- a/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx
+++ b/src/view/com/util/post-embeds/ActiveVideoNativeContext.tsx
@@ -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 (
diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx
index 7db25b4d0..e5457555b 100644
--- a/src/view/com/util/post-embeds/VideoEmbed.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbed.tsx
@@ -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')
+  // 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
     }
-  }, [isActive, playerStatus])
 
-  const onChangeStatus = (isVisible: boolean) => {
+    if (isActive) {
+      player.play()
+    } else {
+      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) {
-      setActiveSource(embed.playlist, viewId)
-      if (!player.playing) {
-        player.play()
-      }
+      startPlaying(false)
     } else {
-      setPlayerStatus('switching')
-      player.muted = true
-      if (player.playing) {
-        player.pause()
+      // 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>
   )
 }
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
index b747223ba..31e863038 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
@@ -67,9 +67,6 @@ export function VideoEmbedInnerNative({
           PlatformInfo.setAudioActive(false)
           player.muted = true
           player.playbackRate = 1
-          if (!player.playing) {
-            player.play()
-          }
           setIsFullscreen(false)
         }}
         accessibilityLabel={