smarter video source handling

This commit is contained in:
Samuel Newman
2025-01-15 23:47:52 +00:00
parent 2e0a088d33
commit 94df97ffae
+78 -48
View File
@@ -1,4 +1,4 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import { import {
LayoutAnimation, LayoutAnimation,
ListRenderItem, ListRenderItem,
@@ -10,7 +10,6 @@ import {
import {Gesture, GestureDetector} from 'react-native-gesture-handler' import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import {runOnJS} from 'react-native-reanimated' import {runOnJS} from 'react-native-reanimated'
import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'
import {useEvent} from 'expo'
import {LinearGradient} from 'expo-linear-gradient' import {LinearGradient} from 'expo-linear-gradient'
import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video' import {useVideoPlayer, VideoPlayer, VideoView} from 'expo-video'
import { import {
@@ -101,6 +100,10 @@ function YoloFeed() {
fetchNextPage, fetchNextPage,
} = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`) } = usePostFeedQuery(`feedgen|${VIBES_FEED_URI}`)
const [currentSources, setCurrentSources] = useState<
[string | null, string | null, string | null]
>([null, null, null])
const player1 = useVideoPlayer('', p => { const player1 = useVideoPlayer('', p => {
p.loop = true p.loop = true
}) })
@@ -125,27 +128,89 @@ function YoloFeed() {
} }
const player = [player1, player2, player3][index % 3] const player = [player1, player2, player3][index % 3]
const currentSource = currentSources[index % 3]
return ( return (
<VibeItem <VibeItem
player={player} player={player}
post={post} post={post}
embed={post.embed} embed={post.embed}
loaded={isFocused && Math.abs(index - currentIndex) < 2} loaded={
isFocused &&
Math.abs(index - currentIndex) < 2 &&
currentSource === post.embed.playlist
}
active={isFocused && index === currentIndex} active={isFocused && index === currentIndex}
/> />
) )
}, },
[player1, player2, player3, currentIndex, isFocused], [player1, player2, player3, currentIndex, isFocused, currentSources],
) )
const updateVideoState = useNonReactiveCallback((index: number) => {
if (!videos) return
setCurrentIndex(index)
setCurrentSources(oldSources => {
const currentSources = [...oldSources] as [
string | null,
string | null,
string | null,
]
const prevEmbed = videos[index - 1]?.post.embed
const prevVideo =
prevEmbed && AppBskyEmbedVideo.isView(prevEmbed)
? prevEmbed.playlist
: null
const currEmbed = videos[index]?.post.embed
const currVideo =
currEmbed && AppBskyEmbedVideo.isView(currEmbed)
? currEmbed.playlist
: null
const nextEmbed = videos[index + 1]?.post.embed
const nextVideo =
nextEmbed && AppBskyEmbedVideo.isView(nextEmbed)
? nextEmbed.playlist
: null
const prevPlayer = [player1, player2, player3][(index + 2) % 3]
const prevPlayerCurrentSource = currentSources[(index + 2) % 3]
const currPlayer = [player1, player2, player3][index % 3]
const currPlayerCurrentSource = currentSources[index % 3]
const nextPlayer = [player1, player2, player3][(index + 1) % 3]
const nextPlayerCurrentSource = currentSources[(index + 1) % 3]
if (prevVideo && prevVideo !== prevPlayerCurrentSource) {
prevPlayer.replace(prevVideo)
currentSources[index + (2 % 3)] = prevVideo
}
prevPlayer.pause()
if (currVideo) {
if (currVideo !== currPlayerCurrentSource) {
currPlayer.replace(currVideo)
currentSources[index % 3] = currVideo
}
currPlayer.play()
}
if (nextVideo && nextVideo !== nextPlayerCurrentSource) {
nextPlayer.replace(nextVideo)
currentSources[(index + 1) % 3] = nextVideo
}
nextPlayer.pause()
return currentSources
})
})
const onViewableItemsChanged = useCallback( const onViewableItemsChanged = useCallback(
({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => { ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
if (viewableItems[0] && viewableItems[0].index !== null) { if (viewableItems[0] && viewableItems[0].index !== null) {
setCurrentIndex(viewableItems[0].index) updateVideoState(viewableItems[0].index)
} }
}, },
[], [updateVideoState],
) )
return ( return (
@@ -170,7 +235,7 @@ function YoloFeed() {
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
onViewableItemsChanged={onViewableItemsChanged} onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig={{itemVisiblePercentThreshold: 75}} viewabilityConfig={{itemVisiblePercentThreshold: 95}}
/> />
) )
} }
@@ -183,7 +248,6 @@ function VibeItem({
player, player,
post, post,
embed, embed,
active,
loaded, loaded,
}: { }: {
player: VideoPlayer player: VideoPlayer
@@ -194,42 +258,6 @@ function VibeItem({
}) { }) {
const {height, width} = useWindowDimensions() const {height, width} = useWindowDimensions()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const source = embed.playlist
const sourceChangeEvent = useEvent(player, 'sourceChange') as {
// incorrect types
source: {uri?: string}
oldSource: {uri?: string}
}
// for initial video - useEffect will handle the typical case where
// videos have a chance to preload
const maybePlay = useNonReactiveCallback(() => {
if (active && !player.playing) {
player.play()
}
})
useEffect(() => {
if (loaded && sourceChangeEvent?.source?.uri !== source) {
player.replace(source)
// play next tick
const timeout = setTimeout(() => {
maybePlay()
}, 0)
return () => {
clearTimeout(timeout)
}
}
}, [sourceChangeEvent?.source?.uri, loaded, source, player, maybePlay])
useEffect(() => {
if (active) {
player.play()
} else {
// should be a cleanup function, but that causes a crash
player.pause()
}
}, [active, player])
const screenAspectRatio = const screenAspectRatio =
(width - insets.left - insets.right) / (height - insets.bottom) (width - insets.left - insets.right) / (height - insets.bottom)
@@ -387,10 +415,12 @@ function ExpandableRichTextView({
<ScrollView <ScrollView
scrollEnabled={expanded} scrollEnabled={expanded}
onContentSizeChange={(_w, h) => { onContentSizeChange={(_w, h) => {
LayoutAnimation.configureNext({ if (contentHeight !== 0) {
duration: 500, LayoutAnimation.configureNext({
update: {type: 'spring', springDamping: 0.6}, duration: 500,
}) update: {type: 'spring', springDamping: 0.6},
})
}
setContentHeight(h) setContentHeight(h)
}} }}
style={{height: Math.min(contentHeight, screenHeight * 0.5)}} style={{height: Math.min(contentHeight, screenHeight * 0.5)}}