about summary refs log tree commit diff
path: root/src/components/Post/Embed
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/Post/Embed')
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx161
-rw-r--r--src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx6
-rw-r--r--src/components/Post/Embed/VideoEmbed/index.tsx21
-rw-r--r--src/components/Post/Embed/VideoEmbed/index.web.tsx61
4 files changed, 137 insertions, 112 deletions
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
index 351e9f305..ecc36dc33 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx
@@ -1,4 +1,4 @@
-import React, {useRef} from 'react'
+import {useImperativeHandle, useRef, useState} from 'react'
 import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
 import {type AppBskyEmbedVideo} from '@atproto/api'
 import {BlueskyVideoView} from '@haileyok/bluesky-video'
@@ -17,91 +17,88 @@ import {MediaInsetBorder} from '#/components/MediaInsetBorder'
 import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
 import {TimeIndicator} from './TimeIndicator'
 
-export const VideoEmbedInnerNative = React.forwardRef(
-  function VideoEmbedInnerNative(
-    {
-      embed,
-      setStatus,
-      setIsLoading,
-      setIsActive,
-    }: {
-      embed: AppBskyEmbedVideo.View
-      setStatus: (status: 'playing' | 'paused') => void
-      setIsLoading: (isLoading: boolean) => void
-      setIsActive: (isActive: boolean) => void
-    },
-    ref: React.Ref<{togglePlayback: () => void}>,
-  ) {
-    const {_} = useLingui()
-    const videoRef = useRef<BlueskyVideoView>(null)
-    const autoplayDisabled = useAutoplayDisabled()
-    const isWithinMessage = useIsWithinMessage()
-    const [muted, setMuted] = useVideoMuteState()
+export function VideoEmbedInnerNative({
+  ref,
+  embed,
+  setStatus,
+  setIsLoading,
+  setIsActive,
+}: {
+  ref: React.Ref<{togglePlayback: () => void}>
+  embed: AppBskyEmbedVideo.View
+  setStatus: (status: 'playing' | 'paused') => void
+  setIsLoading: (isLoading: boolean) => void
+  setIsActive: (isActive: boolean) => void
+}) {
+  const {_} = useLingui()
+  const videoRef = useRef<BlueskyVideoView>(null)
+  const autoplayDisabled = useAutoplayDisabled()
+  const isWithinMessage = useIsWithinMessage()
+  const [muted, setMuted] = useVideoMuteState()
 
-    const [isPlaying, setIsPlaying] = React.useState(false)
-    const [timeRemaining, setTimeRemaining] = React.useState(0)
-    const [error, setError] = React.useState<string>()
+  const [isPlaying, setIsPlaying] = useState(false)
+  const [timeRemaining, setTimeRemaining] = useState(0)
+  const [error, setError] = useState<string>()
 
-    React.useImperativeHandle(ref, () => ({
-      togglePlayback: () => {
-        videoRef.current?.togglePlayback()
-      },
-    }))
+  useImperativeHandle(ref, () => ({
+    togglePlayback: () => {
+      videoRef.current?.togglePlayback()
+    },
+  }))
 
-    if (error) {
-      throw new Error(error)
-    }
+  if (error) {
+    throw new Error(error)
+  }
 
-    return (
-      <View style={[a.flex_1, a.relative]}>
-        <BlueskyVideoView
-          url={embed.playlist}
-          autoplay={!autoplayDisabled && !isWithinMessage}
-          beginMuted={autoplayDisabled ? false : muted}
-          style={[a.rounded_sm]}
-          onActiveChange={e => {
-            setIsActive(e.nativeEvent.isActive)
-          }}
-          onLoadingChange={e => {
-            setIsLoading(e.nativeEvent.isLoading)
-          }}
-          onMutedChange={e => {
-            setMuted(e.nativeEvent.isMuted)
-          }}
-          onStatusChange={e => {
-            setStatus(e.nativeEvent.status)
-            setIsPlaying(e.nativeEvent.status === 'playing')
-          }}
-          onTimeRemainingChange={e => {
-            setTimeRemaining(e.nativeEvent.timeRemaining)
-          }}
-          onError={e => {
-            setError(e.nativeEvent.error)
-          }}
-          ref={videoRef}
-          accessibilityLabel={
-            embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
-          }
-          accessibilityHint=""
-        />
-        <VideoControls
-          enterFullscreen={() => {
-            videoRef.current?.enterFullscreen(true)
-          }}
-          toggleMuted={() => {
-            videoRef.current?.toggleMuted()
-          }}
-          togglePlayback={() => {
-            videoRef.current?.togglePlayback()
-          }}
-          isPlaying={isPlaying}
-          timeRemaining={timeRemaining}
-        />
-        <MediaInsetBorder />
-      </View>
-    )
-  },
-)
+  return (
+    <View style={[a.flex_1, a.relative]}>
+      <BlueskyVideoView
+        url={embed.playlist}
+        autoplay={!autoplayDisabled && !isWithinMessage}
+        beginMuted={autoplayDisabled ? false : muted}
+        style={[a.rounded_sm]}
+        onActiveChange={e => {
+          setIsActive(e.nativeEvent.isActive)
+        }}
+        onLoadingChange={e => {
+          setIsLoading(e.nativeEvent.isLoading)
+        }}
+        onMutedChange={e => {
+          setMuted(e.nativeEvent.isMuted)
+        }}
+        onStatusChange={e => {
+          setStatus(e.nativeEvent.status)
+          setIsPlaying(e.nativeEvent.status === 'playing')
+        }}
+        onTimeRemainingChange={e => {
+          setTimeRemaining(e.nativeEvent.timeRemaining)
+        }}
+        onError={e => {
+          setError(e.nativeEvent.error)
+        }}
+        ref={videoRef}
+        accessibilityLabel={
+          embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
+        }
+        accessibilityHint=""
+      />
+      <VideoControls
+        enterFullscreen={() => {
+          videoRef.current?.enterFullscreen(true)
+        }}
+        toggleMuted={() => {
+          videoRef.current?.toggleMuted()
+        }}
+        togglePlayback={() => {
+          videoRef.current?.togglePlayback()
+        }}
+        isPlaying={isPlaying}
+        timeRemaining={timeRemaining}
+      />
+      <MediaInsetBorder />
+    </View>
+  )
+}
 
 function VideoControls({
   enterFullscreen,
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
index ce3a7b2c9..266438c04 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
@@ -224,15 +224,19 @@ function useHLS({
       throw new HLSUnsupportedError()
     }
 
+    const latestEstimate = BandwidthEstimate.get()
     const hls = new Hls({
       maxMaxBufferLength: 10, // only load 10s ahead
       // note: the amount buffered is affected by both maxBufferLength and maxBufferSize
       // it will buffer until it is greater than *both* of those values
       // so we use maxMaxBufferLength to set the actual maximum amount of buffering instead
+      startLevel:
+        latestEstimate === undefined ? -1 : Hls.DefaultConfig.startLevel,
+      // the '-1' value makes a test request to estimate bandwidth and quality level
+      // before showing the first fragment
     })
     hlsRef.current = hls
 
-    const latestEstimate = BandwidthEstimate.get()
     if (latestEstimate !== undefined) {
       hls.bandwidthEstimate = latestEstimate
     }
diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx
index 8cb78ff70..c66d1a218 100644
--- a/src/components/Post/Embed/VideoEmbed/index.tsx
+++ b/src/components/Post/Embed/VideoEmbed/index.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useState} from 'react'
+import {useCallback, useRef, useState} from 'react'
 import {ActivityIndicator, View} from 'react-native'
 import {ImageBackground} from 'expo-image'
 import {type AppBskyEmbedVideo} from '@atproto/api'
@@ -81,13 +81,13 @@ export function VideoEmbed({embed, crop}: Props) {
 
 function InnerWrapper({embed}: Props) {
   const {_} = useLingui()
-  const ref = React.useRef<{togglePlayback: () => void}>(null)
+  const ref = useRef<{togglePlayback: () => void}>(null)
 
-  const [status, setStatus] = React.useState<'playing' | 'paused' | 'pending'>(
+  const [status, setStatus] = useState<'playing' | 'paused' | 'pending'>(
     'pending',
   )
-  const [isLoading, setIsLoading] = React.useState(false)
-  const [isActive, setIsActive] = React.useState(false)
+  const [isLoading, setIsLoading] = useState(false)
+  const [isActive, setIsActive] = useState(false)
   const showSpinner = useThrottledValue(isActive && isLoading, 100)
 
   const showOverlay =
@@ -96,11 +96,9 @@ function InnerWrapper({embed}: Props) {
     (status === 'paused' && !isActive) ||
     status === 'pending'
 
-  React.useEffect(() => {
-    if (!isActive && status !== 'pending') {
-      setStatus('pending')
-    }
-  }, [isActive, status])
+  if (!isActive && status !== 'pending') {
+    setStatus('pending')
+  }
 
   return (
     <>
@@ -131,8 +129,7 @@ function InnerWrapper({embed}: Props) {
             onPress={() => {
               ref.current?.togglePlayback()
             }}
-            label={_(msg`Play video`)}
-            color="secondary">
+            label={_(msg`Play video`)}>
             {showSpinner ? (
               <View
                 style={[
diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx
index 7f601af47..5bb54eef8 100644
--- a/src/components/Post/Embed/VideoEmbed/index.web.tsx
+++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx
@@ -1,4 +1,11 @@
-import {useCallback, useEffect, useRef, useState} from 'react'
+import {
+  createContext,
+  useCallback,
+  useContext,
+  useEffect,
+  useRef,
+  useState,
+} from 'react'
 import {View} from 'react-native'
 import {type AppBskyEmbedVideo} from '@atproto/api'
 import {msg} from '@lingui/macro'
@@ -83,9 +90,7 @@ export function VideoEmbed({
       style={{display: 'flex', flex: 1, cursor: 'default'}}
       onClick={evt => evt.stopPropagation()}>
       <ErrorBoundary renderError={renderError} key={key}>
-        <ViewportObserver
-          sendPosition={sendPosition}
-          isAnyViewActive={currentActiveView !== null}>
+        <OnlyNearScreen>
           <VideoEmbedInnerWeb
             embed={embed}
             active={active}
@@ -93,31 +98,39 @@ export function VideoEmbed({
             onScreen={onScreen}
             lastKnownTime={lastKnownTime}
           />
-        </ViewportObserver>
+        </OnlyNearScreen>
       </ErrorBoundary>
     </div>
   )
 
   return (
     <View style={[a.pt_xs]}>
-      {cropDisabled ? (
-        <View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
-          {contents}
-        </View>
-      ) : (
-        <ConstrainedImage
-          fullBleed={crop === 'square'}
-          aspectRatio={constrained || 1}>
-          {contents}
-        </ConstrainedImage>
-      )}
+      <ViewportObserver
+        sendPosition={sendPosition}
+        isAnyViewActive={currentActiveView !== null}>
+        {cropDisabled ? (
+          <View style={[a.w_full, a.overflow_hidden, {aspectRatio: max ?? 1}]}>
+            {contents}
+          </View>
+        ) : (
+          <ConstrainedImage
+            fullBleed={crop === 'square'}
+            aspectRatio={constrained || 1}>
+            {contents}
+          </ConstrainedImage>
+        )}
+      </ViewportObserver>
     </View>
   )
 }
 
+const NearScreenContext = createContext(false)
+
 /**
  * Renders a 100vh tall div and watches it with an IntersectionObserver to
  * send the position of the div when it's near the screen.
+ *
+ * IMPORTANT: ViewportObserver _must_ not be within a `overflow: hidden` container.
  */
 function ViewportObserver({
   children,
@@ -164,7 +177,9 @@ function ViewportObserver({
 
   return (
     <View style={[a.flex_1, a.flex_row]}>
-      {nearScreen && children}
+      <NearScreenContext.Provider value={nearScreen}>
+        {children}
+      </NearScreenContext.Provider>
       <div
         ref={ref}
         style={{
@@ -182,6 +197,18 @@ function ViewportObserver({
   )
 }
 
+/**
+ * Awkward data flow here, but we need to hide the video when it's not near the screen.
+ * But also, ViewportObserver _must_ not be within a `overflow: hidden` container.
+ * So we put it at the top level of the component tree here, then hide the children of
+ * the auto-resizing container.
+ */
+export const OnlyNearScreen = ({children}: {children: React.ReactNode}) => {
+  const nearScreen = useContext(NearScreenContext)
+
+  return nearScreen ? children : null
+}
+
 function VideoError({error, retry}: {error: unknown; retry: () => void}) {
   const {_} = useLingui()