this repo has no description
1import {useImperativeHandle, useRef, useState} from 'react'
2import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
3import {type AppBskyEmbedVideo} from '@atproto/api'
4import {BlueskyVideoView} from '@haileyok/bluesky-video'
5import {msg} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7
8import {HITSLOP_30} from '#/lib/constants'
9import {useAutoplayDisabled} from '#/state/preferences'
10import {atoms as a, useTheme} from '#/alf'
11import {useIsWithinMessage} from '#/components/dms/MessageContext'
12import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
13import {Pause_Filled_Corner0_Rounded as PauseIcon} from '#/components/icons/Pause'
14import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
15import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
16import {MediaInsetBorder} from '#/components/MediaInsetBorder'
17import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
18import {GifPresentationControls} from '../GifPresentationControls'
19import {TimeIndicator} from './TimeIndicator'
20
21export function VideoEmbedInnerNative({
22 ref,
23 embed,
24 setStatus,
25 setIsLoading,
26 setIsActive,
27}: {
28 ref: React.Ref<{togglePlayback: () => void}>
29 embed: AppBskyEmbedVideo.View
30 setStatus: (status: 'playing' | 'paused') => void
31 setIsLoading: (isLoading: boolean) => void
32 setIsActive: (isActive: boolean) => void
33}) {
34 const {_} = useLingui()
35 const videoRef = useRef<BlueskyVideoView>(null)
36 const autoplayDisabled = useAutoplayDisabled()
37 const isWithinMessage = useIsWithinMessage()
38 const [muted, setMuted] = useVideoMuteState()
39
40 const [isPlaying, setIsPlaying] = useState(false)
41 const [timeRemaining, setTimeRemaining] = useState(0)
42 const [error, setError] = useState<string>()
43
44 useImperativeHandle(ref, () => ({
45 togglePlayback: () => {
46 videoRef.current?.togglePlayback()
47 },
48 }))
49
50 if (error) {
51 throw new Error(error)
52 }
53
54 const isGif = embed.presentation === 'gif'
55
56 return (
57 <View style={[a.flex_1, a.relative]}>
58 <BlueskyVideoView
59 url={embed.playlist}
60 autoplay={!autoplayDisabled && !isWithinMessage}
61 beginMuted={isGif || (autoplayDisabled ? false : muted)}
62 style={[a.rounded_sm]}
63 onActiveChange={e => {
64 setIsActive(e.nativeEvent.isActive)
65 }}
66 onLoadingChange={e => {
67 setIsLoading(e.nativeEvent.isLoading)
68 }}
69 onMutedChange={e => {
70 if (!isGif) {
71 setMuted(e.nativeEvent.isMuted)
72 }
73 }}
74 onStatusChange={e => {
75 setStatus(e.nativeEvent.status)
76 setIsPlaying(e.nativeEvent.status === 'playing')
77 }}
78 onTimeRemainingChange={e => {
79 setTimeRemaining(e.nativeEvent.timeRemaining)
80 }}
81 onError={e => {
82 setError(e.nativeEvent.error)
83 }}
84 ref={videoRef}
85 accessibilityLabel={
86 embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`)
87 }
88 accessibilityHint=""
89 />
90 {isGif ? (
91 <GifPresentationControls
92 onPress={() => {
93 videoRef.current?.togglePlayback()
94 }}
95 isPlaying={isPlaying}
96 isLoading={false}
97 altText={embed.alt}
98 />
99 ) : (
100 <VideoPresentationControls
101 enterFullscreen={() => {
102 videoRef.current?.enterFullscreen(true)
103 }}
104 toggleMuted={() => {
105 videoRef.current?.toggleMuted()
106 }}
107 togglePlayback={() => {
108 videoRef.current?.togglePlayback()
109 }}
110 isPlaying={isPlaying}
111 timeRemaining={timeRemaining}
112 />
113 )}
114 <MediaInsetBorder />
115 </View>
116 )
117}
118
119function VideoPresentationControls({
120 enterFullscreen,
121 toggleMuted,
122 togglePlayback,
123 timeRemaining,
124 isPlaying,
125}: {
126 enterFullscreen: () => void
127 toggleMuted: () => void
128 togglePlayback: () => void
129 timeRemaining: number
130 isPlaying: boolean
131}) {
132 const {_} = useLingui()
133 const t = useTheme()
134 const [muted] = useVideoMuteState()
135
136 // show countdown when:
137 // 1. timeRemaining is a number - was seeing NaNs
138 // 2. duration is greater than 0 - means metadata has loaded
139 // 3. we're less than 5 second into the video
140 const showTime = !isNaN(timeRemaining)
141
142 return (
143 <View style={[a.absolute, a.inset_0]}>
144 <Pressable
145 onPress={enterFullscreen}
146 style={a.flex_1}
147 accessibilityLabel={_(msg`Video`)}
148 accessibilityHint={_(msg`Enters full screen`)}
149 accessibilityRole="button"
150 />
151 <ControlButton
152 onPress={togglePlayback}
153 label={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
154 accessibilityHint={_(msg`Plays or pauses the video`)}
155 style={{left: 6}}>
156 {isPlaying ? (
157 <PauseIcon width={13} fill={t.palette.white} />
158 ) : (
159 <PlayIcon width={13} fill={t.palette.white} />
160 )}
161 </ControlButton>
162 {showTime && <TimeIndicator time={timeRemaining} style={{left: 33}} />}
163
164 <ControlButton
165 onPress={toggleMuted}
166 label={
167 muted
168 ? _(msg({message: `Unmute`, context: 'video'}))
169 : _(msg({message: `Mute`, context: 'video'}))
170 }
171 accessibilityHint={_(msg`Toggles the sound`)}
172 style={{right: 6}}>
173 {muted ? (
174 <MuteIcon width={13} fill={t.palette.white} />
175 ) : (
176 <UnmuteIcon width={13} fill={t.palette.white} />
177 )}
178 </ControlButton>
179 </View>
180 )
181}
182
183function ControlButton({
184 onPress,
185 children,
186 label,
187 accessibilityHint,
188 style,
189}: {
190 onPress: () => void
191 children: React.ReactNode
192 label: string
193 accessibilityHint: string
194 style?: StyleProp<ViewStyle>
195}) {
196 return (
197 <View
198 style={[
199 a.absolute,
200 a.rounded_full,
201 a.justify_center,
202 {
203 backgroundColor: 'rgba(0, 0, 0, 0.5)',
204 paddingHorizontal: 4,
205 paddingVertical: 4,
206 bottom: 6,
207 minHeight: 21,
208 minWidth: 21,
209 },
210 style,
211 ]}>
212 <Pressable
213 onPress={onPress}
214 style={a.flex_1}
215 accessibilityLabel={label}
216 accessibilityHint={accessibilityHint}
217 accessibilityRole="button"
218 hitSlop={HITSLOP_30}>
219 {children}
220 </Pressable>
221 </View>
222 )
223}