@zezosoft/zezo-ott-react-native-video-player 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/AdsPlayer/AdsPlayer.tsx +0 -311
- package/src/AdsPlayer/MediaControls/AdBottomControls.tsx +0 -191
- package/src/AdsPlayer/MediaControls/AdMediaControls.tsx +0 -104
- package/src/AdsPlayer/MediaControls/AdMediaControlsProvider.tsx +0 -62
- package/src/AdsPlayer/MediaControls/AdMiddleControls.tsx +0 -63
- package/src/AdsPlayer/MediaControls/AdTopControls.tsx +0 -191
- package/src/AdsPlayer/MediaControls/index.ts +0 -5
- package/src/AdsPlayer/components/RotatingLoader.tsx +0 -79
- package/src/AdsPlayer/index.ts +0 -4
- package/src/AdsPlayer/store/adsPlayer.type.ts +0 -29
- package/src/AdsPlayer/store/adsPlayerStore.ts +0 -59
- package/src/AdsPlayer/store/index.ts +0 -2
- package/src/AdsPlayer/utils/adStateReset.ts +0 -29
- package/src/AdsPlayer/utils/controls.ts +0 -69
- package/src/AdsPlayer/utils/useAdControlsAutoHide.ts +0 -32
- package/src/AdsPlayer/utils/useAdInitialization.ts +0 -86
- package/src/AdsPlayer/utils/useAdTracking.ts +0 -89
- package/src/AdsPlayer/utils/useAdsManager.ts +0 -215
- package/src/VideoPlayer/MediaControls/BottomControls.tsx +0 -210
- package/src/VideoPlayer/MediaControls/MediaControls.tsx +0 -30
- package/src/VideoPlayer/MediaControls/MediaControlsProvider.tsx +0 -104
- package/src/VideoPlayer/MediaControls/MiddleControls.tsx +0 -259
- package/src/VideoPlayer/MediaControls/TopControls.tsx +0 -100
- package/src/VideoPlayer/Settings/AudioAndSubtitles.tsx +0 -295
- package/src/VideoPlayer/Settings/Episodes.tsx +0 -297
- package/src/VideoPlayer/Settings/SettingModal.tsx +0 -127
- package/src/VideoPlayer/Settings/SpeedControls.tsx +0 -130
- package/src/VideoPlayer/Settings/VideoPlayerSettings.tsx +0 -141
- package/src/VideoPlayer/VideoPlayerCore.tsx +0 -356
- package/src/VideoPlayer/components/ProgressBar.tsx +0 -211
- package/src/VideoPlayer/components/SkipAndNextControls.tsx +0 -192
- package/src/VideoPlayer/components/SubtitleView.tsx +0 -53
- package/src/VideoPlayer/components/Toast.tsx +0 -61
- package/src/VideoPlayer/context/VideoPlayerConfig.tsx +0 -65
- package/src/VideoPlayer/context/index.ts +0 -5
- package/src/VideoPlayer/index.ts +0 -4
- package/src/VideoPlayer/store/index.ts +0 -2
- package/src/VideoPlayer/store/videoPlayer.type.ts +0 -214
- package/src/VideoPlayer/store/videoPlayerStore.ts +0 -97
- package/src/VideoPlayer/styles/globalStyles.ts +0 -73
- package/src/VideoPlayer/utils/display/Display.ts +0 -10
- package/src/VideoPlayer/utils/display/index.ts +0 -1
- package/src/VideoPlayer/utils/format/index.ts +0 -1
- package/src/VideoPlayer/utils/format/timeFormatter.ts +0 -44
- package/src/VideoPlayer/utils/hooks/index.ts +0 -5
- package/src/VideoPlayer/utils/hooks/useAdEventHandler.ts +0 -95
- package/src/VideoPlayer/utils/hooks/useOrientationLock.ts +0 -29
- package/src/VideoPlayer/utils/hooks/usePauseVideoOnAd.ts +0 -46
- package/src/VideoPlayer/utils/hooks/useVideoPlayerBack.ts +0 -66
- package/src/VideoPlayer/utils/hooks/useVideoResolutions.ts +0 -125
- package/src/VideoPlayer/utils/index.ts +0 -6
- package/src/VideoPlayer/utils/platform/PlatformSelector.ts +0 -13
- package/src/VideoPlayer/utils/platform/index.ts +0 -2
- package/src/VideoPlayer/utils/platform/lockOrientation.ts +0 -40
- package/src/VideoPlayer/utils/player/index.ts +0 -2
- package/src/VideoPlayer/utils/player/playerEvents.ts +0 -97
- package/src/VideoPlayer/utils/player/useWatchReporter.ts +0 -105
- package/src/VideoPlayer/utils/video/index.ts +0 -5
- package/src/VideoPlayer/utils/video/videoControl.ts +0 -185
- package/src/VideoPlayer/utils/video/videoRef.ts +0 -21
- package/src/VideoPlayer/utils/video/videoResume.ts +0 -23
- package/src/VideoPlayer/utils/video/videoSource.ts +0 -23
- package/src/VideoPlayer.tsx +0 -181
- package/src/index.tsx +0 -3
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useCallback,
|
|
3
|
-
useEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
8
|
-
import { AppState, View, type AppStateStatus } from 'react-native';
|
|
9
|
-
import Video, {
|
|
10
|
-
type OnProgressData,
|
|
11
|
-
type SelectedTrack,
|
|
12
|
-
} from 'react-native-video';
|
|
13
|
-
import { type EdgeInsets } from 'react-native-safe-area-context';
|
|
14
|
-
import {
|
|
15
|
-
Gesture,
|
|
16
|
-
GestureDetector,
|
|
17
|
-
GestureHandlerRootView,
|
|
18
|
-
} from 'react-native-gesture-handler';
|
|
19
|
-
import {
|
|
20
|
-
runOnJS,
|
|
21
|
-
useSharedValue,
|
|
22
|
-
useAnimatedStyle,
|
|
23
|
-
withTiming,
|
|
24
|
-
} from 'react-native-reanimated';
|
|
25
|
-
|
|
26
|
-
import { hideControlsStyles, videoRef } from './utils';
|
|
27
|
-
import { getVideoSource, handlePause } from './utils';
|
|
28
|
-
import MediaControlsProvider from './MediaControls/MediaControlsProvider';
|
|
29
|
-
import { useVideoPlayerStore } from './store/videoPlayerStore';
|
|
30
|
-
import { useAdsPlayerStore } from '../AdsPlayer/store/adsPlayerStore';
|
|
31
|
-
import { createPlayerEvents } from './utils';
|
|
32
|
-
import globalStyles from './styles/globalStyles';
|
|
33
|
-
import { VideoPlayerConfigProvider, type VideoPlayerTheme } from './context';
|
|
34
|
-
import { useWatchReporter, type ExtendedWatchProgress } from './utils';
|
|
35
|
-
import type { MediaEpisode } from './store/videoPlayer.type';
|
|
36
|
-
import Toast from './components/Toast';
|
|
37
|
-
|
|
38
|
-
export interface VideoPlayerCoreProps {
|
|
39
|
-
onClose?: () => void;
|
|
40
|
-
isFocused?: boolean;
|
|
41
|
-
seekTime?: number | null;
|
|
42
|
-
mode?: 'fullscreen' | 'normal';
|
|
43
|
-
autoNext?: boolean;
|
|
44
|
-
event?: {
|
|
45
|
-
onPressEpisode?: ({
|
|
46
|
-
episode,
|
|
47
|
-
}: {
|
|
48
|
-
episode: MediaEpisode;
|
|
49
|
-
}) => Promise<boolean>;
|
|
50
|
-
};
|
|
51
|
-
theme?: Partial<VideoPlayerTheme>;
|
|
52
|
-
onWatchProgress?: (progress: ExtendedWatchProgress) => void;
|
|
53
|
-
insets: EdgeInsets;
|
|
54
|
-
isPausedOverride?: boolean;
|
|
55
|
-
onVideoEnd?: () => void;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const VideoPlayerCore: React.FC<VideoPlayerCoreProps> = ({
|
|
59
|
-
onClose,
|
|
60
|
-
seekTime,
|
|
61
|
-
event,
|
|
62
|
-
autoNext = true,
|
|
63
|
-
theme,
|
|
64
|
-
onWatchProgress,
|
|
65
|
-
insets,
|
|
66
|
-
isPausedOverride,
|
|
67
|
-
onVideoEnd,
|
|
68
|
-
}) => {
|
|
69
|
-
// Use separate selectors to avoid infinite loop (object selectors create new objects on each call)
|
|
70
|
-
const isPaused = useVideoPlayerStore((state) => state.isPaused);
|
|
71
|
-
const playBackRate = useVideoPlayerStore((state) => state.playBackRate);
|
|
72
|
-
const resizeMode = useVideoPlayerStore((state) => state.resizeMode);
|
|
73
|
-
const controlsVisible = useVideoPlayerStore((state) => state.controlsVisible);
|
|
74
|
-
const selectedAudioTrack = useVideoPlayerStore(
|
|
75
|
-
(state) => state.selectedAudioTrack
|
|
76
|
-
);
|
|
77
|
-
const selectedSubtitleTrack = useVideoPlayerStore(
|
|
78
|
-
(state) => state.selectedSubtitleTrack
|
|
79
|
-
);
|
|
80
|
-
const selectedVideoTrack = useVideoPlayerStore(
|
|
81
|
-
(state) => state.selectedVideoTrack
|
|
82
|
-
);
|
|
83
|
-
const maxBitRate = useVideoPlayerStore((state) => state.maxBitRate);
|
|
84
|
-
const playList = useVideoPlayerStore((state) => state.playList);
|
|
85
|
-
const currentTrackIndex = useVideoPlayerStore(
|
|
86
|
-
(state) => state.currentTrackIndex
|
|
87
|
-
);
|
|
88
|
-
const activeTrack = useVideoPlayerStore((state) => state.activeTrack);
|
|
89
|
-
|
|
90
|
-
const setResizeMode = useVideoPlayerStore((state) => state.setResizeMode);
|
|
91
|
-
const setControlsVisible = useVideoPlayerStore(
|
|
92
|
-
(state) => state.setControlsVisible
|
|
93
|
-
);
|
|
94
|
-
const setIsPaused = useVideoPlayerStore((state) => state.setIsPaused);
|
|
95
|
-
const setError = useVideoPlayerStore((state) => state.setError);
|
|
96
|
-
|
|
97
|
-
const playerEventsRef = useRef(createPlayerEvents());
|
|
98
|
-
const playerEvents = playerEventsRef.current;
|
|
99
|
-
const { reportProgress } = useWatchReporter({ onWatchProgress });
|
|
100
|
-
const seekTimeRef = useRef(seekTime);
|
|
101
|
-
|
|
102
|
-
// Toast state
|
|
103
|
-
const [toastMessage, setToastMessage] = useState<string | null>(null);
|
|
104
|
-
const toastOpacity = useSharedValue(0);
|
|
105
|
-
|
|
106
|
-
// Pinch gesture state
|
|
107
|
-
const pinchStartScale = useRef(1);
|
|
108
|
-
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
seekTimeRef.current = seekTime;
|
|
111
|
-
}, [seekTime]);
|
|
112
|
-
|
|
113
|
-
const effectivePaused = useMemo(
|
|
114
|
-
() => (isPausedOverride !== undefined ? isPausedOverride : isPaused),
|
|
115
|
-
[isPausedOverride, isPaused]
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const videoSource = useMemo(
|
|
119
|
-
() => getVideoSource({ playList, currentTrackIndex }),
|
|
120
|
-
[playList, currentTrackIndex]
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const selectedAudioTrackMemo = useMemo(
|
|
124
|
-
() => selectedAudioTrack as SelectedTrack,
|
|
125
|
-
[selectedAudioTrack]
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const selectedTextTrackMemo = useMemo(
|
|
129
|
-
() =>
|
|
130
|
-
selectedSubtitleTrack?.value === 'Off'
|
|
131
|
-
? undefined
|
|
132
|
-
: (selectedSubtitleTrack as SelectedTrack),
|
|
133
|
-
[selectedSubtitleTrack]
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const selectedVideoTrackMemo = useMemo(
|
|
137
|
-
() =>
|
|
138
|
-
selectedVideoTrack
|
|
139
|
-
? {
|
|
140
|
-
type: selectedVideoTrack.type,
|
|
141
|
-
value: selectedVideoTrack.value,
|
|
142
|
-
}
|
|
143
|
-
: undefined,
|
|
144
|
-
[selectedVideoTrack]
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Memoize video style for performance
|
|
148
|
-
const videoStyle = useMemo(() => globalStyles.absoluteFill, []);
|
|
149
|
-
|
|
150
|
-
// Memoize buffer config for performance
|
|
151
|
-
const bufferConfig = useMemo(
|
|
152
|
-
() => ({
|
|
153
|
-
minBufferMs: 15000,
|
|
154
|
-
maxBufferMs: 50000,
|
|
155
|
-
bufferForPlaybackMs: 2500,
|
|
156
|
-
bufferForPlaybackAfterRebufferMs: 5000,
|
|
157
|
-
}),
|
|
158
|
-
[]
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (seekTimeRef.current && !activeTrack?.isTrailer && videoRef?.current) {
|
|
163
|
-
videoRef.current.seek(seekTimeRef.current);
|
|
164
|
-
}
|
|
165
|
-
}, [seekTime, activeTrack?.isTrailer]);
|
|
166
|
-
|
|
167
|
-
const handleAppStateChange = useCallback(
|
|
168
|
-
(nextState: AppStateStatus) => {
|
|
169
|
-
if (nextState === 'inactive' || nextState === 'background') {
|
|
170
|
-
handlePause();
|
|
171
|
-
setIsPaused(true);
|
|
172
|
-
reportProgress('PROGRESS');
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
[setIsPaused, reportProgress]
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
useEffect(() => {
|
|
179
|
-
const sub = AppState.addEventListener('change', handleAppStateChange);
|
|
180
|
-
return () => sub.remove();
|
|
181
|
-
}, [handleAppStateChange]);
|
|
182
|
-
|
|
183
|
-
const handleProgress = useCallback(
|
|
184
|
-
(progressEvent: OnProgressData) => {
|
|
185
|
-
playerEvents.onProgress(progressEvent);
|
|
186
|
-
},
|
|
187
|
-
[playerEvents]
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const handleVideoEnd = useCallback(() => {
|
|
191
|
-
if (onVideoEnd) {
|
|
192
|
-
onVideoEnd();
|
|
193
|
-
} else {
|
|
194
|
-
playerEvents.onEnd({
|
|
195
|
-
reportProgress,
|
|
196
|
-
onPressEpisode: event?.onPressEpisode ?? (async () => true),
|
|
197
|
-
autoNext,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}, [
|
|
201
|
-
onVideoEnd,
|
|
202
|
-
playerEvents,
|
|
203
|
-
reportProgress,
|
|
204
|
-
event?.onPressEpisode,
|
|
205
|
-
autoNext,
|
|
206
|
-
]);
|
|
207
|
-
|
|
208
|
-
const handleError = useCallback(() => {
|
|
209
|
-
setError("Video couldn't be loaded");
|
|
210
|
-
}, [setError]);
|
|
211
|
-
|
|
212
|
-
const handleTap = useCallback(() => {
|
|
213
|
-
setControlsVisible(!controlsVisible);
|
|
214
|
-
}, [controlsVisible, setControlsVisible]);
|
|
215
|
-
|
|
216
|
-
const showToast = useCallback(
|
|
217
|
-
(message: string) => {
|
|
218
|
-
setToastMessage(message);
|
|
219
|
-
toastOpacity.value = withTiming(1, { duration: 200 });
|
|
220
|
-
setTimeout(() => {
|
|
221
|
-
toastOpacity.value = withTiming(0, { duration: 200 }, () => {
|
|
222
|
-
runOnJS(setToastMessage)(null);
|
|
223
|
-
});
|
|
224
|
-
}, 2000);
|
|
225
|
-
},
|
|
226
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
-
[]
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
const handlePinchEnd = useCallback(
|
|
231
|
-
(finalScale: number) => {
|
|
232
|
-
// Determine pinch direction based on scale change
|
|
233
|
-
// If scale increased (pinch out) → cover (zoomed to fill)
|
|
234
|
-
// If scale decreased (pinch in) → contain (original)
|
|
235
|
-
const scaleChange = finalScale - pinchStartScale.current;
|
|
236
|
-
const newMode = scaleChange > 0 ? 'cover' : 'contain';
|
|
237
|
-
setResizeMode(newMode);
|
|
238
|
-
|
|
239
|
-
// Show toast message
|
|
240
|
-
const message = newMode === 'cover' ? 'Zoomed to Fill' : 'Original';
|
|
241
|
-
showToast(message);
|
|
242
|
-
},
|
|
243
|
-
[setResizeMode, showToast]
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
// Animated style for toast
|
|
247
|
-
const toastAnimatedStyle = useAnimatedStyle(() => {
|
|
248
|
-
return {
|
|
249
|
-
opacity: toastOpacity.value,
|
|
250
|
-
};
|
|
251
|
-
}, []);
|
|
252
|
-
|
|
253
|
-
const pinchGesture = useMemo(
|
|
254
|
-
() =>
|
|
255
|
-
Gesture.Pinch()
|
|
256
|
-
.onStart((pinchEvent) => {
|
|
257
|
-
'worklet';
|
|
258
|
-
pinchStartScale.current = pinchEvent.scale;
|
|
259
|
-
})
|
|
260
|
-
.onEnd((pinchEvent) => {
|
|
261
|
-
'worklet';
|
|
262
|
-
runOnJS(handlePinchEnd)(pinchEvent.scale);
|
|
263
|
-
}),
|
|
264
|
-
[handlePinchEnd]
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const tapGesture = useMemo(
|
|
268
|
-
() =>
|
|
269
|
-
Gesture.Tap()
|
|
270
|
-
.onEnd(() => {
|
|
271
|
-
runOnJS(handleTap)();
|
|
272
|
-
})
|
|
273
|
-
.maxDuration(250),
|
|
274
|
-
[handleTap]
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const composedGestures = useMemo(
|
|
278
|
-
() => Gesture.Race(pinchGesture, tapGesture),
|
|
279
|
-
[pinchGesture, tapGesture]
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
const handleClose = useCallback(() => {
|
|
283
|
-
reportProgress('VIDEO_CLOSE');
|
|
284
|
-
// Reset stores after report is sent
|
|
285
|
-
setTimeout(() => {
|
|
286
|
-
const { resetStore } = useVideoPlayerStore.getState();
|
|
287
|
-
const { resetAdsStore } = useAdsPlayerStore.getState();
|
|
288
|
-
resetStore();
|
|
289
|
-
resetAdsStore();
|
|
290
|
-
}, 0);
|
|
291
|
-
onClose?.();
|
|
292
|
-
}, [reportProgress, onClose]);
|
|
293
|
-
|
|
294
|
-
const handlePressEpisode = useCallback(
|
|
295
|
-
async ({ episode }: { episode: MediaEpisode }) => {
|
|
296
|
-
if (event?.onPressEpisode) {
|
|
297
|
-
return await event.onPressEpisode({ episode });
|
|
298
|
-
}
|
|
299
|
-
return Promise.resolve(true);
|
|
300
|
-
},
|
|
301
|
-
[event]
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<VideoPlayerConfigProvider theme={theme}>
|
|
306
|
-
<GestureHandlerRootView style={globalStyles.flexOne}>
|
|
307
|
-
<MediaControlsProvider
|
|
308
|
-
onClose={handleClose}
|
|
309
|
-
onPressEpisode={handlePressEpisode}
|
|
310
|
-
reportProgress={reportProgress}
|
|
311
|
-
insets={insets}
|
|
312
|
-
>
|
|
313
|
-
<GestureDetector gesture={composedGestures}>
|
|
314
|
-
<View style={globalStyles.flexOneWithBlackBackground}>
|
|
315
|
-
<Video
|
|
316
|
-
ref={videoRef}
|
|
317
|
-
source={videoSource}
|
|
318
|
-
paused={effectivePaused}
|
|
319
|
-
onProgress={handleProgress}
|
|
320
|
-
onLoad={playerEvents.onLoad}
|
|
321
|
-
onBuffer={playerEvents.onBuffer}
|
|
322
|
-
onEnd={handleVideoEnd}
|
|
323
|
-
onLoadStart={playerEvents.onLoadStart}
|
|
324
|
-
onError={handleError}
|
|
325
|
-
controls={false}
|
|
326
|
-
resizeMode={resizeMode || 'contain'}
|
|
327
|
-
rate={playBackRate || 1}
|
|
328
|
-
selectedAudioTrack={selectedAudioTrackMemo}
|
|
329
|
-
selectedTextTrack={selectedTextTrackMemo}
|
|
330
|
-
selectedVideoTrack={selectedVideoTrackMemo}
|
|
331
|
-
maxBitRate={maxBitRate ?? undefined}
|
|
332
|
-
style={videoStyle}
|
|
333
|
-
controlsStyles={hideControlsStyles}
|
|
334
|
-
ignoreSilentSwitch="ignore"
|
|
335
|
-
playInBackground={false}
|
|
336
|
-
playWhenInactive={false}
|
|
337
|
-
allowsExternalPlayback={true}
|
|
338
|
-
bufferConfig={bufferConfig}
|
|
339
|
-
/>
|
|
340
|
-
{toastMessage && (
|
|
341
|
-
<Toast
|
|
342
|
-
message={toastMessage}
|
|
343
|
-
animatedStyle={toastAnimatedStyle}
|
|
344
|
-
insets={insets}
|
|
345
|
-
position="top"
|
|
346
|
-
/>
|
|
347
|
-
)}
|
|
348
|
-
</View>
|
|
349
|
-
</GestureDetector>
|
|
350
|
-
</MediaControlsProvider>
|
|
351
|
-
</GestureHandlerRootView>
|
|
352
|
-
</VideoPlayerConfigProvider>
|
|
353
|
-
);
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
export default React.memo(VideoPlayerCore);
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
/* eslint-disable react-hooks/exhaustive-deps */
|
|
2
|
-
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
|
|
3
|
-
import { View, StyleSheet } from 'react-native';
|
|
4
|
-
import { Slider } from 'react-native-awesome-slider';
|
|
5
|
-
import { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
|
|
6
|
-
import { moderateScale } from 'react-native-size-matters';
|
|
7
|
-
import { useVideoPlayerConfig } from '../context';
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
duration: number;
|
|
11
|
-
currentTime: number;
|
|
12
|
-
bufferedTime: number;
|
|
13
|
-
onSeek?: (time: number) => void;
|
|
14
|
-
showThumb?: boolean;
|
|
15
|
-
height?: number;
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
adMarkers?: number[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const SEEK_COOLDOWN_MS = 400;
|
|
21
|
-
const PROGRESS_ANIM_DURATION = 220;
|
|
22
|
-
const CACHE_ANIM_DURATION = 280;
|
|
23
|
-
const SMOOTH_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
|
24
|
-
|
|
25
|
-
const ProgressBar: React.FC<Props> = React.memo(
|
|
26
|
-
({
|
|
27
|
-
duration,
|
|
28
|
-
currentTime,
|
|
29
|
-
bufferedTime,
|
|
30
|
-
onSeek,
|
|
31
|
-
showThumb = true,
|
|
32
|
-
disabled = false,
|
|
33
|
-
adMarkers = [],
|
|
34
|
-
height,
|
|
35
|
-
}) => {
|
|
36
|
-
const { colors, metrics } = useVideoPlayerConfig();
|
|
37
|
-
const sliderHeight = height ?? metrics.sliderHeight;
|
|
38
|
-
const thumbSize = useMemo(() => {
|
|
39
|
-
return sliderHeight * 3;
|
|
40
|
-
}, [sliderHeight]);
|
|
41
|
-
|
|
42
|
-
const progress = useSharedValue(currentTime);
|
|
43
|
-
const cache = useSharedValue(bufferedTime);
|
|
44
|
-
const min = useSharedValue(0);
|
|
45
|
-
const max = useSharedValue(duration);
|
|
46
|
-
|
|
47
|
-
const isSlidingRef = useRef(false);
|
|
48
|
-
const seekCooldownUntilRef = useRef(0);
|
|
49
|
-
const seekCooldownTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
50
|
-
null
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (isSlidingRef.current) return;
|
|
55
|
-
if (Date.now() < seekCooldownUntilRef.current) return;
|
|
56
|
-
progress.value = withTiming(currentTime, {
|
|
57
|
-
duration: PROGRESS_ANIM_DURATION,
|
|
58
|
-
easing: SMOOTH_EASING,
|
|
59
|
-
});
|
|
60
|
-
}, [currentTime]);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
cache.value = withTiming(bufferedTime, {
|
|
64
|
-
duration: CACHE_ANIM_DURATION,
|
|
65
|
-
easing: SMOOTH_EASING,
|
|
66
|
-
});
|
|
67
|
-
}, [bufferedTime]);
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
max.value = duration;
|
|
71
|
-
}, [duration]);
|
|
72
|
-
|
|
73
|
-
const adPositions = useMemo(() => {
|
|
74
|
-
if (duration <= 0) return [];
|
|
75
|
-
return adMarkers.map((t) => (t / duration) * 100);
|
|
76
|
-
}, [adMarkers, duration]);
|
|
77
|
-
|
|
78
|
-
const handleSeek = useCallback(
|
|
79
|
-
(value: number) => {
|
|
80
|
-
if (!disabled) onSeek?.(value);
|
|
81
|
-
},
|
|
82
|
-
[onSeek, disabled]
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const handleSlidingStart = useCallback(() => {
|
|
86
|
-
isSlidingRef.current = true;
|
|
87
|
-
if (seekCooldownTimeoutRef.current) {
|
|
88
|
-
clearTimeout(seekCooldownTimeoutRef.current);
|
|
89
|
-
seekCooldownTimeoutRef.current = null;
|
|
90
|
-
}
|
|
91
|
-
}, []);
|
|
92
|
-
|
|
93
|
-
const handleSlidingComplete = useCallback(
|
|
94
|
-
(value: number) => {
|
|
95
|
-
progress.value = value;
|
|
96
|
-
seekCooldownUntilRef.current = Date.now() + SEEK_COOLDOWN_MS;
|
|
97
|
-
seekCooldownTimeoutRef.current = setTimeout(() => {
|
|
98
|
-
isSlidingRef.current = false;
|
|
99
|
-
seekCooldownTimeoutRef.current = null;
|
|
100
|
-
}, SEEK_COOLDOWN_MS);
|
|
101
|
-
},
|
|
102
|
-
[progress]
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
return () => {
|
|
107
|
-
if (seekCooldownTimeoutRef.current) {
|
|
108
|
-
clearTimeout(seekCooldownTimeoutRef.current);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<View style={[styles.container, { height: thumbSize }]}>
|
|
115
|
-
<Slider
|
|
116
|
-
progress={progress}
|
|
117
|
-
minimumValue={min}
|
|
118
|
-
maximumValue={max}
|
|
119
|
-
cache={cache}
|
|
120
|
-
disable={disabled}
|
|
121
|
-
onSlidingStart={handleSlidingStart}
|
|
122
|
-
onSlidingComplete={handleSlidingComplete}
|
|
123
|
-
onValueChange={handleSeek}
|
|
124
|
-
containerStyle={[
|
|
125
|
-
styles.sliderContainer,
|
|
126
|
-
{
|
|
127
|
-
borderRadius: sliderHeight / 2,
|
|
128
|
-
height: sliderHeight,
|
|
129
|
-
},
|
|
130
|
-
]}
|
|
131
|
-
renderThumb={
|
|
132
|
-
showThumb
|
|
133
|
-
? () => (
|
|
134
|
-
<View
|
|
135
|
-
style={{
|
|
136
|
-
backgroundColor: colors.primary,
|
|
137
|
-
width: thumbSize,
|
|
138
|
-
height: thumbSize,
|
|
139
|
-
borderRadius: thumbSize / 2,
|
|
140
|
-
}}
|
|
141
|
-
/>
|
|
142
|
-
)
|
|
143
|
-
: () => null
|
|
144
|
-
}
|
|
145
|
-
renderBubble={() => null}
|
|
146
|
-
thumbWidth={showThumb ? thumbSize : 0}
|
|
147
|
-
theme={{
|
|
148
|
-
minimumTrackTintColor: colors.primary,
|
|
149
|
-
maximumTrackTintColor: colors.secondary,
|
|
150
|
-
cacheTrackTintColor: colors.buffer,
|
|
151
|
-
disableMinTrackTintColor: colors.primary,
|
|
152
|
-
bubbleBackgroundColor: colors.white,
|
|
153
|
-
}}
|
|
154
|
-
/>
|
|
155
|
-
|
|
156
|
-
{/* Ad Markers */}
|
|
157
|
-
{adPositions.length > 0 && (
|
|
158
|
-
<View style={styles.adMarkersContainer} pointerEvents="none">
|
|
159
|
-
{adPositions.map((pos, index) => (
|
|
160
|
-
<View
|
|
161
|
-
key={`ad-${index}`}
|
|
162
|
-
style={[
|
|
163
|
-
styles.adMarker,
|
|
164
|
-
{
|
|
165
|
-
left: `${pos}%`,
|
|
166
|
-
backgroundColor: colors.primary,
|
|
167
|
-
width: sliderHeight,
|
|
168
|
-
},
|
|
169
|
-
]}
|
|
170
|
-
/>
|
|
171
|
-
))}
|
|
172
|
-
</View>
|
|
173
|
-
)}
|
|
174
|
-
</View>
|
|
175
|
-
);
|
|
176
|
-
},
|
|
177
|
-
(p, n) =>
|
|
178
|
-
p.currentTime === n.currentTime &&
|
|
179
|
-
p.bufferedTime === n.bufferedTime &&
|
|
180
|
-
p.duration === n.duration &&
|
|
181
|
-
p.disabled === n.disabled &&
|
|
182
|
-
p.showThumb === n.showThumb &&
|
|
183
|
-
p.height === n.height &&
|
|
184
|
-
JSON.stringify(p.adMarkers) === JSON.stringify(n.adMarkers)
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
export default ProgressBar;
|
|
188
|
-
|
|
189
|
-
const styles = StyleSheet.create({
|
|
190
|
-
container: {
|
|
191
|
-
position: 'relative',
|
|
192
|
-
width: '100%',
|
|
193
|
-
overflow: 'hidden',
|
|
194
|
-
justifyContent: 'center',
|
|
195
|
-
},
|
|
196
|
-
sliderContainer: {
|
|
197
|
-
overflow: 'hidden',
|
|
198
|
-
},
|
|
199
|
-
adMarkersContainer: {
|
|
200
|
-
position: 'absolute',
|
|
201
|
-
width: '100%',
|
|
202
|
-
height: '100%',
|
|
203
|
-
justifyContent: 'center',
|
|
204
|
-
},
|
|
205
|
-
adMarker: {
|
|
206
|
-
position: 'absolute',
|
|
207
|
-
aspectRatio: 1,
|
|
208
|
-
top: '50%',
|
|
209
|
-
transform: [{ translateY: -moderateScale(2.5) }],
|
|
210
|
-
},
|
|
211
|
-
});
|