@zezosoft/zezo-ott-react-native-video-player 1.0.2 → 1.0.4

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.
Files changed (45) hide show
  1. package/README.md +24 -26
  2. package/lib/module/AdsPlayer/AdsPlayer.js +7 -11
  3. package/lib/module/AdsPlayer/AdsPlayer.js.map +1 -1
  4. package/lib/module/AdsPlayer/MediaControls/AdBottomControls.js +3 -1
  5. package/lib/module/AdsPlayer/MediaControls/AdBottomControls.js.map +1 -1
  6. package/lib/module/AdsPlayer/MediaControls/AdMediaControls.js +12 -7
  7. package/lib/module/AdsPlayer/MediaControls/AdMediaControls.js.map +1 -1
  8. package/lib/module/AdsPlayer/MediaControls/AdTopControls.js +5 -17
  9. package/lib/module/AdsPlayer/MediaControls/AdTopControls.js.map +1 -1
  10. package/lib/module/VideoPlayer/MediaControls/BottomControls.js +15 -7
  11. package/lib/module/VideoPlayer/MediaControls/BottomControls.js.map +1 -1
  12. package/lib/module/VideoPlayer/MediaControls/MediaControls.js +4 -5
  13. package/lib/module/VideoPlayer/MediaControls/MediaControls.js.map +1 -1
  14. package/lib/module/VideoPlayer/MediaControls/MediaControlsProvider.js +8 -5
  15. package/lib/module/VideoPlayer/MediaControls/MediaControlsProvider.js.map +1 -1
  16. package/lib/module/VideoPlayer/MediaControls/TopControls.js +4 -11
  17. package/lib/module/VideoPlayer/MediaControls/TopControls.js.map +1 -1
  18. package/lib/module/VideoPlayer/components/ProgressBar.js +39 -4
  19. package/lib/module/VideoPlayer/components/ProgressBar.js.map +1 -1
  20. package/lib/module/VideoPlayer/utils/hooks/useVideoResolutions.js +11 -7
  21. package/lib/module/VideoPlayer/utils/hooks/useVideoResolutions.js.map +1 -1
  22. package/lib/module/VideoPlayer.js +26 -14
  23. package/lib/module/VideoPlayer.js.map +1 -1
  24. package/lib/typescript/src/AdsPlayer/AdsPlayer.d.ts.map +1 -1
  25. package/lib/typescript/src/AdsPlayer/MediaControls/AdBottomControls.d.ts.map +1 -1
  26. package/lib/typescript/src/AdsPlayer/MediaControls/AdMediaControls.d.ts.map +1 -1
  27. package/lib/typescript/src/AdsPlayer/MediaControls/AdTopControls.d.ts.map +1 -1
  28. package/lib/typescript/src/VideoPlayer/MediaControls/MediaControls.d.ts.map +1 -1
  29. package/lib/typescript/src/VideoPlayer/MediaControls/MediaControlsProvider.d.ts.map +1 -1
  30. package/lib/typescript/src/VideoPlayer/MediaControls/TopControls.d.ts.map +1 -1
  31. package/lib/typescript/src/VideoPlayer/components/ProgressBar.d.ts.map +1 -1
  32. package/lib/typescript/src/VideoPlayer/utils/hooks/useVideoResolutions.d.ts.map +1 -1
  33. package/lib/typescript/src/VideoPlayer.d.ts.map +1 -1
  34. package/package.json +6 -3
  35. package/src/AdsPlayer/AdsPlayer.tsx +12 -13
  36. package/src/AdsPlayer/MediaControls/AdBottomControls.tsx +3 -1
  37. package/src/AdsPlayer/MediaControls/AdMediaControls.tsx +13 -7
  38. package/src/AdsPlayer/MediaControls/AdTopControls.tsx +8 -13
  39. package/src/VideoPlayer/MediaControls/BottomControls.tsx +13 -7
  40. package/src/VideoPlayer/MediaControls/MediaControls.tsx +4 -4
  41. package/src/VideoPlayer/MediaControls/MediaControlsProvider.tsx +9 -5
  42. package/src/VideoPlayer/MediaControls/TopControls.tsx +4 -10
  43. package/src/VideoPlayer/components/ProgressBar.tsx +53 -4
  44. package/src/VideoPlayer/utils/hooks/useVideoResolutions.ts +17 -9
  45. package/src/VideoPlayer.tsx +44 -29
