[Video] 🫧 Move logic around by platform (#5003)
parent
9aa2b2d14e
commit
5ae0d40a14
|
@ -52,7 +52,7 @@ import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoNativeContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {Shell} from '#/view/shell'
|
import {Shell} from '#/view/shell'
|
||||||
import {ThemeProvider as Alf} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
|
|
|
@ -40,7 +40,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
|
||||||
import {ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoContext'
|
import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ToastContainer} from '#/view/com/util/Toast.web'
|
import {ToastContainer} from '#/view/com/util/Toast.web'
|
||||||
import {Shell} from '#/view/shell/index'
|
import {Shell} from '#/view/shell/index'
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {useVideoPlayer, VideoPlayer} from 'expo-video'
|
||||||
|
|
||||||
|
import {isNative} from '#/platform/detection'
|
||||||
|
|
||||||
|
const Context = React.createContext<{
|
||||||
|
activeSource: string | null
|
||||||
|
setActiveSource: (src: string) => void
|
||||||
|
player: VideoPlayer
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
|
if (!isNative) {
|
||||||
|
throw new Error('ActiveVideoProvider may only be used on native.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [activeSource, setActiveSource] = React.useState('')
|
||||||
|
|
||||||
|
const player = useVideoPlayer(activeSource, p => {
|
||||||
|
p.muted = true
|
||||||
|
p.loop = true
|
||||||
|
p.play()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{activeSource, setActiveSource, player}}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActiveVideoNative() {
|
||||||
|
const context = React.useContext(Context)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useActiveVideoNative must be used within a ActiveVideoNativeProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
|
@ -8,19 +8,21 @@ import React, {
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {useWindowDimensions} from 'react-native'
|
import {useWindowDimensions} from 'react-native'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
import {VideoPlayerProvider} from './VideoPlayerContext'
|
|
||||||
|
|
||||||
const ActiveVideoContext = React.createContext<{
|
const Context = React.createContext<{
|
||||||
activeViewId: string | null
|
activeViewId: string | null
|
||||||
setActiveView: (viewId: string, src: string) => void
|
setActiveView: (viewId: string) => void
|
||||||
sendViewPosition: (viewId: string, y: number) => void
|
sendViewPosition: (viewId: string, y: number) => void
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
|
export function Provider({children}: {children: React.ReactNode}) {
|
||||||
|
if (!isWeb) {
|
||||||
|
throw new Error('ActiveVideoWebContext may onl be used on web.')
|
||||||
|
}
|
||||||
|
|
||||||
const [activeViewId, setActiveViewId] = useState<string | null>(null)
|
const [activeViewId, setActiveViewId] = useState<string | null>(null)
|
||||||
const activeViewLocationRef = useRef(Infinity)
|
const activeViewLocationRef = useRef(Infinity)
|
||||||
const [source, setSource] = useState<string | null>(null)
|
|
||||||
const {height: windowHeight} = useWindowDimensions()
|
const {height: windowHeight} = useWindowDimensions()
|
||||||
|
|
||||||
// minimising re-renders by using refs
|
// minimising re-renders by using refs
|
||||||
|
@ -31,9 +33,8 @@ export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
|
||||||
}, [activeViewId])
|
}, [activeViewId])
|
||||||
|
|
||||||
const setActiveView = useCallback(
|
const setActiveView = useCallback(
|
||||||
(viewId: string, src: string) => {
|
(viewId: string) => {
|
||||||
setActiveViewId(viewId)
|
setActiveViewId(viewId)
|
||||||
setSource(src)
|
|
||||||
manuallySetRef.current = true
|
manuallySetRef.current = true
|
||||||
// we don't know the exact position, but it's definitely on screen
|
// we don't know the exact position, but it's definitely on screen
|
||||||
// so just guess that it's in the middle. Any value is fine
|
// so just guess that it's in the middle. Any value is fine
|
||||||
|
@ -88,32 +89,26 @@ export function ActiveVideoProvider({children}: {children: React.ReactNode}) {
|
||||||
[activeViewId, setActiveView, sendViewPosition],
|
[activeViewId, setActiveView, sendViewPosition],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return <Context.Provider value={value}>{children}</Context.Provider>
|
||||||
<ActiveVideoContext.Provider value={value}>
|
|
||||||
<VideoPlayerProvider source={source ?? ''}>
|
|
||||||
{children}
|
|
||||||
</VideoPlayerProvider>
|
|
||||||
</ActiveVideoContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useActiveVideoView({source}: {source: string}) {
|
export function useActiveVideoWeb() {
|
||||||
const context = React.useContext(ActiveVideoContext)
|
const context = React.useContext(Context)
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useActiveVideo must be used within a ActiveVideoProvider')
|
throw new Error(
|
||||||
|
'useActiveVideoWeb must be used within a ActiveVideoWebProvider',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {activeViewId, setActiveView, sendViewPosition} = context
|
||||||
const id = useId()
|
const id = useId()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: context.activeViewId === id,
|
active: activeViewId === id,
|
||||||
setActive: useCallback(
|
setActive: () => {
|
||||||
() => context.setActiveView(id, source),
|
setActiveView(id)
|
||||||
[context, id, source],
|
},
|
||||||
),
|
currentActiveView: activeViewId,
|
||||||
currentActiveView: context.activeViewId,
|
sendPosition: (y: number) => sendViewPosition(id, y),
|
||||||
sendPosition: useCallback(
|
|
||||||
(y: number) => context.sendViewPosition(id, y),
|
|
||||||
[context, id],
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,12 +9,13 @@ import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play'
|
||||||
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army'
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoView} from './ActiveVideoContext'
|
import {useActiveVideoNative} from './ActiveVideoNativeContext'
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
export function VideoEmbed({source}: {source: string}) {
|
export function VideoEmbed({source}: {source: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {active, setActive} = useActiveVideoView({source})
|
const {activeSource, setActiveSource} = useActiveVideoNative()
|
||||||
|
const isActive = source === activeSource
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const [key, setKey] = useState(0)
|
const [key, setKey] = useState(0)
|
||||||
|
@ -40,15 +41,17 @@ export function VideoEmbed({source}: {source: string}) {
|
||||||
enabled={true}
|
enabled={true}
|
||||||
onChangeStatus={isActive => {
|
onChangeStatus={isActive => {
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
setActive()
|
setActiveSource(source)
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{active ? (
|
{isActive ? (
|
||||||
<VideoEmbedInnerNative />
|
<VideoEmbedInnerNative />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
||||||
onPress={setActive}
|
onPress={() => {
|
||||||
|
setActiveSource(source)
|
||||||
|
}}
|
||||||
label={_(msg`Play video`)}
|
label={_(msg`Play video`)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
|
|
@ -8,14 +8,14 @@ import {
|
||||||
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ErrorBoundary} from '../ErrorBoundary'
|
import {ErrorBoundary} from '../ErrorBoundary'
|
||||||
import {useActiveVideoView} from './ActiveVideoContext'
|
import {useActiveVideoWeb} from './ActiveVideoWebContext'
|
||||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||||
|
|
||||||
export function VideoEmbed({source}: {source: string}) {
|
export function VideoEmbed({source}: {source: string}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
const {active, setActive, sendPosition, currentActiveView} =
|
const {active, setActive, sendPosition, currentActiveView} =
|
||||||
useActiveVideoView({source})
|
useActiveVideoWeb()
|
||||||
const [onScreen, setOnScreen] = useState(false)
|
const [onScreen, setOnScreen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {useIsFocused} from '@react-navigation/native'
|
||||||
import {HITSLOP_30} from '#/lib/constants'
|
import {HITSLOP_30} from '#/lib/constants'
|
||||||
import {useAppState} from '#/lib/hooks/useAppState'
|
import {useAppState} from '#/lib/hooks/useAppState'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useVideoPlayer} from '#/view/com/util/post-embeds/VideoPlayerContext'
|
import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||||
|
@ -20,7 +20,7 @@ import {
|
||||||
import {TimeIndicator} from './TimeIndicator'
|
import {TimeIndicator} from './TimeIndicator'
|
||||||
|
|
||||||
export function VideoEmbedInnerNative() {
|
export function VideoEmbedInnerNative() {
|
||||||
const player = useVideoPlayer()
|
const {player} = useActiveVideoNative()
|
||||||
const ref = useRef<VideoView>(null)
|
const ref = useRef<VideoView>(null)
|
||||||
const isScreenFocused = useIsFocused()
|
const isScreenFocused = useIsFocused()
|
||||||
const isAppFocused = useAppState()
|
const isAppFocused = useAppState()
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
import React, {useContext} from 'react'
|
|
||||||
import type {VideoPlayer} from 'expo-video'
|
|
||||||
import {useVideoPlayer as useExpoVideoPlayer} from 'expo-video'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {
|
|
||||||
AudioCategory,
|
|
||||||
PlatformInfo,
|
|
||||||
} from '../../../../../modules/expo-bluesky-swiss-army'
|
|
||||||
|
|
||||||
const VideoPlayerContext = React.createContext<VideoPlayer | null>(null)
|
|
||||||
|
|
||||||
export function VideoPlayerProvider({
|
|
||||||
source,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
source: string
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
||||||
const player = useExpoVideoPlayer(source, player => {
|
|
||||||
try {
|
|
||||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
|
||||||
PlatformInfo.setAudioActive(false)
|
|
||||||
|
|
||||||
player.loop = true
|
|
||||||
player.muted = true
|
|
||||||
player.play()
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Failed to init video player', {safeMessage: err})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VideoPlayerContext.Provider value={player}>
|
|
||||||
{children}
|
|
||||||
</VideoPlayerContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVideoPlayer() {
|
|
||||||
const context = useContext(VideoPlayerContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useVideoPlayer must be used within a VideoPlayerProvider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export function VideoPlayerProvider({children}: {children: React.ReactNode}) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useVideoPlayer() {
|
|
||||||
throw new Error('useVideoPlayer must not be used on web')
|
|
||||||
}
|
|
Loading…
Reference in New Issue