bsky-app/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative.tsx
Samuel Newman eb868a042a
[Video] Misc player style tweaks (#5064)
* use actual black rather than theme black

* adjust time/mute indicators
2024-09-02 09:33:46 +01:00

200 lines
5.9 KiB
TypeScript

import React, {useCallback, useEffect, useRef, useState} from 'react'
import {Pressable, View} from 'react-native'
import Animated, {FadeInDown} from 'react-native-reanimated'
import {VideoPlayer, VideoView} from 'expo-video'
import {AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useIsFocused} from '@react-navigation/native'
import {HITSLOP_30} from '#/lib/constants'
import {useAppState} from '#/lib/hooks/useAppState'
import {clamp} from '#/lib/numbers'
import {logger} from '#/logger'
import {useActiveVideoNative} from 'view/com/util/post-embeds/ActiveVideoNativeContext'
import {atoms as a, useTheme} from '#/alf'
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {
AudioCategory,
PlatformInfo,
} from '../../../../../../modules/expo-bluesky-swiss-army'
import {TimeIndicator} from './TimeIndicator'
export function VideoEmbedInnerNative({
embed,
}: {
embed: AppBskyEmbedVideo.View
}) {
const {_} = useLingui()
const {player} = useActiveVideoNative()
const ref = useRef<VideoView>(null)
const isScreenFocused = useIsFocused()
const isAppFocused = useAppState()
useEffect(() => {
try {
if (isAppFocused === 'active' && isScreenFocused && !player.playing) {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false)
player.muted = true
player.play()
} else if (player.playing) {
player.pause()
}
} catch (err) {
logger.error(
'Failed to play/pause while backgrounding/switching screens',
{safeMessage: err},
)
}
}, [isAppFocused, player, isScreenFocused])
const enterFullscreen = useCallback(() => {
ref.current?.enterFullscreen()
}, [])
let aspectRatio = 16 / 9
if (embed.aspectRatio) {
const {width, height} = embed.aspectRatio
aspectRatio = width / height
aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1)
}
return (
<View style={[a.flex_1, a.relative, {aspectRatio}]}>
<VideoView
ref={ref}
player={player}
style={[a.flex_1, a.rounded_sm]}
contentFit="contain"
nativeControls={true}
accessibilityIgnoresInvertColors
onEnterFullscreen={() => {
PlatformInfo.setAudioCategory(AudioCategory.Playback)
PlatformInfo.setAudioActive(true)
player.muted = false
}}
onExitFullscreen={() => {
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
PlatformInfo.setAudioActive(false)
player.muted = true
if (!player.playing) player.play()
}}
accessibilityLabel={
embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
}
accessibilityHint=""
/>
<VideoControls player={player} enterFullscreen={enterFullscreen} />
</View>
)
}
function VideoControls({
player,
enterFullscreen,
}: {
player: VideoPlayer
enterFullscreen: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const [isMuted, setIsMuted] = useState(player.muted)
const [timeRemaining, setTimeRemaining] = React.useState(0)
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const volumeSub = player.addListener('volumeChange', ({isMuted}) => {
setIsMuted(isMuted)
})
const timeSub = player.addListener(
'timeRemainingChange',
secondsRemaining => {
setTimeRemaining(secondsRemaining)
},
)
return () => {
volumeSub.remove()
timeSub.remove()
}
}, [player])
const onPressFullscreen = useCallback(() => {
switch (player.status) {
case 'idle':
case 'loading':
case 'readyToPlay': {
if (!player.playing) player.play()
enterFullscreen()
break
}
case 'error': {
player.replay()
break
}
}
}, [player, enterFullscreen])
const toggleMuted = useCallback(() => {
const muted = !player.muted
// We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when
// the video is muted, and vice versa.
const mix = !muted
const category = muted ? AudioCategory.Ambient : AudioCategory.Playback
PlatformInfo.setAudioCategory(category)
PlatformInfo.setAudioActive(mix)
player.muted = muted
}, [player])
// show countdown when:
// 1. timeRemaining is a number - was seeing NaNs
// 2. duration is greater than 0 - means metadata has loaded
// 3. we're less than 5 second into the video
const showTime = !isNaN(timeRemaining)
return (
<View style={[a.absolute, a.inset_0]}>
{showTime && <TimeIndicator time={timeRemaining} />}
<Pressable
onPress={onPressFullscreen}
style={a.flex_1}
accessibilityLabel={_(msg`Video`)}
accessibilityHint={_(msg`Tap to enter full screen`)}
accessibilityRole="button"
/>
<Animated.View
entering={FadeInDown.duration(300)}
style={[
a.absolute,
a.rounded_full,
a.justify_center,
{
backgroundColor: 'rgba(0, 0, 0, 0.5)',
paddingHorizontal: 4,
paddingVertical: 4,
bottom: 6,
right: 6,
minHeight: 21,
minWidth: 21,
},
]}>
<Pressable
onPress={toggleMuted}
style={a.flex_1}
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
accessibilityHint={_(msg`Tap to toggle sound`)}
accessibilityRole="button"
hitSlop={HITSLOP_30}>
{isMuted ? (
<MuteIcon width={13} fill={t.palette.white} />
) : (
<UnmuteIcon width={13} fill={t.palette.white} />
)}
</Pressable>
</Animated.View>
</View>
)
}