@@ -23,8 +23,14 @@ const AdTopControls: React.FC<AdTopControlsProps> = ({ onClose }) => {
23
23
  const controlsVisible = useVideoPlayerStore((state) => state.controlsVisible);
24
24
  const currentAd = useAdsPlayerStore((state) => state.currentAd);
25
25
  const adCurrentTime = useAdsPlayerStore((state) => state.adCurrentTime);
26
+ const adDuration = useAdsPlayerStore((state) => state.adDuration);
26
27
  const fadeAnim = useAdControlsAutoHide();
27
28
 
29
+ const adTimeRemaining = useMemo(
30
+ () => Math.max(0, (currentAd?.duration ?? adDuration) - adCurrentTime),
31
+ [currentAd?.duration, adDuration, adCurrentTime]
32
+ );
33
+
28
34
  // Memoize styles to avoid recalculation
29
35
  const adLabelContainerStyle = useMemo(
30
36
  () => [
@@ -58,7 +64,7 @@ const AdTopControls: React.FC<AdTopControlsProps> = ({ onClose }) => {
58
64
  <Text style={styles.adLabelText}>Ad</Text>
59
65
  </View>
60
66
  <View style={styles.separator} />
61
- <Text style={styles.adTimeText}>{formatTime(adCurrentTime)}</Text>
67
+ <Text style={styles.adTimeText}>{formatTime(adTimeRemaining)}</Text>
62
68
  </View>
63
69
  </Animated.View>
64
70
  )}
@@ -81,7 +87,7 @@ const styles = StyleSheet.create({
81
87
  container: {
82
88
  flexDirection: 'row',
83
89
  justifyContent: 'space-between',
84
- alignItems: 'flex-start',
90
+ alignItems: 'center',
85
91
  marginTop: moderateScale(8),
86
92
  paddingHorizontal: moderateScale(10),
87
93
  paddingBottom: 0,
@@ -179,17 +185,6 @@ const styles = StyleSheet.create({
179
185
  alignItems: 'center',
180
186
  borderWidth: 1,
181
187
  borderColor: 'rgba(255, 255, 255, 0.2)',
182
- ...Platform.select({
183
- ios: {
184
- shadowColor: '#000',
185
- shadowOffset: { width: 0, height: 2 },
186
- shadowOpacity: 0.25,
187
- shadowRadius: 3,
188
- },
189
- android: {
190
- elevation: 3,
191
- },
192
- }),
193
188
  },
194
189
  });
195
190
 
@@ -109,7 +109,7 @@ const BottomControls = () => {
109
109
  );
110
110
 
111
111
  return (
112
- <View>
112
+ <View style={styles.root}>
113
113
  {/* Progress Bar Section */}
114
114
  <View style={styles.sliderContainer}>
115
115
  <View style={styles.sliderWrapper}>
@@ -121,7 +121,7 @@ const BottomControls = () => {
121
121
  adMarkers={adMarkers}
122
122
  />
123
123
  </View>
124
- <Text style={timeTextStyle}>
124
+ <Text style={timeTextStyle} numberOfLines={1}>
125
125
  {getRemainingTime(duration, currentTime)}
126
126
  </Text>
127
127
  </View>
@@ -166,35 +166,41 @@ const BottomControls = () => {
166
166
  export default BottomControls;
167
167
 
168
168
  const styles = StyleSheet.create({
169
+ root: {
170
+ paddingTop: verticalScale(8),
171
+ paddingBottom: verticalScale(4),
172
+ },
169
173
  sliderContainer: {
170
174
  flexDirection: 'row',
171
175
  alignItems: 'center',
172
176
  paddingHorizontal: scale(12),
177
+ marginBottom: verticalScale(4),
173
178
  },
174
179
  sliderWrapper: {
175
180
  flex: 1,
176
181
  minWidth: 0,
177
182
  },
178
183
  timeText: {
179
- width: scale(50),
184
+ minWidth: scale(52),
185
+ maxWidth: scale(56),
180
186
  textAlign: 'right',
181
- marginLeft: scale(4),
187
+ marginLeft: scale(6),
182
188
  fontSize: RFValue(12),
183
189
  fontWeight: '500',
184
190
  },
185
191
  buttonsContainer: {
186
192
  flexDirection: 'row',
187
- justifyContent: 'center',
188
193
  flexWrap: 'wrap',
194
+ justifyContent: 'center',
195
+ alignContent: 'center',
189
196
  paddingHorizontal: scale(10),
197
+ gap: scale(4),
190
198
  },
191
199
  button: {
192
200
  flexDirection: 'row',
193
201
  alignItems: 'center',
194
202
  paddingHorizontal: scale(12),
195
203
  paddingVertical: verticalScale(6),
196
- marginHorizontal: scale(6),
197
- marginVertical: verticalScale(4),
198
204
  borderRadius: moderateScale(8),
199
205
  },
200
206
  buttonText: {
@@ -1,6 +1,5 @@
1
1
  import { memo } from 'react';
2
- import { StyleSheet } from 'react-native';
3
- import { SafeAreaView } from 'react-native-safe-area-context';
2
+ import { StyleSheet, View } from 'react-native';
4
3
  import TopControls from './TopControls';
5
4
  import MiddleControls from './MiddleControls';
6
5
  import BottomControls from './BottomControls';
@@ -11,11 +10,11 @@ export type MediaControlsProps = {
11
10
 
12
11
  const MediaControls: React.FC<MediaControlsProps> = ({ onClose }) => {
13
12
  return (
14
- <SafeAreaView style={styles.container} edges={['top']}>
13
+ <View style={styles.container}>
15
14
  <TopControls onClose={onClose} />
16
15
  <MiddleControls />
17
16
  <BottomControls />
18
- </SafeAreaView>
17
+ </View>
19
18
  );
20
19
  };
21
20
 
@@ -26,5 +25,6 @@ const styles = StyleSheet.create({
26
25
  flex: 1,
27
26
  flexDirection: 'column',
28
27
  justifyContent: 'space-between',
28
+ minHeight: 0,
29
29
  },
30
30
  });
@@ -11,6 +11,10 @@ import globalStyles from '../styles/globalStyles';
11
11
  import type { ExtendedWatchProgress } from '../utils';
12
12
  import SubtitleView from '../components/SubtitleView';
13
13
 
14
+ const HORIZONTAL_PADDING = moderateScale(12);
15
+ const BOTTOM_PADDING_EXTRA = moderateScale(8);
16
+ const MIN_TOP_PADDING = moderateScale(10);
17
+
14
18
  interface MediaControlsProviderProps extends MediaControlsProps {
15
19
  children: React.ReactNode;
16
20
  onPressEpisode: ({ episode }: { episode: MediaEpisode }) => Promise<boolean>;
@@ -34,13 +38,13 @@ const MediaControlsProvider: React.FC<MediaControlsProviderProps> = ({
34
38
  const timeoutId = React.useRef<NodeJS.Timeout | null>(null);
35
39
  const fadeAnim = useRef(new Animated.Value(0)).current;
36
40
 
37
- // Memoize container padding to avoid recalculation on every render
41
+ // Safe-area padding: never negative, single source of truth (MediaControls uses View, not SafeAreaView)
38
42
  const containerPadding: ViewStyle = useMemo(
39
43
  () => ({
40
- paddingTop: top,
41
- paddingBottom: bottom + moderateScale(5),
42
- paddingLeft: left + moderateScale(10),
43
- paddingRight: right + moderateScale(10),
44
+ paddingTop: top - MIN_TOP_PADDING,
45
+ paddingBottom: bottom + BOTTOM_PADDING_EXTRA,
46
+ paddingLeft: left + HORIZONTAL_PADDING,
47
+ paddingRight: right + HORIZONTAL_PADDING,
44
48
  }),
45
49
  [top, bottom, left, right]
46
50
  );
@@ -1,10 +1,9 @@
1
- import React, { useMemo } from 'react';
1
+ import React from 'react';
2
2
  import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
3
3
  import { moderateScale } from 'react-native-size-matters';
4
4
  import { RFValue } from 'react-native-responsive-fontsize';
5
5
  import { X } from 'lucide-react-native';
6
6
  import { lockToPortrait } from '../utils';
7
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
8
7
  import { useVideoPlayerConfig } from '../context';
9
8
  import { useVideoPlayerStore } from '../store/videoPlayerStore';
10
9
 
@@ -14,7 +13,6 @@ interface TopControlsProps {
14
13
 
15
14
  const TopControls: React.FC<TopControlsProps> = ({ onClose }) => {
16
15
  const { activeTrack } = useVideoPlayerStore();
17
- const { top } = useSafeAreaInsets();
18
16
  const { colors } = useVideoPlayerConfig();
19
17
  const handleClose = () => {
20
18
  // onClose already handles reportProgress and store reset in VideoPlayerCore
@@ -27,14 +25,8 @@ const TopControls: React.FC<TopControlsProps> = ({ onClose }) => {
27
25
  activeTrack?.seasonNumber !== undefined ||
28
26
  activeTrack?.episodeNumber !== undefined;
29
27
 
30
- // Memoize container style to avoid recalculation
31
- const containerStyle = useMemo(
32
- () => [styles.container, { paddingTop: top + moderateScale(10) }],
33
- [top]
34
- );
35
-
36
28
  return (
37
- <View style={containerStyle}>
29
+ <View style={styles.container}>
38
30
  <View style={styles.textContainer}>
39
31
  <Text
40
32
  numberOfLines={1}
@@ -81,10 +73,12 @@ const styles = StyleSheet.create({
81
73
  justifyContent: 'space-between',
82
74
  alignItems: 'center',
83
75
  paddingHorizontal: moderateScale(16),
76
+ paddingVertical: moderateScale(8),
84
77
  zIndex: 51,
85
78
  },
86
79
  textContainer: {
87
80
  flex: 1,
81
+ minWidth: 0,
88
82
  paddingRight: moderateScale(10),
89
83
  },
90
84
  title: {
@@ -1,8 +1,8 @@
1
1
  /* eslint-disable react-hooks/exhaustive-deps */
2
- import React, { useEffect, useMemo, useCallback } from 'react';
2
+ import React, { useEffect, useMemo, useCallback, useRef } from 'react';
3
3
  import { View, StyleSheet } from 'react-native';
4
4
  import { Slider } from 'react-native-awesome-slider';
5
- import { useSharedValue, withTiming } from 'react-native-reanimated';
5
+ import { useSharedValue, withTiming, Easing } from 'react-native-reanimated';
6
6
  import { moderateScale } from 'react-native-size-matters';
7
7
  import { useVideoPlayerConfig } from '../context';
8
8
 
@@ -17,6 +17,11 @@ interface Props {
17
17
  adMarkers?: number[];
18
18
  }
19
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
+
20
25
  const ProgressBar: React.FC<Props> = React.memo(
21
26
  ({
22
27
  duration,
@@ -39,12 +44,26 @@ const ProgressBar: React.FC<Props> = React.memo(
39
44
  const min = useSharedValue(0);
40
45
  const max = useSharedValue(duration);
41
46
 
47
+ const isSlidingRef = useRef(false);
48
+ const seekCooldownUntilRef = useRef(0);
49
+ const seekCooldownTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
50
+ null
51
+ );
52
+
42
53
  useEffect(() => {
43
- progress.value = withTiming(currentTime, { duration: 160 });
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
+ });
44
60
  }, [currentTime]);
45
61
 
46
62
  useEffect(() => {
47
- cache.value = withTiming(bufferedTime, { duration: 180 });
63
+ cache.value = withTiming(bufferedTime, {
64
+ duration: CACHE_ANIM_DURATION,
65
+ easing: SMOOTH_EASING,
66
+ });
48
67
  }, [bufferedTime]);
49
68
 
50
69
  useEffect(() => {
@@ -63,6 +82,34 @@ const ProgressBar: React.FC<Props> = React.memo(
63
82
  [onSeek, disabled]
64
83
  );
65
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
+
66
113
  return (
67
114
  <View style={[styles.container, { height: thumbSize }]}>
68
115
  <Slider
@@ -71,6 +118,8 @@ const ProgressBar: React.FC<Props> = React.memo(
71
118
  maximumValue={max}
72
119
  cache={cache}
73
120
  disable={disabled}
121
+ onSlidingStart={handleSlidingStart}
122
+ onSlidingComplete={handleSlidingComplete}
74
123
  onValueChange={handleSeek}
75
124
  containerStyle={[
76
125
  styles.sliderContainer,
@@ -9,12 +9,15 @@ export interface StreamInfo {
9
9
  }
10
10
 
11
11
  async function getHLSBandwidthAndResolutions(
12
- m3u8Url: string
12
+ m3u8Url: string,
13
+ headers?: Record<string, string>
13
14
  ): Promise<StreamInfo[]> {
14
15
  try {
15
16
  if (!m3u8Url?.endsWith('.m3u8')) return [];
16
17
 
17
- const { data } = await axios.get<string>(m3u8Url);
18
+ const { data } = await axios.get<string>(m3u8Url, {
19
+ headers: headers ?? undefined,
20
+ });
18
21
  if (!data) return [];
19
22
 
20
23
  const lines = data.split('\n');
@@ -58,8 +61,7 @@ async function getHLSBandwidthAndResolutions(
58
61
  };
59
62
 
60
63
  return [autoStream, ...streams];
61
- } catch (error) {
62
- console.error('Error fetching or parsing HLS stream:', error);
64
+ } catch {
63
65
  return [];
64
66
  }
65
67
  }
@@ -87,30 +89,36 @@ export const useVideoResolutions = (track: MediaTrack | null) => {
87
89
  return;
88
90
  }
89
91
 
92
+ let cancelled = false;
93
+
90
94
  const fetchResolutions = async () => {
91
95
  try {
92
96
  const data = await getHLSBandwidthAndResolutions(source);
97
+ if (cancelled) return;
93
98
 
94
99
  const filteredData = data.filter(
95
- (item) => typeof item.height === 'number'
100
+ (item): item is StreamInfo & { height: number } =>
101
+ typeof item.height === 'number'
96
102
  );
97
103
 
98
104
  const newResolutions: Resolution[] = [
99
105
  { height: 'auto', bandwidth: null },
100
106
  ...filteredData.map((item) => ({
101
- height: item.height as number,
107
+ height: item.height,
102
108
  bandwidth: item.bandwidth,
103
109
  })),
104
110
  ];
105
111
 
106
112
  resolutionCache[source] = newResolutions;
107
113
  setResolutions(newResolutions);
108
- } catch (error) {
109
- console.error('Failed to fetch video resolutions', error);
110
- }
114
+ } catch {}
111
115
  };
112
116
 
113
117
  fetchResolutions();
118
+
119
+ return () => {
120
+ cancelled = true;
121
+ };
114
122
  }, [track]);
115
123
 
116
124
  return resolutions;
@@ -118,37 +118,45 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
118
118
  onClose?.();
119
119
  }, [onClose]);
120
120
 
121
+ const showAdsOverlay =
122
+ ads.length > 0 && isAdPlaying && currentAd && !activeTrack?.isTrailer;
123
+
121
124
  return (
122
125
  <View style={styles.container}>
123
- <VideoPlayerCore
124
- onClose={onClose}
125
- isFocused={isFocused}
126
- mode={mode}
127
- seekTime={seekTime}
128
- event={event}
129
- autoNext={autoNext}
130
- theme={theme}
131
- onWatchProgress={onWatchProgress}
132
- insets={insets}
133
- isPausedOverride={isAdPlaying ? true : isPaused}
134
- onVideoEnd={handleVideoEnd}
135
- />
136
- {ads.length > 0 &&
137
- isAdPlaying &&
138
- currentAd &&
139
- !activeTrack?.isTrailer && (
140
- <View style={styles.adsOverlay}>
141
- <AdsPlayer
142
- ref={adVideoRef}
143
- insets={insets}
144
- onAdEnd={handleAdEnd}
145
- onAdSkip={handleAdSkip}
146
- onAdError={handleAdError}
147
- onAdTracking={onAdTracking}
148
- onClose={handleAdClose}
149
- />
150
- </View>
151
- )}
126
+ <View
127
+ style={[
128
+ styles.mainPlayerWrapper,
129
+ showAdsOverlay && styles.mainPlayerHidden,
130
+ ]}
131
+ pointerEvents={showAdsOverlay ? 'none' : 'auto'}
132
+ >
133
+ <VideoPlayerCore
134
+ onClose={onClose}
135
+ isFocused={isFocused}
136
+ mode={mode}
137
+ seekTime={seekTime}
138
+ event={event}
139
+ autoNext={autoNext}
140
+ theme={theme}
141
+ onWatchProgress={onWatchProgress}
142
+ insets={insets}
143
+ isPausedOverride={isAdPlaying ? true : isPaused}
144
+ onVideoEnd={handleVideoEnd}
145
+ />
146
+ </View>
147
+ {showAdsOverlay && (
148
+ <View style={styles.adsOverlay}>
149
+ <AdsPlayer
150
+ ref={adVideoRef}
151
+ insets={insets}
152
+ onAdEnd={handleAdEnd}
153
+ onAdSkip={handleAdSkip}
154
+ onAdError={handleAdError}
155
+ onAdTracking={onAdTracking}
156
+ onClose={handleAdClose}
157
+ />
158
+ </View>
159
+ )}
152
160
  </View>
153
161
  );
154
162
  };
@@ -157,9 +165,16 @@ const styles = StyleSheet.create({
157
165
  container: {
158
166
  flex: 1,
159
167
  },
168
+ mainPlayerWrapper: {
169
+ flex: 1,
170
+ },
171
+ mainPlayerHidden: {
172
+ opacity: 0,
173
+ },
160
174
  adsOverlay: {
161
175
  ...StyleSheet.absoluteFillObject,
162
176
  zIndex: 1000,
177
+ backgroundColor: 'black',
163
178
  },
164
179
  });
165
180